937 elodin_client: Optional[ElodinClient] = None,
938 bind_address: str = "0.0.0.0",
941 super().__init__(parent)
942 self.receiver = receiver
943 self.elodin_client = elodin_client
944 self.bind_address = bind_address
947 self.sensor_data: Dict[int, deque] = {}
948 self.sensor_adc_codes: Dict[int, deque] = (
950 ) # Store raw ADC codes for calibration
951 self.sensor_psi_data: Dict[int, deque] = {}
952 self.sensor_plots: Dict[int, pg.PlotDataItem] = {}
953 self.sensor_labels: Dict[int, str] = {
954 i: CONFIG.get_sensor_label(i) for i in range(1, NUM_SENSORS + 1)
958 self.pt_calibration = load_pt_calibration()
960 # Filter by source IP (only accept PT board data, not actuator board)
961 self.filter_source_ip = CONFIG.config["network"].get(
962 "sensor_ip_filter", DEFAULT_SENSOR_IP
965 # ADC conversion settings
967 self.reference_voltage = 2.5
970 self.window_seconds = CONFIG.config["display"]["window_seconds"]
971 self.y_axis_auto_scale = CONFIG.config["display"]["y_axis_autoscale"]
972 self.y_axis_min = CONFIG.config["display"]["y_axis_min"]
973 self.y_axis_max = CONFIG.config["display"]["y_axis_max"]
975 self.stats_start_time = time.time()
981 self.receiver.sensor_data_received.connect(self.on_sensor_data)
982 self.receiver.status_update.connect(self.on_status_update)
985 self.update_timer = QtCore.QTimer(self)
986 self.update_timer.timeout.connect(self.update_plots)
987 self.update_timer.start(UPDATE_INTERVAL_MS)
989 # Elodin polling if available
990 if self.elodin_client and self.elodin_client.connected:
991 self.elodin_timer = QtCore.QTimer(self)
992 self.elodin_timer.timeout.connect(self.poll_elodin_data)
993 self.elodin_timer.start(100) # 10 Hz
1005 layout = QtWidgets.QVBoxLayout(self)
1008 top_panel = QtWidgets.QHBoxLayout()
1009 self.status_label = QtWidgets.QLabel("Starting...")
1010 self.status_label.setStyleSheet("font-weight: bold; padding: 5px;")
1011 top_panel.addWidget(self.status_label)
1012 top_panel.addStretch()
1014 self.auto_scale_checkbox = QtWidgets.QCheckBox("Auto-scale Y-axis")
1015 self.auto_scale_checkbox.setChecked(self.y_axis_auto_scale)
1016 self.auto_scale_checkbox.stateChanged.connect(self.on_auto_scale_toggled)
1017 top_panel.addWidget(self.auto_scale_checkbox)
1019 layout.addLayout(top_panel)
1022 plot_stats_layout = QtWidgets.QHBoxLayout()
1025 self.plot_widget = pg.GraphicsLayoutWidget()
1026 self.plot_widget.setBackground("k")
1027 plot_stats_layout.addWidget(self.plot_widget, 1)
1030 stats_widget = QtWidgets.QWidget()
1031 stats_layout = QtWidgets.QVBoxLayout(stats_widget)
1034 network_group = QtWidgets.QGroupBox("Network")
1035 network_layout = QtWidgets.QVBoxLayout()
1036 self.packets_label = QtWidgets.QLabel("Packets: 0")
1037 self.pps_label = QtWidgets.QLabel("Packets/sec: 0.0")
1038 network_layout.addWidget(self.packets_label)
1039 network_layout.addWidget(self.pps_label)
1040 network_group.setLayout(network_layout)
1041 stats_layout.addWidget(network_group)
1044 sensor_group = QtWidgets.QGroupBox("Sensors")
1045 sensor_layout = QtWidgets.QVBoxLayout()
1046 self.connector_labels = {}
1047 for i in range(1, NUM_SENSORS + 1):
1048 label = QtWidgets.QLabel(f"C{i}: --- V")
1049 color_idx = i % len(SENSOR_COLORS)
1050 color = SENSOR_COLORS[color_idx]
1051 label.setStyleSheet(f"color: rgb{color}; padding: 2px;")
1052 sensor_layout.addWidget(label)
1053 self.connector_labels[i] = label
1054 sensor_group.setLayout(sensor_layout)
1055 stats_layout.addWidget(sensor_group, 1)
1057 stats_widget.setFixedWidth(200)
1058 plot_stats_layout.addWidget(stats_widget)
1059 layout.addLayout(plot_stats_layout, 1)
1062 self.plot_item = self.plot_widget.addPlot(title="Pressure Data Over Time")
1063 self.plot_item.setTitle("Pressure Data Over Time", color="w", size="14pt")
1064 self.plot_item.setLabel("left", "Pressure (psi)", color="w")
1065 self.plot_item.setLabel("bottom", "Time (seconds)", color="w")
1066 self.plot_item.addLegend()
1067 self.plot_item.showGrid(x=True, y=True, alpha=0.5)
1068 self.plot_item.getViewBox().setBackgroundColor("k")
1071 font = QtGui.QFont()
1072 font.setPointSize(12)
1073 left_axis = self.plot_item.getAxis("left")
1074 bottom_axis = self.plot_item.getAxis("bottom")
1075 left_axis.setStyle(tickFont=font)
1076 bottom_axis.setStyle(tickFont=font)
1077 left_axis.setPen("w")
1078 bottom_axis.setPen("w")
1079 left_axis.setTextPen("w")
1080 bottom_axis.setTextPen("w")
1082 def on_sensor_data(self, header: dict, chunks: List[dict], source_ip: str):
1083 """Handle sensor data from UDP or Elodin - ONLY from PT board, not actuator board"""
1084 # Filter by source IP - only accept PT board data
1085 if source_ip != self.filter_source_ip:
1086 return # Ignore actuator board data (handled separately)
1088 current_time = time.time()
1090 for chunk in chunks:
1091 chunk_timestamp_ms = chunk["timestamp"]
1092 relative_time = current_time - self.stats_start_time
1094 for dp in chunk["datapoints"]:
1095 sensor_id = dp["sensor_id"]
1096 code_uint32 = dp["data"]
1098 # Convert to voltage
1099 voltage = self.code_to_voltage(code_uint32)
1101 # Initialize if needed
1102 if sensor_id not in self.sensor_data:
1103 self.sensor_data[sensor_id] = deque(maxlen=MAX_POINTS)
1104 self.sensor_adc_codes[sensor_id] = deque(maxlen=MAX_POINTS)
1105 # Only create PSI storage and plot for calibrated sensors
1106 if sensor_id in self.pt_calibration:
1107 self.sensor_psi_data[sensor_id] = deque(maxlen=MAX_POINTS)
1108 self.add_sensor_plot(sensor_id)
1111 self.sensor_data[sensor_id].append((relative_time, voltage))
1112 self.sensor_adc_codes[sensor_id].append((relative_time, code_uint32))
1114 # Calculate and store PSI if calibration exists
1115 if sensor_id in self.pt_calibration:
1116 a, b, c, d = self.pt_calibration[sensor_id]
1117 psi = calculate_pressure(code_uint32, a, b, c, d)
1118 self.sensor_psi_data[sensor_id].append((relative_time, psi))
1130 def update_plots(self):
1131 """Update plot data"""
1132 current_time = time.time()
1133 relative_time = current_time - self.stats_start_time
1135 # Update each sensor plot
1136 for sensor_id, plot in self.sensor_plots.items():
1137 if sensor_id in self.sensor_psi_data:
1138 data = self.sensor_psi_data[sensor_id]
1141 cutoff_time = relative_time - self.window_seconds
1142 filtered_data = [(t, v) for t, v in data if t >= cutoff_time]
1145 times = [t for t, v in filtered_data]
1146 values = [v for t, v in filtered_data]
1147 plot.setData(times, values)
1150 if self.y_axis_auto_scale:
1151 self.plot_item.enableAutoRange(axis="y")
1153 self.plot_item.setYRange(self.y_axis_min, self.y_axis_max)
1157 stats = self.receiver.get_stats()
1158 self.packets_label.setText(f"Packets: {stats['packets']}")
1159 self.pps_label.setText(f"Packets/sec: {stats['packets_per_sec']:.1f}")
1161 # Update sensor labels
1162 for sensor_id, label_widget in self.connector_labels.items():
1163 if sensor_id in self.sensor_data and len(self.sensor_data[sensor_id]) > 0:
1164 voltage = self.sensor_data[sensor_id][-1][1]
1166 sensor_id in self.sensor_psi_data
1167 and len(self.sensor_psi_data[sensor_id]) > 0
1169 psi = self.sensor_psi_data[sensor_id][-1][1]
1170 label_text = self.sensor_labels.get(sensor_id, "")
1172 label_widget.setText(
1173 f"C{sensor_id}: {label_text}<br/>{voltage:.3f} V<br/>{psi:.2f} psi"
1176 label_widget.setText(
1177 f"C{sensor_id}: {voltage:.3f} V<br/>{psi:.2f} psi"
1180 label_widget.setText(f"C{sensor_id}: {voltage:.3f} V")