STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
start_calibration_system.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Mission-Critical Calibration System Startup
4Provides mode selection, validation, and health checks before launching
5calibration tools for PT, TC, RTD, and LC sensors.
6
7Usage:
8 # Interactive mode selection
9 python3 start_calibration_system.py
10
11 # Launch specific autonomous calibration tool directly
12 python3 start_calibration_system.py --tool autonomous_pt_calibration --udp-port 5006 --board-ip 192.168.2.101
13 python3 start_calibration_system.py --tool autonomous_tc_calibration --udp-port 5006 --board-ip 192.168.2.103 -i
14 python3 start_calibration_system.py --tool autonomous_rtd_calibration --udp-port 5006 --board-ip 192.168.2.104 -i
15 python3 start_calibration_system.py --tool autonomous_lc_calibration --udp-port 5006 --board-ip 192.168.2.105 -i
16
17 # Launch calibration GUI
18 python3 start_calibration_system.py --tool pt_calibration_gui --mode calibration
19"""
20
21import sys
22import os
23import argparse
24import logging
25import subprocess
26from pathlib import Path
27
28# Setup logging
29logging.basicConfig(
30 level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
31)
32logger = logging.getLogger(__name__)
33
34# ── Config loader (single source of truth) ────────────────────────────────
35try:
36 from config_loader import (
37 load_config,
38 get_board_by_type,
39 get_sensor_port,
40 get_boards,
41 print_config_summary,
42 )
43
44 _cfg_loaded = True
45except ImportError:
46 _cfg_loaded = False
47
48# Try to import robustness module (optional)
49ROBUSTNESS_AVAILABLE = False
50OperationMode = None
51SystemConfig = None
52try:
53 from calibration_robustness import (
54 RobustnessManager,
55 SystemConfig as _SC,
56 OperationMode as _OM,
57 )
58
59 ROBUSTNESS_AVAILABLE = True
60 OperationMode = _OM
61 SystemConfig = _SC
62except ImportError:
63 logger.warning("⚠️ Robustness module not available (non-critical)")
64
65# Calibration directory (where this script lives)
66CALIBRATION_DIR = Path(__file__).parent.resolve()
67
68
69def _board_ip_for(sensor_type: str) -> str:
70 """Get default board IP for a sensor type from config, with hardcoded fallbacks."""
71 fallbacks = {
72 "PT": "192.168.2.101",
73 "LC": "192.168.2.102",
74 "TC": "192.168.2.103",
75 "RTD": "192.168.2.104",
76 }
77 if _cfg_loaded:
78 b = get_board_by_type(sensor_type)
79 if b:
80 return b.get("ip", fallbacks.get(sensor_type, ""))
81 return fallbacks.get(sensor_type, "")
82
83
84def _default_udp_port() -> int:
85 if _cfg_loaded:
86 return get_sensor_port()
87 return 5006
88
89
90# ── Available calibration tools ────────────────────────────────────────────
91TOOL_REGISTRY = {
92 # ── ORCHESTRATOR (recommended) ──
93 "orchestrator": {
94 "script": "calibration_orchestrator.py",
95 "description": "Unified calibration lifecycle — Phase 1 (cal) + Phase 2 (monitor)",
96 "sensor_type": "ALL",
97 "gui": False,
98 },
99 "orchestrator_gui": {
100 "script": "calibration_orchestrator_gui.py",
101 "description": "Calibration orchestrator with PyQt6 GUI",
102 "sensor_type": "ALL",
103 "gui": True,
104 },
105 "orchestrator_monitor": {
106 "script": "calibration_orchestrator.py",
107 "description": "Self-recalibration monitor only (loads existing cal, skips Phase 1)",
108 "sensor_type": "ALL",
109 "gui": False,
110 "extra_args": ["--skip-phase1"],
111 },
112 # ── PT (Pressure Transducer) ──
113 "pt_calibration_gui": {
114 "script": "pt_calibration_gui.py",
115 "description": "Interactive PT calibration GUI (PyQt6)",
116 "sensor_type": "PT",
117 "gui": True,
118 },
119 "robust_pt_calibration_gui": {
120 "script": "robust_pt_calibration_gui.py",
121 "description": "Robust PT calibration GUI with consensus",
122 "sensor_type": "PT",
123 "gui": True,
124 },
125 "autonomous_pt_calibration": {
126 "script": "autonomous_pt_calibration.py",
127 "description": "Autonomous PT calibration via UDP (no GUI)",
128 "sensor_type": "PT",
129 "gui": False,
130 },
131 # ── TC (Thermocouple) ──
132 "autonomous_tc_calibration": {
133 "script": "autonomous_tc_calibration.py",
134 "description": "Autonomous TC calibration via UDP (no GUI)",
135 "sensor_type": "TC",
136 "gui": False,
137 },
138 # ── RTD (Resistance Temperature Detector) ──
139 "autonomous_rtd_calibration": {
140 "script": "autonomous_rtd_calibration.py",
141 "description": "Autonomous RTD calibration via UDP (no GUI)",
142 "sensor_type": "RTD",
143 "gui": False,
144 },
145 # ── LC (Load Cell) ──
146 "autonomous_lc_calibration": {
147 "script": "autonomous_lc_calibration.py",
148 "description": "Autonomous LC calibration via UDP (no GUI)",
149 "sensor_type": "LC",
150 "gui": False,
151 },
152 # ── IMU ──
153 "imu_calibration_gui": {
154 "script": "imu_calibration_gui.py",
155 "description": "IMU calibration GUI",
156 "sensor_type": "IMU",
157 "gui": True,
158 },
159}
160
161
163 """Check system health before startup"""
164 logger.info("🔍 Performing system health checks...")
165
166 checks_passed = 0
167 checks_total = 0
168
169 # Check 1: Python version
170 checks_total += 1
171 if sys.version_info >= (3, 8):
172 logger.info("✅ Python version OK: " + sys.version.split()[0])
173 checks_passed += 1
174 else:
175 logger.error(f"❌ Python version {sys.version.split()[0]} < 3.8")
176
177 # Check 2: Required modules
178 checks_total += 1
179 required_modules = ["numpy"]
180 optional_modules = ["PyQt6", "pyqtgraph", "serial", "scipy"]
181 missing_required = []
182 missing_optional = []
183
184 for module in required_modules:
185 try:
186 __import__(module)
187 except ImportError:
188 missing_required.append(module)
189
190 for module in optional_modules:
191 try:
192 __import__(module)
193 except ImportError:
194 missing_optional.append(module)
195
196 if not missing_required:
197 logger.info("✅ All required modules available")
198 if missing_optional:
199 logger.warning(
200 f"⚠️ Optional modules missing: {', '.join(missing_optional)}"
201 )
202 checks_passed += 1
203 else:
204 logger.error(f"❌ Missing required modules: {', '.join(missing_required)}")
205
206 # Check 3: Write permissions for calibration directory
207 checks_total += 1
208 calib_dir = CALIBRATION_DIR / "calibrations"
209 calib_dir.mkdir(parents=True, exist_ok=True)
210 test_file = calib_dir / "._write_test"
211 try:
212 test_file.write_text("test")
213 test_file.unlink()
214 logger.info("✅ Calibration directory write permissions OK")
215 checks_passed += 1
216 except Exception:
217 logger.error(f"❌ No write permissions in calibration directory: {calib_dir}")
218
219 # Check 4: Network socket availability (for UDP-based calibration)
220 checks_total += 1
221 try:
222 import socket
223
224 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
225 s.close()
226 logger.info("✅ UDP socket creation OK")
227 checks_passed += 1
228 except Exception as e:
229 logger.error(f"❌ UDP socket test failed: {e}")
230
231 success_rate = checks_passed / checks_total if checks_total > 0 else 0
232 logger.info(f"\n{'='*80}")
233 logger.info(
234 f"Health check: {checks_passed}/{checks_total} passed ({success_rate*100:.0f}%)"
235 )
236 logger.info(f"{'='*80}\n")
237
238 return checks_passed >= checks_total - 1 # Allow 1 non-critical failure
239
240
242 """List all available calibration tools and their status."""
243 print("\n" + "=" * 80)
244 print("AVAILABLE CALIBRATION TOOLS")
245 print("=" * 80)
246
247 by_type = {}
248 for name, info in TOOL_REGISTRY.items():
249 by_type.setdefault(info["sensor_type"], []).append((name, info))
250
251 for sensor_type in ["ALL", "PT", "TC", "RTD", "LC", "IMU"]:
252 tools = by_type.get(sensor_type, [])
253 if not tools:
254 continue
255 print(f"\n ── {sensor_type} ──")
256 for name, info in tools:
257 script_path = CALIBRATION_DIR / info["script"]
258 exists = "✅" if script_path.exists() else "❌"
259 kind = "GUI" if info.get("gui") else "CLI"
260 print(f" {exists} {name:<35s} [{kind}] {info['description']}")
261
262 print("\n" + "=" * 80)
263
264
266 """Interactive tool selection."""
268
269 tools_list = list(TOOL_REGISTRY.keys())
270 print("\nEnter tool number or name:")
271 for i, name in enumerate(tools_list, 1):
272 info = TOOL_REGISTRY[name]
273 print(f" {i:>2d}. {name} ({info['sensor_type']})")
274
275 while True:
276 choice = input("\nSelect tool [1-{}]: ".format(len(tools_list))).strip()
277 if not choice:
278 continue
279 # Try as number
280 try:
281 idx = int(choice) - 1
282 if 0 <= idx < len(tools_list):
283 return tools_list[idx]
284 except ValueError:
285 pass
286 # Try as name
287 if choice in TOOL_REGISTRY:
288 return choice
289 print(f" ⚠️ Invalid choice: {choice}")
290
291
292def build_tool_command(tool_name: str, args) -> list:
293 """Build subprocess command for the selected tool."""
294 info = TOOL_REGISTRY[tool_name]
295 script_path = CALIBRATION_DIR / info["script"]
296 cmd = [sys.executable, str(script_path.resolve())]
297
298 # Extra args baked into the registry entry (e.g. --skip-phase1)
299 if info.get("extra_args"):
300 cmd.extend(info["extra_args"])
301
302 # For autonomous / CLI calibration tools, pass UDP arguments
303 if not info.get("gui"):
304 # Orchestrator doesn't take --board-ip (it reads config directly)
305 is_orchestrator = "orchestrator" in info.get("script", "")
306
307 if args.udp_port and not is_orchestrator:
308 cmd.extend(["--port", str(args.udp_port)])
309 if is_orchestrator and args.udp_port:
310 cmd.extend(["--port", str(args.udp_port)])
311
312 if not is_orchestrator:
313 if args.board_ip:
314 cmd.extend(["--board-ip", args.board_ip])
315 else:
316 default_ip = _board_ip_for(info["sensor_type"])
317 if default_ip:
318 cmd.extend(["--board-ip", default_ip])
319
320 if args.ref_all is not None and not is_orchestrator:
321 cmd.extend(["--ref-all", str(args.ref_all)])
322 if args.collect_time and not is_orchestrator:
323 cmd.extend(["--collect-time", str(args.collect_time)])
324 if args.interactive_cal and not is_orchestrator:
325 cmd.append("--interactive")
326
327 # Pass through --sensor / --temp or --sensor / --force pairs
328 if args.sensor and not is_orchestrator:
329 for s in args.sensor:
330 cmd.extend(["--sensor", str(s)])
331 if args.ref_value and not is_orchestrator:
332 flag = "--force" if info["sensor_type"] == "LC" else "--temp"
333 for v in args.ref_value:
334 cmd.extend([flag, str(v)])
335
336 # For orchestrator, pass --sensors if user specified a type
337 if is_orchestrator and info["sensor_type"] != "ALL":
338 cmd.extend(["--sensors", info["sensor_type"]])
339
340 return cmd
341
342
343def main():
344 parser = argparse.ArgumentParser(
345 description="Mission-Critical Calibration System Startup",
346 formatter_class=argparse.RawDescriptionHelpFormatter,
347 epilog="""
348Examples:
349 # Interactive menu
350 python3 start_calibration_system.py
351
352 # Launch autonomous PT calibration interactively
353 python3 start_calibration_system.py --tool autonomous_pt_calibration -i
354
355 # Collect TC calibration at 100°C for 15 seconds
356 python3 start_calibration_system.py --tool autonomous_tc_calibration --ref-all 100.0 --collect-time 15
357
358 # LC zero-cal
359 python3 start_calibration_system.py --tool autonomous_lc_calibration --ref-all 0.0 --collect-time 10
360""",
361 )
362 parser.add_argument(
363 "--tool",
364 choices=list(TOOL_REGISTRY.keys()),
365 help="Calibration tool to launch",
366 )
367 parser.add_argument(
368 "--mode",
369 choices=["test", "calibration", "flight", "safe"],
370 help="Operation mode (for GUI tools)",
371 )
372 parser.add_argument(
373 "--skip-health-check",
374 action="store_true",
375 help="Skip system health checks",
376 )
377 # ── UDP / autonomous calibration arguments ──
378 parser.add_argument(
379 "--udp-port",
380 type=int,
381 default=_default_udp_port(),
382 help=f"UDP port for board data (default: {_default_udp_port()} from config.toml)",
383 )
384 parser.add_argument(
385 "--board-ip",
386 default=None,
387 help="Filter packets to this board IP (e.g. 192.168.2.101)",
388 )
389 parser.add_argument(
390 "--ref-all",
391 type=float,
392 default=None,
393 help="Set reference value for all channels (PSI / °C / lbs)",
394 )
395 parser.add_argument(
396 "--collect-time",
397 type=float,
398 default=5.0,
399 help="Collection duration in seconds (default: 5)",
400 )
401 parser.add_argument(
402 "--sensor",
403 type=int,
404 action="append",
405 help="Sensor channel (repeatable, paired with --ref-value)",
406 )
407 parser.add_argument(
408 "--ref-value",
409 type=float,
410 action="append",
411 help="Reference value per sensor (repeatable, paired with --sensor)",
412 )
413 parser.add_argument(
414 "-i",
415 "--interactive-cal",
416 action="store_true",
417 help="Run autonomous calibration in interactive CLI mode",
418 )
419 parser.add_argument(
420 "--list-tools",
421 action="store_true",
422 help="List all available calibration tools and exit",
423 )
424
425 args = parser.parse_args()
426
427 # ── Header ──
428 print("\n" + "=" * 80)
429 print(
430 """
431 ██████╗ █████╗ ██╗ ██╗██████╗ ██████╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗
432 ██╔════╝ ██╔══██╗██║ ██║██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║
433 ██║ ███╗███████║██║ ██║██████╔╝██████╔╝███████║ ██║ ██║██║ ██║██╔██╗ ██║
434 ██║ ██║██╔══██║██║ ██║██╔══██╗██╔══██╗██╔══██║ ██║ ██║██║ ██║██║╚██╗██║
435 ╚██████╔╝██║ ██║███████╗██║██████╔╝██║ ██║██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║
436 ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
437 """
438 )
439 print(" SENSOR CALIBRATION SYSTEM v5.0")
440 print(" PT · TC · RTD · LC | UDP Pipeline")
441 print("=" * 80 + "\n")
442
443 if _cfg_loaded:
444 print_config_summary()
445
446 if args.list_tools:
448 return 0
449
450 # ── Health check ──
451 if not args.skip_health_check:
452 if not check_system_health():
453 logger.error(
454 "❌ System health checks failed. Use --skip-health-check to override."
455 )
456 return 1
457 else:
458 logger.warning("⚠️ Health checks skipped")
459
460 # ── Tool selection ──
461 tool_name = args.tool
462 if not tool_name:
463 tool_name = select_tool_interactive()
464
465 info = TOOL_REGISTRY[tool_name]
466 script_path = CALIBRATION_DIR / info["script"]
467
468 if not script_path.exists():
469 logger.error(f"❌ Tool script not found: {script_path}")
471 return 1
472
473 # ── Build command ──
474 cmd = build_tool_command(tool_name, args)
475
476 print(f"\n{'─'*80}")
477 print(f"🚀 Launching: {tool_name}")
478 print(f" Script: {script_path}")
479 print(f" Sensor: {info['sensor_type']}")
480 print(f" Type: {'GUI' if info.get('gui') else 'CLI / Autonomous'}")
481 if not info.get("gui"):
482 print(f" UDP: 0.0.0.0:{args.udp_port}")
483 board = args.board_ip or _board_ip_for(info["sensor_type"]) or "any"
484 print(
485 f" Board: {board} (from {'CLI' if args.board_ip else 'config.toml'})"
486 )
487 print(f" Cmd: {' '.join(cmd)}")
488 print(f"{'─'*80}\n")
489
490 try:
491 result = subprocess.run(cmd, cwd=str(CALIBRATION_DIR))
492 return result.returncode
493 except KeyboardInterrupt:
494 logger.info("\n🛑 Calibration system stopped by user")
495 return 0
496 except Exception as e:
497 logger.error(f"❌ Failed to launch calibration tool: {e}")
498 import traceback
499
500 traceback.print_exc()
501 return 1
502
503
504if __name__ == "__main__":
505 sys.exit(main())
list build_tool_command(str tool_name, args)