3PT_BOARD_multi_gui – Live plots for PT_BOARD_Multi_Send
5Receives DAQv2-Comms SENSOR_DATA packets over UDP from PT_BOARD_Multi_Send,
6decodes them, and displays real-time voltage plots for each PT (1–10).
8Ensure the firmware's receiverIP matches this machine's IP (or use 192.168.2.20)
9and that the GUI listens on port 5007 (PT board). Actuator_Testing uses 5006 so
12 pip install -r requirements.txt
13 python pt_multi_gui.py [-p 5007] [-a 0.0.0.0]
15 On macOS, if you get a segfault: use PyQt5 instead of PyQt6
16 (pip install PyQt5; this script will try PyQt5 if PyQt6 is unavailable).
25from typing
import Optional, Tuple, List, Dict
26from collections
import deque
29if sys.platform ==
"darwin":
30 os.environ.setdefault(
"QT_MAC_WANTS_LAYER",
"1")
32 from PyQt5
import QtCore, QtGui, QtWidgets
34 _QT_HORIZ = QtCore.Qt.Horizontal
36 from PyQt6
import QtCore, QtGui, QtWidgets
38 _QT_HORIZ = QtCore.Qt.Orientation.Horizontal
41 from PyQt6
import QtCore, QtGui, QtWidgets
43 _QT_HORIZ = QtCore.Qt.Orientation.Horizontal
45 from PyQt5
import QtCore, QtGui, QtWidgets
47 _QT_HORIZ = QtCore.Qt.Horizontal
53pg.setConfigOptions(antialias=
False, useOpenGL=
False)
57PacketType = type(
"PacketType", (), {
"SENSOR_DATA": 3})
58PACKET_HEADER_FORMAT =
"<BBI"
60SENSOR_DATA_PACKET_FORMAT =
"<BB"
61SENSOR_DATA_PACKET_SIZE = 2
62SENSOR_DATA_CHUNK_FORMAT =
"<I"
63SENSOR_DATA_CHUNK_SIZE = 4
64SENSOR_DATAPOINT_FORMAT =
"<Bf"
65SENSOR_DATAPOINT_SIZE = 5
68DEFAULT_WINDOW_SECONDS = 30.0
70UPDATE_INTERVAL_MS = 50
90 -1.14351422603069e-09,
95 (-8.46148513e-09, 3.85591637e-05, 1.50430101e-01, -8.31516318e01),
96 (-1.12886040e-08, 5.37687043e-05, 1.24582809e-01, -7.92816379e01),
97 (-2.92357884e-09, 1.60928197e-05, 1.73172679e-01, -8.41620248e01),
98 (-2.07890243e-09, 7.98159784e-06, 1.92631458e-01, -7.84202084e01),
100 -1.14351422603069e-09,
101 6.07260454422966e-06,
106 -1.14351422603069e-09,
107 6.07260454422966e-06,
112 -1.14351422603069e-09,
113 6.07260454422966e-06,
118 -1.14351422603069e-09,
119 6.07260454422966e-06,
124 -1.14351422603069e-09,
125 6.07260454422966e-06,
135 raw_value: float, a: float, b: float, c: float, d: float
137 """Convert raw voltage to pressure using polynomial calibration."""
139 return (a * (v**3)) + (b * (v**2)) + (c * v) + d
143 if len(data) < PACKET_HEADER_SIZE:
146 return struct.unpack(PACKET_HEADER_FORMAT, data[:PACKET_HEADER_SIZE])
152 if len(data) < PACKET_HEADER_SIZE + SENSOR_DATA_PACKET_SIZE:
155 if header
is None or header[0] != PacketType.SENSOR_DATA:
157 packet_type, version, timestamp = header
158 offset = PACKET_HEADER_SIZE
160 num_chunks, num_sensors = struct.unpack(
161 SENSOR_DATA_PACKET_FORMAT,
162 data[offset : offset + SENSOR_DATA_PACKET_SIZE],
166 offset += SENSOR_DATA_PACKET_SIZE
167 per_chunk = SENSOR_DATA_CHUNK_SIZE + num_sensors * SENSOR_DATAPOINT_SIZE
170 < PACKET_HEADER_SIZE + SENSOR_DATA_PACKET_SIZE + num_chunks * per_chunk
174 for _
in range(num_chunks):
175 (chunk_ts,) = struct.unpack(
176 SENSOR_DATA_CHUNK_FORMAT, data[offset : offset + SENSOR_DATA_CHUNK_SIZE]
178 offset += SENSOR_DATA_CHUNK_SIZE
180 for _
in range(num_sensors):
181 sid, val = struct.unpack(
182 SENSOR_DATAPOINT_FORMAT, data[offset : offset + SENSOR_DATAPOINT_SIZE]
184 datapoints.append({
"sensor_id": sid,
"data": val})
185 offset += SENSOR_DATAPOINT_SIZE
186 chunks.append({
"timestamp": chunk_ts,
"datapoints": datapoints})
188 {
"packet_type": packet_type,
"version": version,
"timestamp": timestamp},
194 sensor_data_received = QtCore.pyqtSignal(dict, list)
195 status_update = QtCore.pyqtSignal(str)
220 "packets_per_sec": 0.0,
221 "bytes_per_sec": 0.0,
225 bps = self.
total_bytes / elapsed
if elapsed > 0
else 0.0
229 "packets_per_sec": pps,
230 "bytes_per_sec": bps,
234 self.
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
235 self.
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
236 self.
sock.settimeout(0.1)
239 msg = f
"Listening {self.bind_address}:{self.port} — Set ESP RECEIVER_IP to this PC. If Packets stay 0: Mac: allow Python in Firewall."
245 while not self.
_stop:
247 data, _ = self.
sock.recvfrom(MAX_PACKET_SIZE)
251 if header
is not None and header[0] == PacketType.SENSOR_DATA:
255 except socket.timeout:
257 except Exception
as e:
269 def __init__(self, port: int = DEFAULT_PORT, bind_address: str =
"0.0.0.0"):
289 return f
"PT {sensor_id + 1}"
292 self.setWindowTitle(f
"PT Board Multi – Live Plots (port {self.port})")
293 self.setGeometry(100, 100, 1200, 700)
294 central = QtWidgets.QWidget()
295 self.setCentralWidget(central)
296 layout = QtWidgets.QVBoxLayout(central)
297 top = QtWidgets.QHBoxLayout()
299 self.
status_label.setStyleSheet(
"font-weight: bold; padding: 5px;")
302 settings_btn = QtWidgets.QPushButton(
"Settings")
304 top.addWidget(settings_btn)
305 csv_btn = QtWidgets.QPushButton(
"Download CSV")
307 top.addWidget(csv_btn)
308 layout.addLayout(top)
309 plot_stats = QtWidgets.QHBoxLayout()
313 stats_group = QtWidgets.QGroupBox(
"Statistics")
314 sl = QtWidgets.QVBoxLayout()
320 w.setFont(QtGui.QFont(
"", 11))
326 stats_group.setLayout(sl)
327 stats_group.setFixedWidth(180)
328 plot_stats.addWidget(stats_group)
330 pt_group = QtWidgets.QGroupBox(
"Channels")
331 pt_layout = QtWidgets.QVBoxLayout()
334 for i
in range(NUM_PTS):
335 row = QtWidgets.QHBoxLayout()
336 cb = QtWidgets.QCheckBox(self.
pt_label(i))
341 vl = QtWidgets.QLabel(f
"-- {PRESSURE_UNIT}")
343 f
"color: rgb{PT_COLORS[i % len(PT_COLORS)]}; font-weight: bold; min-width: 60px;"
347 pt_layout.addLayout(row)
348 pt_group.setLayout(pt_layout)
349 pt_group.setFixedWidth(140)
350 plot_stats.addWidget(pt_group)
351 layout.addLayout(plot_stats, 1)
353 self.
plot_item.setTitle(
"PT Voltages", color=
"w", size=
"14pt")
354 self.
plot_item.setLabel(
"left",
"Voltage (V)", color=
"w")
355 self.
plot_item.setLabel(
"bottom",
"Time (s)", color=
"w")
357 self.
plot_item.showGrid(x=
True, y=
True, alpha=0.5)
358 self.
plot_item.getViewBox().setBackgroundColor(
"k")
359 f = QtGui.QFont(
"", 12)
360 for ax
in (
"left",
"bottom"):
362 a.setStyle(tickFont=f)
371 self.
legend.setBrush(pg.mkBrush(
"k"))
372 self.
legend.setPen(pg.mkPen(
"w"))
374 for i
in range(NUM_PTS):
375 c = PT_COLORS[i % len(PT_COLORS)]
376 pen = pg.mkPen(color=c, width=2)
380 for it
in self.
legend.items
or []:
399 for dp
in ch[
"datapoints"]:
400 sid = int(dp[
"sensor_id"])
401 if 0 <= sid < NUM_PTS:
402 self.
sensor_data[sid].append((t0, float(dp[
"data"])))
407 for i
in range(NUM_PTS):
425 self.
sensor_plots[i].setData(np.array(times), np.array(vals))
427 self.
plot_item.setXRange(t - win, t, padding=0)
429 self.
plot_item.setXRange(0, win, padding=0)
436 self.
pps_label.setText(f
"Packets/sec: {s['packets_per_sec']:.2f}")
439 f
"Bytes: {b/1024:.2f} KB" if b >= 1024
else f
"Bytes: {b} B"
441 bp = s[
"bytes_per_sec"]
443 f
"Bytes/sec: {bp/1024:.2f} KB/s"
445 else f
"Bytes/sec: {bp:.2f} B/s"
449 """Save all PT timestamp and voltage data to a CSV file."""
450 filepath, _ = QtWidgets.QFileDialog.getSaveFileName(
451 self,
"Save CSV",
"",
"CSV Files (*.csv);;All Files (*)"
455 if not filepath.endswith(
".csv"):
458 with open(filepath,
"w", newline=
"")
as f:
459 writer = csv.writer(f)
461 header = [
"timestamp"] + [f
"PT{i+1}_voltage" for i
in range(NUM_PTS)]
462 writer.writerow(header)
465 for i
in range(NUM_PTS):
469 for ti
in sorted(all_times):
471 for i
in range(NUM_PTS):
481 except Exception
as e:
485 d = QtWidgets.QDialog(self)
486 d.setWindowTitle(
"Settings")
487 lo = QtWidgets.QVBoxLayout(d)
488 lo.addWidget(QtWidgets.QLabel(
"Time window (s)"))
489 row = QtWidgets.QHBoxLayout()
490 sld = QtWidgets.QSlider(_QT_HORIZ)
494 lbl = QtWidgets.QLabel(f
"{self.window_seconds:.0f}")
500 sld.valueChanged.connect(onv)
501 row.addWidget(sld, 1)
504 cb = QtWidgets.QPushButton(
"Close")
505 cb.clicked.connect(d.accept)
519 p = argparse.ArgumentParser(
520 description=
"PT Board Multi – live PT plots from DAQv2-Comms UDP"
526 default=DEFAULT_PORT,
527 help=f
"UDP port (default {DEFAULT_PORT})",
529 p.add_argument(
"-a",
"--address", default=
"0.0.0.0", help=
"Bind address")
531 app = QtWidgets.QApplication(sys.argv)
534 sys.exit(app.exec_()
if hasattr(app,
"exec_")
else app.exec())
537if __name__ ==
"__main__":
__init__(self, int port=DEFAULT_PORT, str bind_address="0.0.0.0")
str pt_label(self, int sensor_id)
_on_plot_toggle(self, int idx, state)
__init__(self, int port, str bind_address)
static void stop(emmc_log_t *log, emmc_disk_state_t why)
static void start(gnss_frame_parser_t *p, uint8_t byte)
Optional[Tuple[dict, List[dict]]] parse_sensor_data_packet(bytes data)
float calculate_pressure(float raw_value, float a, float b, float c, float d)
Optional[Tuple[int, int, int]] parse_packet_header(bytes data)
static void emit(sensors_core_t *c, uint8_t type, const void *data, uint8_t len)
TOML_NODISCARD_CTOR time() noexcept=default