diff --git a/calib_logs.csv b/calib_logs.csv new file mode 100644 index 0000000..58855d5 --- /dev/null +++ b/calib_logs.csv @@ -0,0 +1,17 @@ +timestamp,calibration_name,sample_count,mean_reading,std_deviation,overall_drift,drift_rate_per_sec,calibration_factor,min_reading,max_reading,measurement_duration_sec +2025-10-13 21:08:58,usb no power LD33V,600,-103152.2933,106.0728,-69.1036,-2.303454,-0.001,-103430,-102867,30.0 +2025-10-13 21:10:40,usb no power LD33V,601,-102888.8087,99.7412,145.1095,4.836984,-0.001,-103138,-102576,30.0 +2025-10-13 21:12:04,usb no power LD33V,600,-103299.5267,87.2157,45.2407,1.508024,-0.001,-103589,-103069,30.0 +2025-10-13 21:12:58,usb no power LD33V,601,-103229.7488,91.4023,-77.1958,-2.573193,-0.001,-103543,-102965,30.0 +2025-10-13 21:14:03,usb no power LD33V,600,-103448.9233,89.5812,-96.3423,-3.211409,-0.001,-103727,-103174,30.0 +2025-10-13 21:17:56,usb power LD33V,601,-103265.4077,90.7402,26.6434,0.888112,-0.001,-103569,-102993,30.0 +2025-10-13 21:18:54,usb power LD33V,601,-103123.9451,90.2391,-36.9311,-1.231038,-0.001,-103389,-102835,30.0 +2025-10-13 21:19:53,usb power LD33V,601,-103115.7687,77.8386,14.0447,0.468157,-0.001,-103343,-102921,30.0 +2025-10-13 21:21:02,usb power LD33V,600,-103254.3067,84.4449,-50.2513,-1.675044,-0.001,-103540,-103024,30.0 +2025-10-13 21:21:59,usb power LD33V,600,-103259.5367,82.3364,-22.2523,-0.741743,-0.001,-103486,-103010,30.0 +2025-10-13 21:29:20,usb no power,601,-104761.3428,51.0645,-5.4956,-0.183187,-0.001,-104945,-104573,30.0 +2025-10-13 21:30:38,usb no power,602,-104635.0515,54.4358,-57.8148,-1.927161,-0.001,-104790,-104465,30.0 +2025-10-13 21:35:08,usb no power,602,-104653.0415,52.5725,-13.5462,-0.451541,-0.001,-104790,-104508,30.0 +2025-10-13 21:36:55,usb no power LD33V,601,-103304.6007,78.7342,-17.8396,-0.594652,-0.001,-103514,-103091,30.0 +2025-10-13 21:44:41,usb no power 100uH 1mF,602,-104692.5233,48.2984,33.4322,1.114407,-0.001,-104825,-104543,30.0 +2025-10-13 21:46:44,usb no power 100uH 1mF,601,-104841.1248,46.0507,10.3711,0.345703,-0.001,-104982,-104714,30.0 diff --git a/evaluation/calibration/frame.py b/evaluation/calibration/frame.py index 616804f..bfb0c88 100644 --- a/evaluation/calibration/frame.py +++ b/evaluation/calibration/frame.py @@ -1,6 +1,9 @@ import time import statistics import threading +import pandas as pd +import os +from datetime import datetime from tkinter import messagebox, ttk class CalibrationFrame(ttk.Frame): @@ -19,6 +22,23 @@ class CalibrationFrame(ttk.Frame): self.std_label = ttk.Label(self, text="Standard Deviation: Not calculated") self.std_label.pack(pady=5) + self.drift_label = ttk.Label(self, text="Drift: Not calculated") + self.drift_label.pack(pady=5) + + # Calibration name input + self.name_frame = ttk.Frame(self) + self.name_frame.pack(pady=10) + + self.name_label = ttk.Label(self.name_frame, text="Calibration Name:") + self.name_label.pack(side='left', padx=(0, 5)) + + self.name_entry = ttk.Entry(self.name_frame, width=25) + self.name_entry.pack(side='left', padx=(0, 5)) + self.name_entry.insert(0, self.generate_default_name()) # Default name + + self.auto_name_button = ttk.Button(self.name_frame, text="Auto", command=self.generate_auto_name, width=6) + self.auto_name_button.pack(side='left') + self.calibrate_button = ttk.Button(self, text="Start Calibration", command=self.start_calibration) self.calibrate_button.pack(pady=10) @@ -39,6 +59,11 @@ class CalibrationFrame(ttk.Frame): messagebox.showerror("Error", "Please connect to device first!") return + # Get calibration name + calibration_name = self.name_entry.get().strip() + if not calibration_name: + calibration_name = "Unnamed Calibration" + # Start calibration routine self.calibration_in_progress = True self.calibrate_button.config(state='disabled') @@ -46,7 +71,7 @@ class CalibrationFrame(ttk.Frame): # Show initial prompt result = messagebox.askokcancel( - "Calibration Procedure", + f"Calibration Procedure - {calibration_name}", "Step 1: Remove all weights from the scale and press OK to continue." ) @@ -62,19 +87,19 @@ class CalibrationFrame(ttk.Frame): # Show second prompt result = messagebox.askokcancel( - "Calibration Procedure", + f"Calibration Procedure - {calibration_name}", "Step 2: Place the 100g calibration weight on the scale and press OK to start measurement." ) if not result: self.calibration_in_progress = False self.calibrate_button.config(state='normal') - self.status_label.config(text="Calibration cancelled") + self.status_label.config(text=f"Calibration '{calibration_name}' cancelled") return # Start measurement thread try: - self.status_label.config(text="Collecting calibration data (30 seconds)...") + self.status_label.config(text=f"Collecting data for '{calibration_name}' (30 seconds)...") # Collect data for 30 seconds start_time = time.time() @@ -98,44 +123,227 @@ class CalibrationFrame(ttk.Frame): messagebox.showerror("Error", "No readings collected. Check device connection.") self.calibration_in_progress = False self.calibrate_button.config(state='normal') - self.status_label.config(text="Calibration failed") + self.status_label.config(text=f"Calibration '{calibration_name}' failed") return # Calculate statistics mean_reading = statistics.mean(calibration_readings) std_reading = statistics.stdev(calibration_readings) if len(calibration_readings) > 1 else 0 + # Calculate overall drift during the 30-second readout + drift_calculation = self.calculate_drift(calibration_readings) + overall_drift = drift_calculation['overall_drift'] + drift_rate = drift_calculation['drift_rate'] + # Calculate calibration factor (100g / mean_reading) calibration_factor = 100.0 / mean_reading # Set calibration factor in serial reader - try multiple ways self.serial_reader.calib_factor = calibration_factor + # Save calibration data to CSV log + self.save_calibration_log(calibration_name, calibration_readings, mean_reading, + std_reading, overall_drift, drift_rate, calibration_factor) + + # Save individual readings to separate CSV file + readings_file = self.save_individual_readings(calibration_name, calibration_readings) + # Update UI - self.status_label.config(text="Calibration complete!") + self.status_label.config(text=f"Calibration '{calibration_name}' complete!") self.calibration_factor_label.config(text=f"Calibration Factor: {calibration_factor:.2f}") self.std_label.config(text=f"Standard Deviation: {std_reading:.2f} (raw units)") + self.drift_label.config(text=f"Drift: {overall_drift:.2f} units ({drift_rate:.2f} units/sec)") # Show results + readings_file_msg = f"Individual readings saved to: {os.path.basename(readings_file)}" if readings_file else "Individual readings save failed" + messagebox.showinfo( - "Calibration Complete", - f"Calibration successful!\n\n" + f"Calibration Complete - {calibration_name}", + f"Calibration '{calibration_name}' successful!\n\n" f"Samples collected: {len(calibration_readings)}\n" f"Mean reading: {mean_reading:.2f} (raw units)\n" f"Standard deviation: {std_reading:.2f} (raw units)\n" + f"Overall drift: {overall_drift:.2f} units ({drift_rate:.2f} units/sec)\n" f"Calibration factor: {calibration_factor:.2f}\n\n" - f"The scale is now calibrated for 100g." + f"The scale is now calibrated for 100g.\n\n" + f"Data saved to:\n" + f"• Summary: calib_logs.csv\n" + f"• {readings_file_msg}" ) except Exception as e: - messagebox.showerror("Error", f"Calibration failed: {str(e)}") - self.status_label.config(text="Calibration failed") + messagebox.showerror("Error", f"Calibration '{calibration_name}' failed: {str(e)}") + self.status_label.config(text=f"Calibration '{calibration_name}' failed") finally: self.calibration_in_progress = False self.calibrate_button.config(state='normal') self.progress_bar['value'] = 0 + def calculate_drift(self, readings): + """ + Calculate the overall drift during the measurement period. + + Args: + readings: List of measurement readings collected over time + + Returns: + dict: Contains overall_drift (difference between end and start) + and drift_rate (drift per second) + """ + if len(readings) < 2: + return {'overall_drift': 0.0, 'drift_rate': 0.0} + + # Calculate drift using linear regression to find the trend + n = len(readings) + x_values = list(range(n)) # Time indices + y_values = readings + + # Simple linear regression: y = mx + b + # Calculate slope (m) which represents the drift rate per sample + x_mean = statistics.mean(x_values) + y_mean = statistics.mean(y_values) + + numerator = sum((x_values[i] - x_mean) * (y_values[i] - y_mean) for i in range(n)) + denominator = sum((x_values[i] - x_mean) ** 2 for i in range(n)) + + if denominator == 0: + slope = 0 + else: + slope = numerator / denominator + + # Overall drift is the difference between the projected end value and start value + overall_drift = slope * (n - 1) + + # Convert to drift rate per second (assuming 30 seconds measurement duration) + measurement_duration = 30.0 # seconds + drift_rate_per_second = overall_drift / measurement_duration + + # Alternative simple calculation: difference between first and last values + simple_drift = readings[-1] - readings[0] + + return { + 'overall_drift': overall_drift, + 'drift_rate': drift_rate_per_second, + 'simple_drift': simple_drift, + 'slope': slope + } + + def save_calibration_log(self, calibration_name, calibration_readings, mean_reading, + std_reading, overall_drift, drift_rate, calibration_factor): + """ + Save calibration data to calib_logs.csv using pandas + + Args: + calibration_name: Name of the calibration run + calibration_readings: List of all raw readings + mean_reading: Mean of the readings + std_reading: Standard deviation of the readings + overall_drift: Overall drift during measurement + drift_rate: Drift rate per second + calibration_factor: Calculated calibration factor + """ + try: + # Create the calibration data record + timestamp = datetime.now() + + # Create a record for the summary data + calibration_record = { + 'timestamp': timestamp.strftime("%Y-%m-%d %H:%M:%S"), + 'calibration_name': calibration_name, + 'sample_count': len(calibration_readings), + 'mean_reading': round(mean_reading, 4), + 'std_deviation': round(std_reading, 4), + 'overall_drift': round(overall_drift, 4), + 'drift_rate_per_sec': round(drift_rate, 6), + 'calibration_factor': round(calibration_factor, 4), + 'min_reading': round(min(calibration_readings), 4), + 'max_reading': round(max(calibration_readings), 4), + 'measurement_duration_sec': 30.0 + } + + # Create DataFrame with the new record + new_record_df = pd.DataFrame([calibration_record]) + + csv_file = 'calib_logs.csv' + + # Check if file exists and append, otherwise create new + if os.path.exists(csv_file): + # Append to existing file + new_record_df.to_csv(csv_file, mode='a', header=False, index=False) + else: + # Create new file with header + new_record_df.to_csv(csv_file, mode='w', header=True, index=False) + + print(f"Calibration data saved to {csv_file}") + + except Exception as e: + print(f"Error saving calibration log: {str(e)}") + messagebox.showwarning("Warning", f"Could not save calibration log: {str(e)}") + + def save_individual_readings(self, calibration_name, calibration_readings): + """ + Save individual calibration readings to a separate CSV file in ./logs directory + + Args: + calibration_name: Name of the calibration run + calibration_readings: List of all raw readings + """ + try: + # Create logs directory if it doesn't exist + logs_dir = './logs' + if not os.path.exists(logs_dir): + os.makedirs(logs_dir) + + # Generate filename with date and calibration name + timestamp = datetime.now() + date_str = timestamp.strftime("%Y%m%d") + time_str = timestamp.strftime("%H%M%S") + + # Clean calibration name for filename (remove invalid characters) + clean_name = "".join(c for c in calibration_name if c.isalnum() or c in (' ', '-', '_')).rstrip() + clean_name = clean_name.replace(' ', '_') + + filename = f"{date_str}_{clean_name}_{time_str}_readings.csv" + filepath = os.path.join(logs_dir, filename) + + # Create DataFrame with individual readings + # Add sample number and timestamp for each reading (assuming ~1 reading per second) + readings_data = [] + for i, reading in enumerate(calibration_readings): + sample_time = i * (30.0 / len(calibration_readings)) # Distribute over 30 seconds + readings_data.append({ + 'sample_number': i + 1, + 'time_seconds': round(sample_time, 3), + 'raw_reading': reading, + 'calibration_name': calibration_name, + 'timestamp': timestamp.strftime("%Y-%m-%d %H:%M:%S") + }) + + readings_df = pd.DataFrame(readings_data) + + # Save to CSV + readings_df.to_csv(filepath, index=False) + + print(f"Individual readings saved to {filepath}") + return filepath + + except Exception as e: + print(f"Error saving individual readings: {str(e)}") + messagebox.showwarning("Warning", f"Could not save individual readings: {str(e)}") + return None + + def generate_default_name(self): + """Generate a default calibration name with timestamp""" + from datetime import datetime + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + return f"Calib_{timestamp}" + + def generate_auto_name(self): + """Generate and set a new automatic calibration name""" + self.name_entry.delete(0, 'end') + self.name_entry.insert(0, self.generate_default_name()) + def reset_calibration(self): """Reset calibration factor and UI display""" # Reset calibration factor in serial reader @@ -152,5 +360,9 @@ class CalibrationFrame(ttk.Frame): self.status_label.config(text="Ready for calibration") self.calibration_factor_label.config(text="Calibration Factor: Not set") self.std_label.config(text="Standard Deviation: Not calculated") - self.current_reading_label.config(text="Current reading: --") + self.drift_label.config(text="Drift: Not calculated") self.progress_bar['value'] = 0 + + # Reset name to a new auto-generated name + self.name_entry.delete(0, 'end') + self.name_entry.insert(0, self.generate_default_name()) diff --git a/uv.lock b/uv.lock index 45b7e72..31f6e61 100644 --- a/uv.lock +++ b/uv.lock @@ -530,8 +530,8 @@ wheels = [ [[package]] name = "python-toolkit" -version = "0.1.2" -source = { git = "https://git.magnuss.link/JannTer/python-toolkit#5bf4f79139c814b04e4d29175e4641111088acf0" } +version = "0.1.3" +source = { git = "https://git.magnuss.link/JannTer/python-toolkit#b8b699020582775f7684110ffa87d12f0c110e4a" } dependencies = [ { name = "pyserial" }, ]