STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
imu_calibration_gui.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3IMU Calibration GUI
4
5Interactive GUI for calibrating IMU sensors (accelerometer, gyroscope, magnetometer).
6Similar to PT calibration GUI.
7
8Usage:
9 python imu_calibration_gui.py [--port PORT] [--sensor-type TYPE]
10"""
11
12import sys
13import socket
14import struct
15import threading
16import time
17import json
18from typing import Optional, Dict, List
19from collections import deque
20from dataclasses import dataclass
21
22try:
23 from PyQt6 import QtCore, QtGui, QtWidgets
24 from PyQt6.QtCore import Qt
25except ImportError:
26 from PyQt5 import QtCore, QtGui, QtWidgets
27 from PyQt5.QtCore import Qt
28
29import pyqtgraph as pg
30import numpy as np
31
32from imu_calibration import (
33 IMUCalibrationSystem,
34 SensorType,
35 CalibrationPoint,
36 AccelerometerCalibrator,
37 GyroscopeCalibrator,
38 MagnetometerCalibrator,
39)
40
41# DAQv2-Comms protocol constants
42MAX_PACKET_SIZE = 512
43PACKET_TYPE_SENSOR_DATA = 3
44PACKET_HEADER_FORMAT = "<BBI"
45PACKET_HEADER_SIZE = 6
46SENSOR_DATA_PACKET_FORMAT = "<BB"
47SENSOR_DATA_PACKET_SIZE = 2
48SENSOR_DATA_CHUNK_FORMAT = "<I"
49SENSOR_DATA_CHUNK_SIZE = 4
50SENSOR_DATAPOINT_FORMAT = "<BI" # sensor_id uint8, data uint32
51SENSOR_DATAPOINT_SIZE = 5
52
53DEFAULT_PORT = 5008
54UPDATE_INTERVAL_MS = 50
55MAX_POINTS = 1000
56
57
58@dataclass
60 """IMU sensor reading"""
61
62 accel: np.ndarray
63 gyro: np.ndarray
64 mag: np.ndarray
65 temperature: float
66 timestamp: float
67
68
69class UDPReceiver(QtCore.QThread):
70 """UDP receiver thread for sensor data"""
71
72 data_received = QtCore.pyqtSignal(object)
73
74 def __init__(self, port: int):
75 super().__init__()
76 self.port = port
77 self.running = False
78 self.sock: Optional[socket.socket] = None
79
80 def run(self):
81 """Receive UDP packets"""
82 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
83 self.sock.bind(("0.0.0.0", self.port))
84 self.sock.settimeout(1.0)
85 self.running = True
86
87 while self.running:
88 try:
89 data, addr = self.sock.recvfrom(MAX_PACKET_SIZE)
90 # Parse packet and emit signal
91 # This is simplified - would need full packet parsing
92 reading = self.parse_packet(data)
93 if reading:
94 self.data_received.emit(reading)
95 except socket.timeout:
96 continue
97 except Exception as e:
98 print(f"Error receiving data: {e}")
99
100 def parse_packet(self, data: bytes) -> Optional[IMUReading]:
101 """Parse sensor data packet"""
102 # Simplified parsing - would need full DAQv2-Comms protocol
103 # For now, return None
104 return None
105
106 def stop(self):
107 """Stop receiver"""
108 self.running = False
109 if self.sock:
110 self.sock.close()
111
112
113class IMUCalibrationGUI(QtWidgets.QMainWindow):
114 """Main calibration GUI window"""
115
116 def __init__(self, sensor_type: str = "accel", port: int = DEFAULT_PORT):
117 super().__init__()
118 self.sensor_type = sensor_type
119 self.port = port
120
121 # Calibration system
122 self.calib_system = IMUCalibrationSystem()
123 if sensor_type == "accel":
124 self.calibrator = self.calib_system.create_calibrator(
125 SensorType.ACCELEROMETER, "imu_0"
126 )
127 elif sensor_type == "gyro":
128 self.calibrator = self.calib_system.create_calibrator(
129 SensorType.GYROSCOPE, "imu_0"
130 )
131 else:
132 self.calibrator = self.calib_system.create_calibrator(
133 SensorType.MAGNETOMETER, "imu_0"
134 )
135
136 # Data storage
137 self.raw_readings: Dict[str, deque] = {
138 "x": deque(maxlen=MAX_POINTS),
139 "y": deque(maxlen=MAX_POINTS),
140 "z": deque(maxlen=MAX_POINTS),
141 }
142 self.timestamps = deque(maxlen=MAX_POINTS)
143 self.calibration_points: List[CalibrationPoint] = []
144
145 # Setup UI
146 self.setup_ui()
147
148 # UDP receiver
149 self.receiver = UDPReceiver(port)
150 self.receiver.data_received.connect(self.on_data_received)
151 self.receiver.start()
152
153 # Update timer
154 self.update_timer = QtCore.QTimer()
155 self.update_timer.timeout.connect(self.update_plots)
156 self.update_timer.start(UPDATE_INTERVAL_MS)
157
158 def setup_ui(self):
159 """Setup user interface"""
160 self.setWindowTitle(f"IMU Calibration - {self.sensor_type.upper()}")
161 self.setGeometry(100, 100, 1200, 800)
162
163 # Central widget
164 central = QtWidgets.QWidget()
165 self.setCentralWidget(central)
166 layout = QtWidgets.QVBoxLayout(central)
167
168 # Plot widget
169 self.plot_widget = pg.GraphicsLayoutWidget()
170 layout.addWidget(self.plot_widget)
171
172 # Create plots for X, Y, Z
173 self.plots = {}
174 for axis in ["x", "y", "z"]:
175 plot = self.plot_widget.addPlot(title=f"{axis.upper()} Axis")
176 plot.setLabel("left", "Value")
177 plot.setLabel("bottom", "Time (s)")
178 curve = plot.plot(pen=pg.mkPen(color=axis, width=2))
179 self.plots[axis] = {"plot": plot, "curve": curve}
180
181 # Control panel
182 control_panel = QtWidgets.QHBoxLayout()
183
184 # Add calibration point button
185 self.add_point_btn = QtWidgets.QPushButton("Add Calibration Point")
186 self.add_point_btn.clicked.connect(self.add_calibration_point)
187 control_panel.addWidget(self.add_point_btn)
188
189 # Calibrate button
190 self.calibrate_btn = QtWidgets.QPushButton("Calibrate")
191 self.calibrate_btn.clicked.connect(self.perform_calibration)
192 control_panel.addWidget(self.calibrate_btn)
193
194 # Save button
195 self.save_btn = QtWidgets.QPushButton("Save Calibration")
196 self.save_btn.clicked.connect(self.save_calibration)
197 control_panel.addWidget(self.save_btn)
198
199 # Status label
200 self.status_label = QtWidgets.QLabel("Ready")
201 control_panel.addWidget(self.status_label)
202
203 layout.addLayout(control_panel)
204
205 def on_data_received(self, reading: IMUReading):
206 """Handle received sensor data"""
207 if self.sensor_type == "accel":
208 values = reading.accel
209 elif self.sensor_type == "gyro":
210 values = reading.gyro
211 else:
212 values = reading.mag
213
214 self.raw_readings["x"].append(values[0])
215 self.raw_readings["y"].append(values[1])
216 self.raw_readings["z"].append(values[2])
217 self.timestamps.append(reading.timestamp)
218
219 def update_plots(self):
220 """Update plots with latest data"""
221 if not self.timestamps:
222 return
223
224 times = np.array(self.timestamps)
225 if len(times) > 1:
226 times = times - times[0] # Normalize to start at 0
227
228 for axis in ["x", "y", "z"]:
229 if len(self.raw_readings[axis]) > 0:
230 values = np.array(self.raw_readings[axis])
231 self.plots[axis]["curve"].setData(times, values)
232
233 def add_calibration_point(self):
234 """Add current reading as calibration point"""
235 if not self.raw_readings["x"]:
236 self.status_label.setText("No data available")
237 return
238
239 # Get current reading
240 raw = np.array(
241 [
242 self.raw_readings["x"][-1],
243 self.raw_readings["y"][-1],
244 self.raw_readings["z"][-1],
245 ]
246 )
247
248 # Get reference value from user
249 ref, ok = self.get_reference_value()
250 if not ok:
251 return
252
253 # Create calibration point
254 point = CalibrationPoint(
255 raw_value=raw,
256 reference_value=ref,
257 temperature=25.0, # Would get from sensor
258 timestamp=time.time(),
259 )
260
261 self.calibrator.add_calibration_point(point)
262 self.calibration_points.append(point)
263
264 self.status_label.setText(
265 f"Added calibration point {len(self.calibration_points)}"
266 )
267
268 def get_reference_value(self) -> tuple:
269 """Get reference value from user input"""
270 dialog = QtWidgets.QDialog(self)
271 dialog.setWindowTitle("Enter Reference Value")
272 layout = QtWidgets.QVBoxLayout(dialog)
273
274 # Input fields for X, Y, Z
275 inputs = {}
276 for axis in ["X", "Y", "Z"]:
277 label = QtWidgets.QLabel(f"{axis}:")
278 input_field = QtWidgets.QLineEdit()
279 inputs[axis.lower()] = input_field
280 row = QtWidgets.QHBoxLayout()
281 row.addWidget(label)
282 row.addWidget(input_field)
283 layout.addLayout(row)
284
285 # Buttons
286 buttons = QtWidgets.QDialogButtonBox(
287 QtWidgets.QDialogButtonBox.StandardButton.Ok
288 | QtWidgets.QDialogButtonBox.StandardButton.Cancel
289 )
290 buttons.accepted.connect(dialog.accept)
291 buttons.rejected.connect(dialog.reject)
292 layout.addWidget(buttons)
293
294 if dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted:
295 try:
296 ref = np.array(
297 [
298 float(inputs["x"].text()),
299 float(inputs["y"].text()),
300 float(inputs["z"].text()),
301 ]
302 )
303 return ref, True
304 except ValueError:
305 return np.zeros(3), False
306
307 return np.zeros(3), False
308
309 def perform_calibration(self):
310 """Perform calibration"""
311 try:
312 params = self.calibrator.calibrate()
313 self.status_label.setText(
314 f"Calibration complete! Quality: {params.calibration_quality:.3f}"
315 )
316 except Exception as e:
317 self.status_label.setText(f"Calibration failed: {e}")
318
319 def save_calibration(self):
320 """Save calibration to file"""
321 filename, _ = QtWidgets.QFileDialog.getSaveFileName(
322 self, "Save Calibration", "", "JSON Files (*.json)"
323 )
324 if filename:
325 self.calibrator.save_calibration(filename)
326 self.status_label.setText(f"Saved to {filename}")
327
328 def closeEvent(self, event):
329 """Handle window close"""
330 self.receiver.stop()
331 self.receiver.wait()
332 event.accept()
333
334
335def main():
336 import argparse
337
338 parser = argparse.ArgumentParser(description="IMU Calibration GUI")
339 parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="UDP port")
340 parser.add_argument(
341 "--sensor-type",
342 choices=["accel", "gyro", "mag"],
343 default="accel",
344 help="Sensor type to calibrate",
345 )
346
347 args = parser.parse_args()
348
349 app = QtWidgets.QApplication(sys.argv)
350 window = IMUCalibrationGUI(sensor_type=args.sensor_type, port=args.port)
351 window.show()
352 sys.exit(app.exec())
353
354
355if __name__ == "__main__":
356 main()
Optional[IMUReading] parse_packet(self, bytes data)
Definition run.py:1
static void emit(sensors_core_t *c, uint8_t type, const void *data, uint8_t len)