This commit is contained in:
2025-10-13 21:56:40 +02:00
parent ba2d67c3f9
commit d48728254c
3 changed files with 243 additions and 14 deletions

17
calib_logs.csv Normal file
View File

@@ -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
1 timestamp calibration_name sample_count mean_reading std_deviation overall_drift drift_rate_per_sec calibration_factor min_reading max_reading measurement_duration_sec
2 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
3 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
4 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
5 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
6 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
7 2025-10-13 21:17:56 usb power LD33V 601 -103265.4077 90.7402 26.6434 0.888112 -0.001 -103569 -102993 30.0
8 2025-10-13 21:18:54 usb power LD33V 601 -103123.9451 90.2391 -36.9311 -1.231038 -0.001 -103389 -102835 30.0
9 2025-10-13 21:19:53 usb power LD33V 601 -103115.7687 77.8386 14.0447 0.468157 -0.001 -103343 -102921 30.0
10 2025-10-13 21:21:02 usb power LD33V 600 -103254.3067 84.4449 -50.2513 -1.675044 -0.001 -103540 -103024 30.0
11 2025-10-13 21:21:59 usb power LD33V 600 -103259.5367 82.3364 -22.2523 -0.741743 -0.001 -103486 -103010 30.0
12 2025-10-13 21:29:20 usb no power 601 -104761.3428 51.0645 -5.4956 -0.183187 -0.001 -104945 -104573 30.0
13 2025-10-13 21:30:38 usb no power 602 -104635.0515 54.4358 -57.8148 -1.927161 -0.001 -104790 -104465 30.0
14 2025-10-13 21:35:08 usb no power 602 -104653.0415 52.5725 -13.5462 -0.451541 -0.001 -104790 -104508 30.0
15 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
16 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
17 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

View File

@@ -1,6 +1,9 @@
import time import time
import statistics import statistics
import threading import threading
import pandas as pd
import os
from datetime import datetime
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
class CalibrationFrame(ttk.Frame): 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 = ttk.Label(self, text="Standard Deviation: Not calculated")
self.std_label.pack(pady=5) 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 = ttk.Button(self, text="Start Calibration", command=self.start_calibration)
self.calibrate_button.pack(pady=10) self.calibrate_button.pack(pady=10)
@@ -39,6 +59,11 @@ class CalibrationFrame(ttk.Frame):
messagebox.showerror("Error", "Please connect to device first!") messagebox.showerror("Error", "Please connect to device first!")
return return
# Get calibration name
calibration_name = self.name_entry.get().strip()
if not calibration_name:
calibration_name = "Unnamed Calibration"
# Start calibration routine # Start calibration routine
self.calibration_in_progress = True self.calibration_in_progress = True
self.calibrate_button.config(state='disabled') self.calibrate_button.config(state='disabled')
@@ -46,7 +71,7 @@ class CalibrationFrame(ttk.Frame):
# Show initial prompt # Show initial prompt
result = messagebox.askokcancel( result = messagebox.askokcancel(
"Calibration Procedure", f"Calibration Procedure - {calibration_name}",
"Step 1: Remove all weights from the scale and press OK to continue." "Step 1: Remove all weights from the scale and press OK to continue."
) )
@@ -62,19 +87,19 @@ class CalibrationFrame(ttk.Frame):
# Show second prompt # Show second prompt
result = messagebox.askokcancel( 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." "Step 2: Place the 100g calibration weight on the scale and press OK to start measurement."
) )
if not result: if not result:
self.calibration_in_progress = False self.calibration_in_progress = False
self.calibrate_button.config(state='normal') self.calibrate_button.config(state='normal')
self.status_label.config(text="Calibration cancelled") self.status_label.config(text=f"Calibration '{calibration_name}' cancelled")
return return
# Start measurement thread # Start measurement thread
try: 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 # Collect data for 30 seconds
start_time = time.time() start_time = time.time()
@@ -98,44 +123,227 @@ class CalibrationFrame(ttk.Frame):
messagebox.showerror("Error", "No readings collected. Check device connection.") messagebox.showerror("Error", "No readings collected. Check device connection.")
self.calibration_in_progress = False self.calibration_in_progress = False
self.calibrate_button.config(state='normal') self.calibrate_button.config(state='normal')
self.status_label.config(text="Calibration failed") self.status_label.config(text=f"Calibration '{calibration_name}' failed")
return return
# Calculate statistics # Calculate statistics
mean_reading = statistics.mean(calibration_readings) mean_reading = statistics.mean(calibration_readings)
std_reading = statistics.stdev(calibration_readings) if len(calibration_readings) > 1 else 0 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) # Calculate calibration factor (100g / mean_reading)
calibration_factor = 100.0 / mean_reading calibration_factor = 100.0 / mean_reading
# Set calibration factor in serial reader - try multiple ways # Set calibration factor in serial reader - try multiple ways
self.serial_reader.calib_factor = calibration_factor 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 # 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.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.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 # 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( messagebox.showinfo(
"Calibration Complete", f"Calibration Complete - {calibration_name}",
f"Calibration successful!\n\n" f"Calibration '{calibration_name}' successful!\n\n"
f"Samples collected: {len(calibration_readings)}\n" f"Samples collected: {len(calibration_readings)}\n"
f"Mean reading: {mean_reading:.2f} (raw units)\n" f"Mean reading: {mean_reading:.2f} (raw units)\n"
f"Standard deviation: {std_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"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: except Exception as e:
messagebox.showerror("Error", f"Calibration failed: {str(e)}") messagebox.showerror("Error", f"Calibration '{calibration_name}' failed: {str(e)}")
self.status_label.config(text="Calibration failed") self.status_label.config(text=f"Calibration '{calibration_name}' failed")
finally: finally:
self.calibration_in_progress = False self.calibration_in_progress = False
self.calibrate_button.config(state='normal') self.calibrate_button.config(state='normal')
self.progress_bar['value'] = 0 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): def reset_calibration(self):
"""Reset calibration factor and UI display""" """Reset calibration factor and UI display"""
# Reset calibration factor in serial reader # Reset calibration factor in serial reader
@@ -152,5 +360,9 @@ class CalibrationFrame(ttk.Frame):
self.status_label.config(text="Ready for calibration") self.status_label.config(text="Ready for calibration")
self.calibration_factor_label.config(text="Calibration Factor: Not set") self.calibration_factor_label.config(text="Calibration Factor: Not set")
self.std_label.config(text="Standard Deviation: Not calculated") 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 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())

4
uv.lock generated
View File

@@ -530,8 +530,8 @@ wheels = [
[[package]] [[package]]
name = "python-toolkit" name = "python-toolkit"
version = "0.1.2" version = "0.1.3"
source = { git = "https://git.magnuss.link/JannTer/python-toolkit#5bf4f79139c814b04e4d29175e4641111088acf0" } source = { git = "https://git.magnuss.link/JannTer/python-toolkit#b8b699020582775f7684110ffa87d12f0c110e4a" }
dependencies = [ dependencies = [
{ name = "pyserial" }, { name = "pyserial" },
] ]