STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
smart_calibration_gui.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Smart Calibration GUI with Progressive Autonomy
4Human-in-the-loop system that becomes autonomous over time
5"""
6
7import sys
8import os
9import time
10import threading
11import queue
12import json
13import numpy as np
14import tkinter as tk
15from tkinter import ttk, messagebox, filedialog
16import matplotlib.pyplot as plt
17from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
18from matplotlib.figure import Figure
19
20# import serial
21import struct
22from collections import deque
23from datetime import datetime
24import logging
25from dataclasses import dataclass
26from typing import Dict, List, Optional, Tuple
27from enum import Enum
28import subprocess
29
30# Import the robust calibration framework
31from robust_calibration import (
32 RobustCalibrationFramework,
33 CalibrationPoint,
34 EnvironmentalState,
35)
36
37# Configure logging
38logging.basicConfig(
39 level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
40)
41logger = logging.getLogger(__name__)
42
43
44class ConfidenceLevel(Enum):
45 LOW = 0
46 MEDIUM = 1
47 HIGH = 2
48 MAXIMUM = 3
49
50
51@dataclass
53 sensor_id: int
54 current_voltage: float
55 predicted_pressure: float
56 confidence_interval_lower: float
57 confidence_interval_upper: float
58 reason: str
59 timestamp: float
60 request_id: int
61
62
63@dataclass
65 confidence_level: ConfidenceLevel
66 human_input_count: int
67 autonomous_success_count: int
68 autonomous_failure_count: int
69 reliability_score: float
70 last_human_input: float
71 calibration_data_points: int
72
73
75 """Smart calibration GUI with progressive autonomy"""
76
77 def __init__(self):
78 self.root = tk.Tk()
79 self.root.title("Smart PT Calibration System - Diablo FSW")
80 self.root.geometry("1800x1200")
81 self.root.configure(bg="#2b2b2b")
82
83 # Data structures
84 self.sensor_states: Dict[int, SensorLearningState] = {}
85 self.pending_requests: List[HumanInputRequest] = []
86 self.latest_measurements: Dict[int, Dict] = {}
87 # Robust calibration frameworks for each sensor
88 self.calibration_frameworks: Dict[int, RobustCalibrationFramework] = {
89 i: RobustCalibrationFramework(i) for i in range(9)
90 }
91
92 # Legacy calibration data (for backward compatibility)
93 self.calibration_data: Dict[int, List] = {i: [] for i in range(9)}
94
95 # Packet parsing structures - EXACT copy from channel_plotter.py
96 self.HEADER_STRUCT = struct.Struct("<4sBBHHII")
97 self.RECORD_STRUCT = struct.Struct("<BBiiII")
98
99 # Constants from channel_plotter.py
100 self.MAGIC = b"AD26"
103 self.HEADER_SIZE = self.HEADER_STRUCT.size
104 self.RECORD_SIZE = self.RECORD_STRUCT.size
106 self.FLAG_TIMING = 0x01
108 self.HEADER_SIZE
109 + self.PACKET_RECORDS * self.RECORD_SIZE
110 + self.CRC_SIZE_OPTIONAL
111 )
112 self.INT32_MIN = -2147483648
113 self.UINT32_MAX = 0xFFFFFFFF
114 self.V_REF = 2.5
115 self.ADC_SCALE = 2147483648.0
116
117 # Real-time data
118 self.data_queue = queue.Queue(maxsize=1000)
119 self.recording_data = False
120 self.autonomous_mode = False
121 self.recent_data: Dict[int, List] = {i: [] for i in range(9)}
122 self.last_update_time: Dict[int, float] = {i: 0.0 for i in range(9)}
123
124 # Serial connection
125 self.serial_connection: Optional[serial.Serial] = None
126 self.serial_thread: Optional[threading.Thread] = None
127 self.stop_serial = threading.Event()
128 self.synced = False
129
130 # Performance tracking
132 "data_rate_hz": 0.0,
133 "latency_ms": 0.0,
134 "autonomous_accuracy": 0.0,
135 "human_input_requests": 0,
136 "last_update": time.time(),
137 }
138
139 # Initialize GUI
140 self.setup_gui()
141 self.setup_serial()
143
144 def setup_gui(self):
145 """Setup the GUI layout"""
146 # Main container
147 main_frame = ttk.Frame(self.root)
148 main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
149
150 # Control panel
151 self.setup_control_panel(main_frame)
152
153 # Data display area
154 self.setup_data_display(main_frame)
155
156 # Human input panel
157 self.setup_human_input_panel(main_frame)
158
159 # Real-time plots
160 self.setup_realtime_plots(main_frame)
161
162 # Status and statistics
163 self.setup_status_panel(main_frame)
164
165 def setup_control_panel(self, parent):
166 """Setup control panel"""
167 control_frame = ttk.LabelFrame(
168 parent, text="Smart Calibration Controls", padding=10
169 )
170 control_frame.grid(row=0, column=0, columnspan=3, sticky="ew", pady=(0, 10))
171
172 # Connection controls
173 conn_frame = ttk.Frame(control_frame)
174 conn_frame.pack(fill=tk.X, pady=(0, 10))
175
176 ttk.Label(conn_frame, text="Serial Port:").pack(side=tk.LEFT)
177 self.port_var = tk.StringVar(value="/dev/ttyACM0")
178 port_combo = ttk.Combobox(conn_frame, textvariable=self.port_var, width=15)
179 port_combo["values"] = self.get_available_ports()
180 port_combo.pack(side=tk.LEFT, padx=(5, 10))
181
182 self.connect_btn = ttk.Button(
183 conn_frame, text="Connect", command=self.toggle_connection
184 )
185 self.connect_btn.pack(side=tk.LEFT, padx=(0, 10))
186
187 # Smart calibration controls
188 smart_frame = ttk.Frame(control_frame)
189 smart_frame.pack(fill=tk.X, pady=(0, 10))
190
191 self.autonomous_btn = ttk.Button(
192 smart_frame,
193 text="Enable Autonomous Mode",
194 command=self.toggle_autonomous_mode,
195 state="disabled",
196 )
197 self.autonomous_btn.pack(side=tk.LEFT, padx=(0, 10))
198
199 self.learn_btn = ttk.Button(
200 smart_frame,
201 text="Start Learning",
202 command=self.start_learning,
203 state="disabled",
204 )
205 self.learn_btn.pack(side=tk.LEFT, padx=(0, 10))
206
207 self.validate_btn = ttk.Button(
208 smart_frame,
209 text="Validate Prediction",
210 command=self.validate_prediction,
211 state="disabled",
212 )
213 self.validate_btn.pack(side=tk.LEFT, padx=(0, 10))
214
215 self.save_btn = ttk.Button(
216 smart_frame,
217 text="Save Learning State",
218 command=self.save_learning_state,
219 state="disabled",
220 )
221 self.save_btn.pack(side=tk.LEFT)
222
223 def setup_data_display(self, parent):
224 """Setup real-time data display"""
225 data_frame = ttk.LabelFrame(parent, text="Real-Time Sensor Data", padding=10)
226 data_frame.grid(row=1, column=0, sticky="nsew", padx=(0, 5), pady=(0, 5))
227
228 # Create data display tree
229 columns = (
230 "Sensor",
231 "Location",
232 "Voltage (V)",
233 "Predicted (kPa)",
234 "Confidence",
235 "Status",
236 )
237 self.data_tree = ttk.Treeview(
238 data_frame, columns=columns, show="headings", height=12
239 )
240
241 for col in columns:
242 self.data_tree.heading(col, text=col)
243 self.data_tree.column(col, width=120, anchor="center")
244
245 # Add scrollbar
246 scrollbar = ttk.Scrollbar(
247 data_frame, orient=tk.VERTICAL, command=self.data_tree.yview
248 )
249 self.data_tree.configure(yscrollcommand=scrollbar.set)
250
251 self.data_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
252 scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
253
254 # Initialize sensor rows
255 pt_locations = [
256 "Pressurant Tank",
257 "Kero Inlet",
258 "Kero Outlet",
259 "Lox Inlet",
260 "Lox Outlet",
261 "Injector",
262 "Chamber Wall #1",
263 "Chamber Wall #2",
264 "Nozzle Exit",
265 ]
266
267 for i, location in enumerate(pt_locations):
268 self.data_tree.insert(
269 "",
270 "end",
271 values=(f"PT {i}", location, "0.000", "0.000", "LOW", "Offline"),
272 )
273
274 def setup_human_input_panel(self, parent):
275 """Setup human input panel"""
276 input_frame = ttk.LabelFrame(parent, text="Human Input Interface", padding=10)
277 input_frame.grid(row=1, column=1, sticky="nsew", padx=(5, 5), pady=(0, 5))
278
279 # Pending requests display
280 ttk.Label(input_frame, text="Pending Human Input Requests:").pack(anchor="w")
281
282 self.requests_listbox = tk.Listbox(
283 input_frame, height=8, bg="#3b3b3b", fg="white"
284 )
285 self.requests_listbox.pack(fill=tk.X, pady=(5, 10))
286
287 # Input controls
288 input_controls_frame = ttk.Frame(input_frame)
289 input_controls_frame.pack(fill=tk.X)
290
291 ttk.Label(input_controls_frame, text="Selected Sensor:").pack(anchor="w")
292 self.selected_sensor_var = tk.StringVar()
293 sensor_combo = ttk.Combobox(
294 input_controls_frame, textvariable=self.selected_sensor_var, width=15
295 )
296 sensor_combo["values"] = [f"PT {i}" for i in range(9)]
297 sensor_combo.pack(fill=tk.X, pady=(0, 5))
298
299 ttk.Label(input_controls_frame, text="Reference Pressure (kPa):").pack(
300 anchor="w"
301 )
302 self.ref_pressure_var = tk.StringVar()
303 ref_entry = ttk.Entry(input_controls_frame, textvariable=self.ref_pressure_var)
304 ref_entry.pack(fill=tk.X, pady=(0, 5))
305
306 self.confirm_btn = ttk.Button(
307 input_controls_frame,
308 text="Confirm Input",
309 command=self.confirm_human_input,
310 state="disabled",
311 )
312 self.confirm_btn.pack(fill=tk.X, pady=(0, 5))
313
314 self.skip_btn = ttk.Button(
315 input_controls_frame,
316 text="Skip Request",
317 command=self.skip_human_input,
318 state="disabled",
319 )
320 self.skip_btn.pack(fill=tk.X)
321
322 # Learning progress
323 progress_frame = ttk.Frame(input_frame)
324 progress_frame.pack(fill=tk.X, pady=(10, 0))
325
326 ttk.Label(progress_frame, text="Learning Progress:").pack(anchor="w")
327
328 # Progress bars for each sensor
331
332 for i in range(9):
333 progress_frame_sensor = ttk.Frame(progress_frame)
334 progress_frame_sensor.pack(fill=tk.X, pady=2)
335
336 ttk.Label(progress_frame_sensor, text=f"PT {i}:", width=8).pack(
337 side=tk.LEFT
338 )
339
340 self.progress_vars[i] = tk.DoubleVar()
341 progress_bar = ttk.Progressbar(
342 progress_frame_sensor,
343 variable=self.progress_vars[i],
344 maximum=100,
345 length=100,
346 )
347 progress_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 5))
348
349 self.progress_bars[i] = ttk.Label(progress_frame_sensor, text="0%", width=6)
350 self.progress_bars[i].pack(side=tk.RIGHT)
351
352 def setup_realtime_plots(self, parent):
353 """Setup real-time plotting area"""
354 plot_frame = ttk.LabelFrame(parent, text="Real-Time Monitoring", padding=10)
355 plot_frame.grid(row=1, column=2, sticky="nsew", padx=(5, 0), pady=(0, 5))
356
357 # Create matplotlib figure
358 self.fig = Figure(figsize=(10, 8), dpi=100)
359 self.fig.patch.set_facecolor("#2b2b2b")
360
361 # Create subplots
362 self.ax_voltage = self.fig.add_subplot(3, 1, 1)
363 self.ax_pressure = self.fig.add_subplot(3, 1, 2)
364 self.ax_confidence = self.fig.add_subplot(3, 1, 3)
365
366 # Configure plots
367 self.ax_voltage.set_title("Real-Time Voltage Readings", color="white")
368 self.ax_voltage.set_ylabel("Voltage (V)", color="white")
369 self.ax_voltage.tick_params(colors="white")
370 self.ax_voltage.set_facecolor("#3b3b3b")
371
372 self.ax_pressure.set_title("Real-Time Pressure Readings", color="white")
373 self.ax_pressure.set_ylabel("Pressure (kPa)", color="white")
374 self.ax_pressure.tick_params(colors="white")
375 self.ax_pressure.set_facecolor("#3b3b3b")
376
377 self.ax_confidence.set_title("Confidence Levels", color="white")
378 self.ax_confidence.set_xlabel("Time (s)", color="white")
379 self.ax_confidence.set_ylabel("Confidence", color="white")
380 self.ax_confidence.tick_params(colors="white")
381 self.ax_confidence.set_facecolor("#3b3b3b")
382
383 # Embed in tkinter
384 self.canvas = FigureCanvasTkAgg(self.fig, plot_frame)
385 self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
386
387 # Initialize plot data
388 self.plot_data = {
389 "time": deque(maxlen=1000),
390 "voltages": {i: deque(maxlen=1000) for i in range(9)},
391 "pressures": {i: deque(maxlen=1000) for i in range(9)},
392 "confidence": {i: deque(maxlen=1000) for i in range(9)},
393 }
394
395 def setup_status_panel(self, parent):
396 """Setup status and statistics panel"""
397 status_frame = ttk.LabelFrame(
398 parent, text="System Status & Statistics", padding=10
399 )
400 status_frame.grid(row=2, column=0, columnspan=3, sticky="ew", pady=(5, 0))
401
402 # Status indicators
403 status_indicators_frame = ttk.Frame(status_frame)
404 status_indicators_frame.pack(fill=tk.X, pady=(0, 10))
405
406 self.status_var = tk.StringVar(value="Ready")
407 ttk.Label(status_indicators_frame, textvariable=self.status_var).pack(
408 side=tk.LEFT
409 )
410
411 # Performance indicators
412 perf_frame = ttk.Frame(status_indicators_frame)
413 perf_frame.pack(side=tk.RIGHT)
414
415 self.data_rate_var = tk.StringVar(value="Data Rate: 0 Hz")
416 self.latency_var = tk.StringVar(value="Latency: 0 ms")
417 self.accuracy_var = tk.StringVar(value="Accuracy: 0%")
418 self.autonomy_var = tk.StringVar(value="Autonomy: 0%")
419
420 ttk.Label(perf_frame, textvariable=self.data_rate_var).pack(
421 side=tk.LEFT, padx=(10, 0)
422 )
423 ttk.Label(perf_frame, textvariable=self.latency_var).pack(
424 side=tk.LEFT, padx=(10, 0)
425 )
426 ttk.Label(perf_frame, textvariable=self.accuracy_var).pack(
427 side=tk.LEFT, padx=(10, 0)
428 )
429 ttk.Label(perf_frame, textvariable=self.autonomy_var).pack(
430 side=tk.LEFT, padx=(10, 0)
431 )
432
433 # Statistics table
434 stats_frame = ttk.Frame(status_frame)
435 stats_frame.pack(fill=tk.X)
436
437 ttk.Label(stats_frame, text="Sensor Statistics:").pack(anchor="w")
438
439 # Create statistics tree
440 stats_columns = (
441 "Sensor",
442 "Confidence",
443 "Human Inputs",
444 "Success Rate",
445 "Reliability",
446 )
447 self.stats_tree = ttk.Treeview(
448 stats_frame, columns=stats_columns, show="headings", height=4
449 )
450
451 for col in stats_columns:
452 self.stats_tree.heading(col, text=col)
453 self.stats_tree.column(col, width=120, anchor="center")
454
455 self.stats_tree.pack(fill=tk.X)
456
457 # Initialize statistics rows
458 for i in range(9):
459 self.stats_tree.insert(
460 "", "end", values=(f"PT {i}", "LOW", "0", "0%", "0%")
461 )
462
463 # Configure grid weights
464 parent.grid_rowconfigure(1, weight=1)
465 parent.grid_columnconfigure(0, weight=1)
466 parent.grid_columnconfigure(1, weight=1)
467 parent.grid_columnconfigure(2, weight=1)
468
470 """Get list of available serial ports"""
471 ports = []
472 try:
473 import serial.tools.list_ports
474
475 for port in serial.tools.list_ports.comports():
476 ports.append(port.device)
477 except:
478 # Fallback
479 ports = ["/tmp/fake_esp32_pipe"]
480 for i in range(10):
481 ports.extend([f"/dev/ttyUSB{i}", f"/dev/ttyACM{i}", f"COM{i}"])
482 return ports
483
484 def setup_serial(self):
485 """Setup serial connection with robust error handling"""
486 try:
487 # Close existing connection if any
488 if (
489 hasattr(self, "serial_connection")
490 and self.serial_connection
491 and self.serial_connection.is_open
492 ):
493 self.serial_connection.close()
494 time.sleep(0.1) # Give it time to close
495
496 # Clear any existing data
497 self.recent_data.clear()
498 self.last_update_time.clear()
499
500 self.serial_connection = serial.Serial(
501 port=self.port_var.get(),
502 baudrate=115200,
503 timeout=0.05, # Longer timeout for stability
504 write_timeout=0.1,
505 inter_byte_timeout=0.1,
506 rtscts=False, # Disable hardware flow control
507 dsrdtr=False, # Disable hardware flow control
508 xonxoff=False, # Disable software flow control
509 )
510
511 # Reset buffers and flush
512 self.serial_connection.reset_input_buffer()
513 self.serial_connection.reset_output_buffer()
514 self.serial_connection.flushInput()
515 self.serial_connection.flushOutput()
516
517 # Reset packet sync state
518 self.synced = False
519
520 logger.info(f"Serial connection established on {self.port_var.get()}")
521 return True
522
523 except serial.SerialException as e:
524 logger.error(f"Serial port error: {e}")
525 messagebox.showerror("Serial Error", f"Serial port error: {e}")
526 return False
527 except Exception as e:
528 logger.error(f"Failed to connect to serial port: {e}")
529 messagebox.showerror(
530 "Connection Error", f"Failed to connect to {self.port_var.get()}: {e}"
531 )
532 return False
533
535 """Start the data processing thread"""
536 self.data_thread = threading.Thread(target=self.data_processor, daemon=True)
537 self.data_thread.start()
538
539 # Start GUI update timer
540 self.update_gui()
541
542 def _resync(self, buffer):
543 """EXACT copy from working channel_plotter.py"""
544 keep = len(self.MAGIC) - 1
545 while True:
546 idx = buffer.find(self.MAGIC)
547 if idx == -1:
548 if keep > 0 and len(buffer) > keep:
549 del buffer[:-keep]
550 return False
551 if idx:
552 del buffer[:idx]
553 if len(buffer) < self.HEADER_SIZE:
554 return False
555 try:
556 (
557 magic,
558 version,
559 flags,
560 count,
561 _failures,
562 _total_time_us,
563 _packet_time_us,
564 ) = self.HEADER_STRUCT.unpack_from(buffer, 0)
565 except struct.error:
566 return False
567 if (
568 magic != self.MAGIC
569 or version != self.PACKET_VERSION
570 or not (flags & self.FLAG_TIMING)
571 ):
572 del buffer[0]
573 continue
574 count = int(count)
575 if count > self.PACKET_RECORDS:
576 del buffer[0]
577 continue
578 payload_len = self.HEADER_SIZE + count * self.RECORD_SIZE
579 if payload_len <= self.HEADER_SIZE or payload_len > self.MAX_PACKET_BYTES:
580 del buffer[0]
581 continue
582 if len(buffer) < payload_len:
583 return False
584 if payload_len < len(buffer) < payload_len + self.CRC_SIZE_OPTIONAL:
585 return False
586 crc_present = False
587 if len(buffer) >= payload_len + self.CRC_SIZE_OPTIONAL:
588 next_bytes = buffer[payload_len : payload_len + len(self.MAGIC)]
589 if next_bytes != self.MAGIC:
590 crc_present = True
591 packet_len = payload_len + (self.CRC_SIZE_OPTIONAL if crc_present else 0)
592 if len(buffer) < packet_len:
593 return False
594 return True
595
596 def _drain_synced(self, buffer):
597 """EXACT copy from working channel_plotter.py"""
598 measurements = []
599 while True:
600 if len(buffer) < self.HEADER_SIZE:
601 break
602 if buffer[: len(self.MAGIC)] != self.MAGIC:
603 self.synced = False
604 del buffer[0]
605 break
606 try:
607 (
608 magic,
609 version,
610 flags,
611 count,
612 failures,
613 total_time_us,
614 packet_time_us,
615 ) = self.HEADER_STRUCT.unpack_from(buffer, 0)
616 except struct.error:
617 self.synced = False
618 del buffer[0]
619 break
620 count = int(count)
621 payload_len = self.HEADER_SIZE + count * self.RECORD_SIZE
622 if payload_len > self.MAX_PACKET_BYTES:
623 self.synced = False
624 del buffer[0]
625 break
626 if len(buffer) < payload_len:
627 break
628 if payload_len < len(buffer) < payload_len + self.CRC_SIZE_OPTIONAL:
629 break
630 crc_present = False
631 if len(buffer) >= payload_len + self.CRC_SIZE_OPTIONAL:
632 next_bytes = buffer[payload_len : payload_len + len(self.MAGIC)]
633 if next_bytes != self.MAGIC:
634 crc_present = True
635 packet_len = payload_len + (self.CRC_SIZE_OPTIONAL if crc_present else 0)
636 if len(buffer) < packet_len:
637 break
638 if (
639 magic != self.MAGIC
640 or version != self.PACKET_VERSION
641 or not (flags & self.FLAG_TIMING)
642 ):
643 self.synced = False
644 del buffer[0]
645 break
646
647 total_time_us = int(total_time_us)
648 failures = int(failures)
649 packet_time_us = int(packet_time_us) & self.UINT32_MAX
650 count_success = max(1, count - min(count, failures))
651 per_sample_default = (
652 max(1, total_time_us // count_success) if total_time_us > 0 else 1
653 )
654
655 for i in range(count):
656 base = self.HEADER_SIZE + i * self.RECORD_SIZE
657 ch, ok, raw, sample_time, read_dur, conv_dur = (
658 self.RECORD_STRUCT.unpack_from(buffer, base)
659 )
660 ch = int(ch)
661 ok = int(ok)
662 raw = int(raw)
663 sample_time = int(sample_time)
664
665 # Skip padding records (ch=0xFF) and invalid records
666 if ch == 0xFF:
667 continue
668 if (
669 not ok
670 or raw == self.INT32_MIN
671 or sample_time in (-1, self.INT32_MIN)
672 ):
673 continue
674 sample_us = sample_time & self.UINT32_MAX
675 read_us = 0 if read_dur == self.UINT32_MAX else int(read_dur)
676 conv_us = 0 if conv_dur == self.UINT32_MAX else int(conv_dur)
677 per_sample = read_us + conv_us
678 if per_sample <= 0:
679 per_sample = per_sample_default
680 per_sample = max(1, int(per_sample))
681 volts = raw * self.V_REF / self.ADC_SCALE
682
683 # Create measurement
684 measurement = {
685 "sensor_id": ch,
686 "voltage": volts,
687 "raw_adc": raw,
688 "sample_time": sample_time,
689 "read_time_dur": read_us,
690 "conv_time_dur": conv_us,
691 "timestamp": time.time(),
692 "filtered_voltage": self.apply_kalman_filter(volts, ch),
693 "predicted_pressure": 0.0,
694 "confidence_level": "LOW",
695 "needs_human_input": True,
696 }
697
698 # Check if sensor has calibration data OR if we can use covariance mapping from other PTs
699 framework = self.calibration_frameworks[ch]
700
701 if len(framework.calibration_points) > 2:
702 # Use robust calibration framework for prediction
703 env_state = EnvironmentalState() # Default environmental state
704 predicted_pressure, uncertainty = (
705 framework.predict_pressure_with_uncertainty(
706 measurement["filtered_voltage"], env_state
707 )
708 )
709 measurement["predicted_pressure"] = predicted_pressure
710 measurement["uncertainty"] = uncertainty
711 measurement["needs_human_input"] = (
712 self.should_request_human_input_robust(ch, measurement)
713 )
714 measurement["confidence_level"] = framework.get_confidence_level()
715 elif self.can_use_covariance_mapping(ch):
716 # Use covariance mapping from other calibrated PTs
717 measurement["predicted_pressure"] = (
719 ch, measurement["filtered_voltage"]
720 )
721 )
722 measurement["needs_human_input"] = False # Autonomous prediction
723 measurement["confidence_level"] = (
724 "MEDIUM" # Medium confidence from covariance
725 )
726 else:
727 measurement["needs_human_input"] = True
728 measurement["confidence_level"] = "LOW"
729
730 measurements.append(measurement)
731
732 del buffer[:packet_len]
733 return measurements
734
735 def data_processor(self):
736 """High-performance data processing thread with robust error handling"""
737 buffer = bytearray()
738 synced = False
739
740 while not self.stop_serial.is_set():
741 if self.serial_connection and self.serial_connection.is_open:
742 try:
743 # Read available data
744 data = self.serial_connection.read(
745 self.serial_connection.in_waiting or 1
746 )
747 if data:
748 buffer.extend(data)
749
750 if not synced:
751 synced = self._resync(buffer)
752
753 if synced:
754 measurements = self._drain_synced(buffer)
755 for measurement in measurements:
756 # Update latest measurements
757 self.latest_measurements[measurement["sensor_id"]] = (
758 measurement
759 )
760
761 # Add to data queue for GUI updates
762 if not self.data_queue.full():
763 self.data_queue.put(measurement)
764
765 # Update performance stats
767
768 except serial.SerialException as e:
769 logger.error(f"Serial error in data processor: {e}")
770 self.status_var.set("Serial Error - Reconnecting...")
771 time.sleep(1.0) # Wait before retrying
772 except Exception as e:
773 logger.error(f"Unexpected error in data processor: {e}")
774 time.sleep(0.1) # Wait before retrying
775 else:
776 time.sleep(0.1)
777
778 def apply_kalman_filter(self, voltage: float, sensor_id: int) -> float:
779 """Apply Kalman filter for high-performance voltage filtering"""
780 if not hasattr(self, "kalman_states"):
782
783 if sensor_id not in self.kalman_states:
784 self.kalman_states[sensor_id] = {
785 "x": voltage,
786 "P": 1.0,
787 "Q": 0.01,
788 "R": 0.1,
789 }
790
791 state = self.kalman_states[sensor_id]
792
793 # Predict step
794 state["P"] += state["Q"]
795
796 # Update step
797 K = state["P"] / (state["P"] + state["R"])
798 state["x"] += K * (voltage - state["x"])
799 state["P"] *= 1 - K
800
801 return state["x"]
802
803 def predict_pressure(self, sensor_id: int, voltage: float) -> float:
804 """Predict pressure from voltage using calibration data"""
805 if (
806 sensor_id not in self.calibration_data
807 or len(self.calibration_data[sensor_id]) < 3
808 ):
809 return 0.0
810
811 # Simple polynomial fit (degree 2)
812 data_points = self.calibration_data[sensor_id]
813 voltages = [point["voltage"] for point in data_points]
814 pressures = [point["pressure"] for point in data_points]
815
816 try:
817 poly_coeffs = np.polyfit(voltages, pressures, min(2, len(voltages) - 1))
818 return np.polyval(poly_coeffs, voltage)
819 except:
820 return 0.0
821
822 def can_use_covariance_mapping(self, sensor_id: int) -> bool:
823 """Check if we can use covariance mapping from other calibrated PTs"""
824 # Need at least one other PT with good calibration
825 calibrated_pts = []
826 for pt_id, framework in self.calibration_frameworks.items():
827 if pt_id != sensor_id and len(framework.calibration_points) >= 3:
828 calibrated_pts.append(pt_id)
829
830 return len(calibrated_pts) >= 1
831
832 def predict_pressure_covariance(self, sensor_id: int, voltage: float) -> float:
833 """Predict pressure using covariance mapping from other calibrated PTs"""
834 # Find the best reference PT (most calibrated)
835 best_ref_pt = None
836 max_cal_points = 0
837
838 for pt_id, framework in self.calibration_frameworks.items():
839 if (
840 pt_id != sensor_id
841 and len(framework.calibration_points) > max_cal_points
842 ):
843 best_ref_pt = pt_id
844 max_cal_points = len(framework.calibration_points)
845
846 if best_ref_pt is None:
847 return 0.0
848
849 # Use robust framework for prediction
850 framework = self.calibration_frameworks[best_ref_pt]
851 env_state = EnvironmentalState() # Default environmental state
852 predicted_pressure, uncertainty = framework.predict_pressure_with_uncertainty(
853 voltage, env_state
854 )
855
856 # Apply covariance scaling based on PT relationships
857 covariance_factor = self.get_pt_covariance_factor(sensor_id, best_ref_pt)
858
859 return predicted_pressure * covariance_factor
860
861 def get_pt_covariance_factor(self, target_pt: int, reference_pt: int) -> float:
862 """Get covariance scaling factor between PTs"""
863 # Simplified covariance model based on PT locations
864 # In practice, this would be learned from historical data
865
866 pt_relationships = {
867 # Pressurant Tank relationships
868 0: {1: 0.95, 2: 0.90, 3: 0.85, 4: 0.80, 5: 0.75, 6: 0.70, 7: 0.65, 8: 0.60},
869 # Kero Inlet relationships
870 1: {0: 0.95, 2: 0.98, 3: 0.85, 4: 0.80, 5: 0.75, 6: 0.70, 7: 0.65, 8: 0.60},
871 # Kero Outlet relationships
872 2: {0: 0.90, 1: 0.98, 3: 0.85, 4: 0.80, 5: 0.75, 6: 0.70, 7: 0.65, 8: 0.60},
873 # Lox Inlet relationships
874 3: {0: 0.85, 1: 0.85, 2: 0.85, 4: 0.95, 5: 0.80, 6: 0.75, 7: 0.70, 8: 0.65},
875 # Lox Outlet relationships
876 4: {0: 0.80, 1: 0.80, 2: 0.80, 3: 0.95, 5: 0.85, 6: 0.80, 7: 0.75, 8: 0.70},
877 # Injector relationships
878 5: {0: 0.75, 1: 0.75, 2: 0.75, 3: 0.80, 4: 0.85, 6: 0.90, 7: 0.85, 8: 0.80},
879 # Chamber Wall #1 relationships
880 6: {0: 0.70, 1: 0.70, 2: 0.70, 3: 0.75, 4: 0.80, 5: 0.90, 7: 0.95, 8: 0.85},
881 # Chamber Wall #2 relationships
882 7: {0: 0.65, 1: 0.65, 2: 0.65, 3: 0.70, 4: 0.75, 5: 0.85, 6: 0.95, 8: 0.90},
883 # Nozzle Exit relationships
884 8: {0: 0.60, 1: 0.60, 2: 0.60, 3: 0.65, 4: 0.70, 5: 0.80, 6: 0.85, 7: 0.90},
885 }
886
887 if (
888 target_pt in pt_relationships
889 and reference_pt in pt_relationships[target_pt]
890 ):
891 return pt_relationships[target_pt][reference_pt]
892
893 # Default covariance factor
894 return 0.8
895
897 self, sensor_id: int, measurement: Dict
898 ) -> bool:
899 """
900 Robust human input decision using uncertainty quantification
901 """
902 framework = self.calibration_frameworks[sensor_id]
903
904 if len(framework.calibration_points) < 3:
905 return True # Need more calibration data
906
907 # Get uncertainty from robust framework
908 uncertainty = measurement.get("uncertainty", 0.1)
909 predicted_pressure = measurement.get("predicted_pressure", 0.0)
910
911 # Request human input if uncertainty is too high
912 uncertainty_threshold = 0.05 # 5% uncertainty threshold
913
914 if uncertainty > uncertainty_threshold:
915 return True
916
917 # Request human input if prediction is outside reasonable range
918 if (
919 predicted_pressure < 0 or predicted_pressure > 1000
920 ): # Reasonable pressure range
921 return True
922
923 # Request human input based on confidence level
924 confidence_level = framework.get_confidence_level()
925 if confidence_level == "LOW":
926 return True
927 elif confidence_level == "MEDIUM" and uncertainty > 0.02:
928 return True
929
930 return False
931
932 def should_request_human_input(self, sensor_id: int, measurement: Dict) -> bool:
933 """Determine if human input is needed"""
934 if sensor_id not in self.sensor_states:
935 return True
936
937 state = self.sensor_states[sensor_id]
938
939 # Always request human input if confidence is low
940 if state.confidence_level == ConfidenceLevel.LOW:
941 return True
942
943 # Check if we haven't had human input recently
944 if state.confidence_level == ConfidenceLevel.MEDIUM:
945 time_since_input = time.time() - state.last_human_input
946 if time_since_input > 1800: # 30 minutes
947 return True
948
949 # Check for extrapolation
950 if self.is_extrapolation(sensor_id, measurement["voltage"]):
951 return True
952
953 return False
954
955 def is_extrapolation(self, sensor_id: int, voltage: float) -> bool:
956 """Check if voltage is outside calibration range"""
957 if (
958 sensor_id not in self.calibration_data
959 or len(self.calibration_data[sensor_id]) < 2
960 ):
961 return True
962
963 voltages = [point["voltage"] for point in self.calibration_data[sensor_id]]
964 min_voltage = min(voltages)
965 max_voltage = max(voltages)
966
967 voltage_range = max_voltage - min_voltage
968 margin = 0.1 * voltage_range # 10% margin
969
970 return voltage < (min_voltage - margin) or voltage > (max_voltage + margin)
971
972 def get_confidence_level(self, sensor_id: int) -> str:
973 """Get confidence level string"""
974 if sensor_id not in self.sensor_states:
975 return "LOW"
976
977 state = self.sensor_states[sensor_id]
978 return state.confidence_level.name
979
981 """Update performance statistics"""
982 current_time = time.time()
983 time_diff = current_time - self.performance_stats["last_update"]
984
985 if time_diff > 0.1: # Update every 100ms
986 self.performance_stats["data_rate_hz"] = (
987 1.0 / time_diff if time_diff > 0 else 0
988 )
989 self.performance_stats["latency_ms"] = time_diff * 1000
990 self.performance_stats["last_update"] = current_time
991
992 def update_gui(self):
993 """Update GUI elements"""
994 # Process data queue
995 while not self.data_queue.empty():
996 try:
997 measurement = self.data_queue.get_nowait()
998 self.process_measurement(measurement)
999 except queue.Empty:
1000 break
1001
1002 # Update data tree
1003 self.update_data_tree()
1004
1005 # Update plots
1006 self.update_plots()
1007
1008 # Update statistics
1009 self.update_statistics()
1010
1011 # Update performance indicators
1013
1014 # Schedule next update
1015 self.root.after(50, self.update_gui) # 20 FPS
1016
1017 def process_measurement(self, measurement: Dict):
1018 """Process a new measurement"""
1019 sensor_id = measurement["sensor_id"]
1020
1021 # Initialize sensor state if needed
1022 if sensor_id not in self.sensor_states:
1023 self.sensor_states[sensor_id] = SensorLearningState(
1024 confidence_level=ConfidenceLevel.LOW,
1025 human_input_count=0,
1026 autonomous_success_count=0,
1027 autonomous_failure_count=0,
1028 reliability_score=0.0,
1029 last_human_input=0.0,
1030 calibration_data_points=0,
1031 )
1032
1033 # Check if human input is needed
1034 if measurement["needs_human_input"] and not self.autonomous_mode:
1035 self.create_human_input_request(sensor_id, measurement)
1036
1037 def create_human_input_request(self, sensor_id: int, measurement: Dict):
1038 """Create human input request"""
1039 request = HumanInputRequest(
1040 sensor_id=sensor_id,
1041 current_voltage=measurement["voltage"],
1042 predicted_pressure=measurement["predicted_pressure"],
1043 confidence_interval_lower=measurement["predicted_pressure"] - 50.0,
1044 confidence_interval_upper=measurement["predicted_pressure"] + 50.0,
1045 reason=(
1046 "Calibration data insufficient"
1047 if len(self.calibration_data.get(sensor_id, [])) < 3
1048 else "High uncertainty"
1049 ),
1050 timestamp=time.time(),
1051 request_id=len(self.pending_requests) + 1,
1052 )
1053
1054 self.pending_requests.append(request)
1056
1057 # Enable input buttons when there are pending requests
1058 self.confirm_btn.config(state="normal")
1059 self.skip_btn.config(state="normal")
1060
1062 """Update data display tree"""
1063 for item in self.data_tree.get_children():
1064 values = list(self.data_tree.item(item)["values"])
1065 sensor_id = int(values[0].split()[1])
1066
1067 if sensor_id in self.latest_measurements:
1068 measurement = self.latest_measurements[sensor_id]
1069 values[2] = f"{measurement['voltage']:.3f}"
1070 values[3] = f"{measurement['predicted_pressure']:.1f}"
1071 values[4] = measurement["confidence_level"]
1072 values[5] = (
1073 "Needs Input" if measurement["needs_human_input"] else "Autonomous"
1074 )
1075 else:
1076 values[2] = "0.000"
1077 values[3] = "0.000"
1078 values[4] = "LOW"
1079 values[5] = "Offline"
1080
1081 self.data_tree.item(item, values=values)
1082
1084 """Update pending requests listbox"""
1085 self.requests_listbox.delete(0, tk.END)
1086 for request in self.pending_requests[-5:]: # Show last 5 requests
1088 tk.END,
1089 f"PT {request.sensor_id}: {request.current_voltage:.3f}V -> {request.predicted_pressure:.1f}kPa ({request.reason})",
1090 )
1091
1092 def update_plots(self):
1093 """Update real-time plots"""
1094 current_time = time.time()
1095
1096 # Clear and redraw voltage plot
1097 self.ax_voltage.clear()
1098 self.ax_voltage.set_title("Real-Time Voltage Readings", color="white")
1099 self.ax_voltage.set_ylabel("Voltage (V)", color="white")
1100 self.ax_voltage.tick_params(colors="white")
1101 self.ax_voltage.set_facecolor("#3b3b3b")
1102
1103 # Plot voltage data for active sensors
1104 for sensor_id, measurement in self.latest_measurements.items():
1105 if len(self.plot_data["voltages"][sensor_id]) > 1:
1106 times = list(self.plot_data["time"])
1107 voltages = list(self.plot_data["voltages"][sensor_id])
1108 self.ax_voltage.plot(
1109 times[-len(voltages) :],
1110 voltages,
1111 label=f"PT {sensor_id}",
1112 linewidth=1,
1113 )
1114
1115 self.ax_voltage.legend()
1116
1117 # Clear and redraw pressure plot
1118 self.ax_pressure.clear()
1119 self.ax_pressure.set_title("Real-Time Pressure Readings", color="white")
1120 self.ax_pressure.set_ylabel("Pressure (kPa)", color="white")
1121 self.ax_pressure.tick_params(colors="white")
1122 self.ax_pressure.set_facecolor("#3b3b3b")
1123
1124 # Plot pressure data for calibrated sensors
1125 for sensor_id, measurement in self.latest_measurements.items():
1126 if len(self.plot_data["pressures"][sensor_id]) > 1:
1127 times = list(self.plot_data["time"])
1128 pressures = list(self.plot_data["pressures"][sensor_id])
1129 self.ax_pressure.plot(
1130 times[-len(pressures) :],
1131 pressures,
1132 label=f"PT {sensor_id}",
1133 linewidth=1,
1134 )
1135
1136 self.ax_pressure.legend()
1137
1138 # Clear and redraw confidence plot
1139 self.ax_confidence.clear()
1140 self.ax_confidence.set_title("Confidence Levels", color="white")
1141 self.ax_confidence.set_xlabel("Time (s)", color="white")
1142 self.ax_confidence.set_ylabel("Confidence", color="white")
1143 self.ax_confidence.tick_params(colors="white")
1144 self.ax_confidence.set_facecolor("#3b3b3b")
1145
1146 # Plot confidence data
1147 for sensor_id in range(9):
1148 if len(self.plot_data["confidence"][sensor_id]) > 1:
1149 times = list(self.plot_data["time"])
1150 confidence = list(self.plot_data["confidence"][sensor_id])
1151 self.ax_confidence.plot(
1152 times[-len(confidence) :],
1153 confidence,
1154 label=f"PT {sensor_id}",
1155 linewidth=1,
1156 )
1157
1158 self.ax_confidence.legend()
1159
1160 self.canvas.draw_idle()
1161
1163 """Update statistics display"""
1164 for item in self.stats_tree.get_children():
1165 values = list(self.stats_tree.item(item)["values"])
1166 sensor_id = int(values[0].split()[1])
1167
1168 if sensor_id in self.sensor_states:
1169 state = self.sensor_states[sensor_id]
1170 success_rate = 0.0
1171 if state.autonomous_success_count + state.autonomous_failure_count > 0:
1172 success_rate = state.autonomous_success_count / (
1173 state.autonomous_success_count + state.autonomous_failure_count
1174 )
1175
1176 values[1] = state.confidence_level.name
1177 values[2] = str(state.human_input_count)
1178 values[3] = f"{success_rate*100:.1f}%"
1179 values[4] = f"{state.reliability_score*100:.1f}%"
1180 else:
1181 values[1] = "LOW"
1182 values[2] = "0"
1183 values[3] = "0%"
1184 values[4] = "0%"
1185
1186 self.stats_tree.item(item, values=values)
1187
1189 """Update performance indicators"""
1190 self.data_rate_var.set(
1191 f"Data Rate: {self.performance_stats['data_rate_hz']:.1f} Hz"
1192 )
1193 self.latency_var.set(f"Latency: {self.performance_stats['latency_ms']:.1f} ms")
1194
1195 # Calculate overall accuracy
1196 total_success = sum(
1197 state.autonomous_success_count for state in self.sensor_states.values()
1198 )
1199 total_attempts = sum(
1200 state.autonomous_success_count + state.autonomous_failure_count
1201 for state in self.sensor_states.values()
1202 )
1203 accuracy = (total_success / total_attempts * 100) if total_attempts > 0 else 0.0
1204 self.accuracy_var.set(f"Accuracy: {accuracy:.1f}%")
1205
1206 # Calculate autonomy percentage
1207 total_requests = len(self.pending_requests)
1208 autonomy = max(0, (100 - total_requests * 10)) if total_requests > 0 else 100.0
1209 self.autonomy_var.set(f"Autonomy: {autonomy:.1f}%")
1210
1212 """Toggle serial connection"""
1213 if self.serial_connection and self.serial_connection.is_open:
1214 self.disconnect()
1215 else:
1216 self.connect()
1217
1218 def connect(self):
1219 """Connect to serial port with retry mechanism"""
1220 max_retries = 3
1221 for attempt in range(max_retries):
1222 if self.setup_serial():
1223 self.connect_btn.config(text="Disconnect")
1224 self.learn_btn.config(state="normal")
1225 self.status_var.set("Connected")
1226 logger.info("Connected to serial port")
1227
1228 # Start data processing
1229 self.stop_serial.clear()
1231 return
1232 else:
1233 if attempt < max_retries - 1:
1234 logger.warning(
1235 f"Connection attempt {attempt + 1} failed, retrying..."
1236 )
1237 time.sleep(0.5)
1238
1239 self.status_var.set("Connection Failed - Check Port")
1240 logger.error("Failed to connect after all retries")
1241
1242 def disconnect(self):
1243 """Disconnect from serial port with proper cleanup"""
1244 # Signal data thread to stop
1245 self.stop_serial.set()
1246
1247 # Wait for data thread to finish
1248 if hasattr(self, "data_thread") and self.data_thread.is_alive():
1249 self.data_thread.join(timeout=1.0)
1250
1251 # Close serial connection
1252 if self.serial_connection and self.serial_connection.is_open:
1253 try:
1254 self.serial_connection.close()
1255 time.sleep(0.1) # Give it time to close
1256 except Exception as e:
1257 logger.warning(f"Error closing serial connection: {e}")
1258
1259 # Reset UI state
1260 self.connect_btn.config(text="Connect")
1261 self.learn_btn.config(state="disabled")
1262 self.autonomous_btn.config(state="disabled")
1263 self.validate_btn.config(state="disabled")
1264 self.status_var.set("Disconnected")
1265
1266 # Clear data
1267 self.latest_measurements.clear()
1268 self.recent_data.clear()
1269 self.last_update_time.clear()
1270
1271 logger.info("Disconnected from serial port")
1272
1274 """Start learning mode"""
1275 self.recording_data = True
1276 self.learn_btn.config(text="Stop Learning", command=self.stop_learning)
1277 self.autonomous_btn.config(state="normal")
1278 self.validate_btn.config(state="normal")
1279 self.status_var.set("Learning Mode Active")
1280
1281 def stop_learning(self):
1282 """Stop learning mode"""
1283 self.recording_data = False
1284 self.learn_btn.config(text="Start Learning", command=self.start_learning)
1285 self.status_var.set("Learning Stopped")
1286
1288 """Toggle autonomous mode"""
1289 self.autonomous_mode = not self.autonomous_mode
1290 if self.autonomous_mode:
1291 self.autonomous_btn.config(text="Disable Autonomous Mode")
1292 self.status_var.set("Autonomous Mode Active")
1293 else:
1294 self.autonomous_btn.config(text="Enable Autonomous Mode")
1295 self.status_var.set("Learning Mode Active")
1296
1298 """Confirm human input"""
1299 try:
1300 sensor_id = int(self.selected_sensor_var.get().split()[1])
1301 reference_pressure = float(self.ref_pressure_var.get())
1302
1303 # Add calibration data point
1304 if sensor_id in self.latest_measurements:
1305 measurement = self.latest_measurements[sensor_id]
1306
1307 # Add to robust calibration framework
1308 framework = self.calibration_frameworks[sensor_id]
1309 env_state = EnvironmentalState() # Default environmental state
1310 robust_calibration_point = CalibrationPoint(
1311 voltage=measurement["filtered_voltage"],
1312 pressure=reference_pressure,
1313 timestamp=time.time(),
1314 environmental_state=env_state,
1315 uncertainty=0.01, # 1% uncertainty
1316 )
1317
1318 # Add to framework and get results
1319 result = framework.add_calibration_point(robust_calibration_point)
1320
1321 # Also add to legacy system for backward compatibility
1322 calibration_point = {
1323 "voltage": measurement["filtered_voltage"],
1324 "pressure": reference_pressure,
1325 "timestamp": time.time(),
1326 }
1327 self.calibration_data[sensor_id].append(calibration_point)
1328
1329 # Update sensor state
1330 if sensor_id in self.sensor_states:
1331 state = self.sensor_states[sensor_id]
1332 state.human_input_count += 1
1333 state.last_human_input = time.time()
1334 state.calibration_data_points = len(
1335 self.calibration_data[sensor_id]
1336 )
1337
1338 # Update confidence level
1339 if state.human_input_count >= 20:
1340 state.confidence_level = ConfidenceLevel.MAXIMUM
1341 elif state.human_input_count >= 10:
1342 state.confidence_level = ConfidenceLevel.HIGH
1343 elif state.human_input_count >= 5:
1344 state.confidence_level = ConfidenceLevel.MEDIUM
1345
1346 # Remove pending requests for this sensor
1347 self.pending_requests = [
1348 req for req in self.pending_requests if req.sensor_id != sensor_id
1349 ]
1351
1352 # Disable buttons if no more pending requests
1353 if not self.pending_requests:
1354 self.confirm_btn.config(state="disabled")
1355 self.skip_btn.config(state="disabled")
1356
1357 # Clear input fields
1358 self.ref_pressure_var.set("")
1359
1360 messagebox.showinfo(
1361 "Input Confirmed", f"Calibration data added for PT {sensor_id}"
1362 )
1363
1364 except ValueError:
1365 messagebox.showerror(
1366 "Invalid Input", "Please enter valid sensor and pressure values"
1367 )
1368
1370 """Skip human input request"""
1371 if self.pending_requests:
1372 self.pending_requests.pop(0)
1374
1375 # Disable buttons if no more pending requests
1376 if not self.pending_requests:
1377 self.confirm_btn.config(state="disabled")
1378 self.skip_btn.config(state="disabled")
1379
1381 """Validate system prediction"""
1382 # This would typically involve comparing with a reference measurement
1383 messagebox.showinfo(
1384 "Validation", "Validation feature - compare with reference measurement"
1385 )
1386
1388 """Save learning state to file"""
1389 filename = filedialog.asksaveasfilename(
1390 defaultextension=".json",
1391 filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
1392 title="Save Learning State",
1393 )
1394
1395 if filename:
1396 learning_data = {
1397 "timestamp": datetime.now().isoformat(),
1398 "sensor_states": {
1399 str(sensor_id): {
1400 "confidence_level": state.confidence_level.name,
1401 "human_input_count": state.human_input_count,
1402 "autonomous_success_count": state.autonomous_success_count,
1403 "autonomous_failure_count": state.autonomous_failure_count,
1404 "reliability_score": state.reliability_score,
1405 "calibration_data_points": state.calibration_data_points,
1406 }
1407 for sensor_id, state in self.sensor_states.items()
1408 },
1409 "calibration_data": {
1410 str(sensor_id): data_points
1411 for sensor_id, data_points in self.calibration_data.items()
1412 },
1413 }
1414
1415 with open(filename, "w") as f:
1416 json.dump(learning_data, f, indent=2)
1417
1418 messagebox.showinfo("Saved", f"Learning state saved to {filename}")
1419
1420 def run(self):
1421 """Run the GUI"""
1422 try:
1423 self.root.mainloop()
1424 except KeyboardInterrupt:
1425 logger.info("GUI interrupted by user")
1426 finally:
1427 self.cleanup()
1428
1429 def cleanup(self):
1430 """Cleanup resources"""
1431 self.stop_serial.set()
1432 if self.serial_connection and self.serial_connection.is_open:
1433 self.serial_connection.close()
1434 logger.info("GUI cleanup complete")
1435
1436
1437def main():
1438 """Main entry point"""
1439 try:
1440 app = SmartCalibrationGUI()
1441 app.run()
1442 except Exception as e:
1443 logger.error(f"GUI failed to start: {e}")
1444 messagebox.showerror(
1445 "Startup Error", f"Failed to start smart calibration GUI: {e}"
1446 )
1447
1448
1449if __name__ == "__main__":
1450 main()
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
void mainloop(BoardState myBoardState, int boardID)
static void put(text_t *t, const char *fmt,...)
Definition boot_report.c:65
create_human_input_request(self, int sensor_id, Dict measurement)
bool is_extrapolation(self, int sensor_id, float voltage)
float apply_kalman_filter(self, float voltage, int sensor_id)
float predict_pressure_covariance(self, int sensor_id, float voltage)
bool should_request_human_input_robust(self, int sensor_id, Dict measurement)
float get_pt_covariance_factor(self, int target_pt, int reference_pt)
float predict_pressure(self, int sensor_id, float voltage)
bool should_request_human_input(self, int sensor_id, Dict measurement)
static void configure(gnss_core_t *c, uint32_t now_us)
Definition gnss_core.c:34
static void start(gnss_frame_parser_t *p, uint8_t byte)
Definition gnss_frame.c:45
static const idle_config_t config
Definition idle_task.c:17
std::pair< iterator, bool > insert(value_type &&value)
Definition json.hpp:19328
static std::vector< string_t > split(const string_t &reference_string)
split the string input to reference tokens
Definition json.hpp:14471
Definition run.py:1
TOML_NODISCARD_CTOR time() noexcept=default
static double read(int32_t code)