132 def create_control_panel(self) -> QtWidgets.QWidget:
133 """Create left control panel"""
134 panel = QtWidgets.QWidget()
135 layout = QtWidgets.QVBoxLayout(panel)
138 title = QtWidgets.QLabel("Calibration Orchestrator")
139 title.setStyleSheet("font-size: 18pt; font-weight: bold; padding: 10px;")
140 layout.addWidget(title)
143 self.phase_label = QtWidgets.QLabel("Phase: INIT")
144 self.phase_label.setStyleSheet("font-size: 14pt; padding: 5px;")
145 layout.addWidget(self.phase_label)
148 status_group = QtWidgets.QGroupBox("Status")
149 status_layout = QtWidgets.QVBoxLayout()
151 self.status_text = QtWidgets.QTextEdit()
152 self.status_text.setReadOnly(True)
153 self.status_text.setMaximumHeight(150)
154 status_layout.addWidget(self.status_text)
155 status_group.setLayout(status_layout)
156 layout.addWidget(status_group)
159 phase1_group = QtWidgets.QGroupBox("Phase 1: Calibration")
160 phase1_layout = QtWidgets.QVBoxLayout()
162 # Reference input section
163 ref_group = QtWidgets.QGroupBox("Set Reference Values")
164 ref_group.setToolTip(
165 "Enter the known reference value (e.g., pressure in PSI, temperature in °C) "
166 "for each sensor channel. This is the true value you're calibrating against. "
167 "After setting references, click 'Collect' to gather calibration data points."
169 ref_layout = QtWidgets.QVBoxLayout()
171 # Number of reference gauges (1 = one value for all channels, N = one per channel)
172 gauge_group = QtWidgets.QGroupBox("Reference gauges")
173 gauge_layout = QtWidgets.QHBoxLayout()
174 gauge_layout.addWidget(QtWidgets.QLabel("Number of gauges:"))
175 self.num_gauges_spin = QtWidgets.QSpinBox()
176 self.num_gauges_spin.setMinimum(1)
177 self.num_gauges_spin.setMaximum(10)
178 self.num_gauges_spin.setValue(1)
179 self.num_gauges_spin.setToolTip(
180 "If all PTs share one reference gauge, set to 1 and enter one value for Phase 1."
182 gauge_layout.addWidget(self.num_gauges_spin)
183 gauge_group.setLayout(gauge_layout)
184 phase1_layout.addWidget(gauge_group)
186 # Per-sensor-type reference inputs (only for active connectors)
187 for stype, info in self.orchestrator.sensor_types.items():
188 # Collect active channels for this type (active_connectors key is (stype, board_ip))
189 channels_for_type: List[int] = []
193 ), active_conns in self.orchestrator.active_connectors.items():
195 channels_for_type.extend(active_conns)
196 channels_for_type = sorted(set(channels_for_type))
197 if not channels_for_type:
200 # Get sensor role names if available
204 from config_loader import load_config
207 roles = cfg.get("sensor_roles", {})
208 for role, role_ch in roles.items():
209 if role_ch in channels_for_type and stype == "PT":
210 role_map[role_ch] = role
214 type_group = QtWidgets.QGroupBox(f"{stype} ({info.unit})")
215 type_layout = QtWidgets.QGridLayout()
218 for ch in channels_for_type:
221 # Create label with role name if available
222 label_text = f"CH{ch}"
224 label_text = f"{role_map[ch]} (CH{ch})"
226 label = QtWidgets.QLabel(f"{label_text}:")
227 ref_input = QtWidgets.QLineEdit()
228 ref_input.setPlaceholderText(f"Reference {info.unit}")
229 ref_input.setToolTip(
230 f"Enter the known reference value in {info.unit}.\n"
231 f"For example, if calibrating a pressure sensor at 14.7 PSI, enter '14.7'.\n"
232 f"This value will be used as the ground truth for calibration."
234 ref_input.returnPressed.connect(
235 lambda checked, k=key, inp=ref_input: self.set_reference(
240 set_btn = QtWidgets.QPushButton("Set")
241 set_btn.setToolTip("Set this reference value for calibration")
242 set_btn.clicked.connect(
243 lambda checked, k=key, inp=ref_input: self.set_reference(
248 self.ref_inputs[key] = ref_input
249 type_layout.addWidget(label, row, 0)
250 type_layout.addWidget(ref_input, row, 1)
251 type_layout.addWidget(set_btn, row, 2)
254 type_group.setLayout(type_layout)
255 ref_layout.addWidget(type_group)
257 ref_group.setLayout(ref_layout)
258 phase1_layout.addWidget(ref_group)
261 batch_group = QtWidgets.QGroupBox("Batch Reference")
262 batch_layout = QtWidgets.QHBoxLayout()
264 self.batch_type_combo = QtWidgets.QComboBox()
265 self.batch_type_combo.addItems(list(self.orchestrator.sensor_types.keys()))
266 batch_layout.addWidget(QtWidgets.QLabel("Type:"))
267 batch_layout.addWidget(self.batch_type_combo)
269 self.batch_value_input = QtWidgets.QLineEdit()
270 self.batch_value_input.setPlaceholderText("Value")
271 batch_layout.addWidget(self.batch_value_input)
273 batch_set_btn = QtWidgets.QPushButton("Set All")
274 batch_set_btn.clicked.connect(self.set_batch_reference)
275 batch_layout.addWidget(batch_set_btn)
277 # When num_gauges == 1: single ref applies to all PT channels
278 self.single_ref_input = QtWidgets.QLineEdit()
279 self.single_ref_input.setPlaceholderText("One ref (PSI) for all PTs")
280 self.single_ref_btn = QtWidgets.QPushButton("Set ref for all PTs")
281 self.single_ref_btn.clicked.connect(self.set_single_ref_all_pts)
282 batch_layout.addWidget(QtWidgets.QLabel("1-gauge ref:"))
283 batch_layout.addWidget(self.single_ref_input)
284 batch_layout.addWidget(self.single_ref_btn)
286 batch_group.setLayout(batch_layout)
287 phase1_layout.addWidget(batch_group)
289 # Collection controls
290 collect_group = QtWidgets.QGroupBox("Collection")
291 collect_layout = QtWidgets.QHBoxLayout()
293 self.collect_duration_input = QtWidgets.QLineEdit("5.0")
294 self.collect_duration_input.setPlaceholderText("Duration (sec)")
295 collect_layout.addWidget(QtWidgets.QLabel("Duration:"))
296 collect_layout.addWidget(self.collect_duration_input)
298 self.collect_btn = QtWidgets.QPushButton("Collect")
299 self.collect_btn.clicked.connect(self.start_collection)
300 collect_layout.addWidget(self.collect_btn)
302 collect_group.setLayout(collect_layout)
303 phase1_layout.addWidget(collect_group)
306 action_layout = QtWidgets.QHBoxLayout()
308 self.fit_btn = QtWidgets.QPushButton("Fit (TLS+Bayesian)")
309 self.fit_btn.clicked.connect(self.fit_calibrations)
310 action_layout.addWidget(self.fit_btn)
312 self.save_btn = QtWidgets.QPushButton("Save calibration")
313 self.save_btn.setToolTip(
314 "Save calibration constants to JSON in config calibration path"
316 self.save_btn.clicked.connect(self.save_calibrations)
317 action_layout.addWidget(self.save_btn)
319 self.clear_btn = QtWidgets.QPushButton("Clear calibration")
320 self.clear_btn.setToolTip("Clear all calibration data and start from scratch")
321 self.clear_btn.clicked.connect(self.clear_calibrations)
322 action_layout.addWidget(self.clear_btn)
324 phase1_layout.addLayout(action_layout)
327 self.done_btn = QtWidgets.QPushButton("Done → Phase 2")
328 self.done_btn.clicked.connect(self.transition_to_phase2)
329 phase1_layout.addWidget(self.done_btn)
331 phase1_group.setLayout(phase1_layout)
332 layout.addWidget(phase1_group)
335 phase2_group = QtWidgets.QGroupBox("Phase 2: Monitoring")
336 phase2_layout = QtWidgets.QVBoxLayout()
338 self.monitor_status_label = QtWidgets.QLabel("Not started")
339 phase2_layout.addWidget(self.monitor_status_label)
341 phase2_group.setLayout(phase2_layout)
342 layout.addWidget(phase2_group)
344 # Actuator controls (if available)
346 hasattr(self.orchestrator, "actuator_comm")
347 and self.orchestrator.actuator_comm
349 actuator_group = QtWidgets.QGroupBox("Actuator Control")
350 actuator_layout = QtWidgets.QGridLayout()
352 # Get number of actuators dynamically from config/CSV
355 from pathlib import Path
357 # Try to import from combined_gui if available
361 Path(__file__).parent.parent.parent
368 from combined_gui import get_num_actuators
370 num_actuators = get_num_actuators()
372 # Fallback: try to count from CSV
374 Path(__file__).parent.parent.parent
376 / "state_machine_actuators.csv"
378 if csv_path.exists():
381 with open(csv_path, "r") as f:
382 reader = csv.reader(f)
388 if len(row) > 0 and row[0].strip()
398 # Calculate optimal grid layout
399 if num_actuators <= 8:
401 elif num_actuators <= 16:
406 for i in range(1, num_actuators + 1):
407 on_btn = QtWidgets.QPushButton(f"ON {i}")
408 off_btn = QtWidgets.QPushButton(f"OFF {i}")
409 on_btn.clicked.connect(
410 lambda checked, aid=i: self.send_actuator_command(aid, 1)
412 off_btn.clicked.connect(
413 lambda checked, aid=i: self.send_actuator_command(aid, 0)
416 row = (i - 1) // cols
418 actuator_layout.addWidget(
419 QtWidgets.QLabel(f"Act {i}:"), row * 2, col * 2
421 actuator_layout.addWidget(on_btn, row * 2, col * 2 + 1)
422 actuator_layout.addWidget(off_btn, row * 2 + 1, col * 2 + 1)
424 actuator_group.setLayout(actuator_layout)
425 layout.addWidget(actuator_group)
431 def create_plot_panel(self) -> QtWidgets.QWidget:
432 """Create right plot panel"""
433 panel = QtWidgets.QWidget()
434 layout = QtWidgets.QVBoxLayout(panel)
436 # Tab widget for different views
437 tabs = QtWidgets.QTabWidget()
439 # Live data tab - grouped by sensor type
440 live_tab = QtWidgets.QWidget()
441 live_layout = QtWidgets.QVBoxLayout(live_tab)
443 # Scroll area for plots
444 scroll = QtWidgets.QScrollArea()
445 scroll.setWidgetResizable(True)
446 scroll_widget = QtWidgets.QWidget()
447 scroll_layout = QtWidgets.QVBoxLayout(scroll_widget)
449 # Group sensors by type
450 self.plot_widgets: Dict[str, pg.GraphicsLayoutWidget] = {}
451 self.plot_curves: Dict[Tuple[str, int], pg.PlotDataItem] = {}
453 # Collect sensors grouped by type
454 sensors_by_type: Dict[str, List[Tuple[str, int]]] = defaultdict(list)
459 ), active_conns in self.orchestrator.active_connectors.items():
460 for ch in active_conns:
462 sensors_by_type[stype].append(key)
464 # Create a plot widget for each sensor type
465 for stype in sorted(sensors_by_type.keys()):
467 sensors_by_type[stype], key=lambda x: x[1]
470 # Create plot widget for this sensor type
471 plot_widget = pg.GraphicsLayoutWidget()
472 plot_widget.setBackground(COLORS["bg"])
473 plot = plot_widget.addPlot(title=f"{stype} Sensors")
474 plot.setLabel("left", "ADC Code")
475 plot.setLabel("bottom", "Time (s)")
476 plot.showGrid(x=True, y=True, alpha=0.3)
479 self.plot_widgets[stype] = plot_widget
481 # Create curves for each sensor in this type
482 for idx, key in enumerate(sensors):
485 # Initialize data buffers
486 self.plot_data[key] = deque(maxlen=1000)
487 self.plot_times[key] = deque(maxlen=1000)
489 # Create curve with channel label
490 color = pg.intColor(idx, len(sensors)) # Cycle through colors
491 curve = plot.plot([], [], pen=color, name=f"CH{ch}")
492 self.plot_curves[key] = curve
494 scroll_layout.addWidget(plot_widget)
496 scroll.setWidget(scroll_widget)
497 live_layout.addWidget(scroll)
499 tabs.addTab(live_tab, "Live Data")
501 # Calibration status tab
502 status_tab = QtWidgets.QWidget()
503 status_layout = QtWidgets.QVBoxLayout(status_tab)
505 self.status_table = QtWidgets.QTableWidget()
506 self.status_table.setColumnCount(7)
507 self.status_table.setHorizontalHeaderLabels(
508 ["Type", "Channel", "Points", "RMSE", "R²", "Confidence", "Status"]
510 status_layout.addWidget(self.status_table)
512 tabs.addTab(status_tab, "Calibration Status")
514 layout.addWidget(tabs)