652 def _add_channel(self, ch):
653 """Dynamically add a new active channel"""
667 pen = pg.mkPen(color=color, width=2)
668 self.curves[ch] = self.plot.plot([], [], name=f"CH{ch}", pen=pen)
670 # Add sensor selector
671 self.cmb_sensor.addItem(f"Channel {ch}")
673 # Add sensor status label
674 lbl = QtWidgets.QLabel(f"CH{ch}: Online (no calibration)")
675 lbl.setStyleSheet("color: #00ff00; font-weight: bold;")
676 self.sensor_labels[ch] = lbl
677 self.sensors_layout.addWidget(lbl)
680 prog_lbl = QtWidgets.QLabel(f"CH{ch}: 0 points, LOW confidence")
681 self.progress_labels[ch] = prog_lbl
682 self.progress_layout.addWidget(prog_lbl)
684 # Get or create calibration
685 self.cal_manager.get_or_create(ch)
687 def _add_calibration_point(self):
688 """Add a human-provided calibration point"""
689 # Get selected sensor
690 sensor_text = self.cmb_sensor.currentText()
692 QtWidgets.QMessageBox.warning(self, "Error", "No sensor selected")
695 ch = int(sensor_text.split()[-1])
697 # Get reference pressure
699 ref_pressure_str = self.txt_ref_pressure.text().strip()
700 ref_pressure = float(ref_pressure_str)
702 # Auto-detect units (if < 1000, assume kPa, otherwise Pa)
703 if ref_pressure < 1000:
704 ref_pressure *= 1000 # Convert kPa to Pa
706 QtWidgets.QMessageBox.warning(self, "Error", "Invalid pressure value")
709 # Get current voltage for this channel
710 if ch not in self.channel_data or not self.channel_data[ch]["v"]:
711 QtWidgets.QMessageBox.warning(self, "Error", f"No data for channel {ch}")
714 current_voltage = self.channel_data[ch]["v"][-1]
717 cal = self.cal_manager.get_or_create(ch)
718 cal.add_calibration_point(current_voltage, ref_pressure, self.env_state)
721 self.txt_ref_pressure.clear()
724 confidence_names = ["LOW", "MEDIUM", "HIGH", "MAXIMUM"]
725 QtWidgets.QMessageBox.information(
727 "Calibration Point Added",
728 f"Channel {ch}: {current_voltage:.4f}V → {ref_pressure/1000:.1f} kPa\n"
729 f"Total points: {cal.human_input_count}\n"
730 f"Confidence: {confidence_names[cal.confidence_level]}\n"
731 f"Reliability: {cal.get_reliability_score()*100:.1f}%",
734 # Auto-save after each point
735 self.cal_manager.save_calibrations()
737 def _update_display(self):
738 """Update plots and displays"""
740 for ch in self.active_channels:
741 if ch in self.curves and ch in self.channel_data:
742 ts = list(self.channel_data[ch]["t"])
743 vs = list(self.channel_data[ch]["v"])
745 self.curves[ch].setData(ts, vs)
747 # Update current voltage display
748 sensor_text = self.cmb_sensor.currentText()
750 ch = int(sensor_text.split()[-1])
751 if ch in self.channel_data and self.channel_data[ch]["v"]:
752 current_v = self.channel_data[ch]["v"][-1]
753 current_raw = self.channel_data[ch]["raw"][-1]
754 self.lbl_current_v.setText(f"{current_v:.6f} V")
755 self.lbl_current_raw.setText(f"{current_raw}")
757 # Show prediction if calibrated
758 cal = self.cal_manager.get_or_create(ch)
759 if cal.human_input_count >= 2:
760 p_mean, p_std = cal.predict_pressure(current_v, self.env_state)
761 self.lbl_prediction.setText(
762 f"Prediction: {p_mean/1000:.2f} ± {p_std/1000:.2f} kPa"
765 self.lbl_prediction.setText(
766 "Prediction: Need ≥2 calibration points"
769 # Update sensor status
770 for ch in self.active_channels:
771 if ch in self.sensor_labels:
772 cal = self.cal_manager.get_or_create(ch)
773 confidence_names = ["LOW", "MEDIUM", "HIGH", "MAXIMUM"]
774 status = f"CH{ch}: {cal.human_input_count} pts, {confidence_names[cal.confidence_level]}, {cal.get_reliability_score()*100:.0f}% reliable"
775 self.sensor_labels[ch].setText(status)
778 for ch in self.active_channels:
779 if ch in self.progress_labels:
780 cal = self.cal_manager.get_or_create(ch)
781 confidence_names = ["LOW", "MEDIUM", "HIGH", "MAXIMUM"]
782 self.progress_labels[ch].setText(
783 f"CH{ch}: {cal.human_input_count} points, {confidence_names[cal.confidence_level]} confidence"
786 # Remove stale channels (not seen in 5 seconds)
788 for ch in list(self.active_channels):
789 if ch in self.last_seen and (now - self.last_seen[ch]) > 5.0:
790 if ch in self.sensor_labels:
791 self.sensor_labels[ch].setText(f"CH{ch}: OFFLINE")
792 self.sensor_labels[ch].setStyleSheet("color: #ff0000;")