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 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())

4
uv.lock generated
View File

@@ -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" },
]