harden sensor acquisition after audit
This commit is contained in:
+111
-11
@@ -2,6 +2,7 @@
|
||||
"""Capture Trikke sensor CSV output until interrupted with Ctrl-C."""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import signal
|
||||
import sys
|
||||
from datetime import datetime
|
||||
@@ -9,17 +10,61 @@ from pathlib import Path
|
||||
|
||||
import serial
|
||||
|
||||
EXPECTED_COLUMNS = [
|
||||
"sequence",
|
||||
"poll_timestamp_us",
|
||||
"accel_x_raw",
|
||||
"accel_y_raw",
|
||||
"accel_z_raw",
|
||||
"gyro_x_raw",
|
||||
"gyro_y_raw",
|
||||
"gyro_z_raw",
|
||||
"accel_native_x_raw",
|
||||
"accel_native_y_raw",
|
||||
"accel_native_z_raw",
|
||||
"gyro_native_x_raw",
|
||||
"gyro_native_y_raw",
|
||||
"gyro_native_z_raw",
|
||||
"accel_int_source",
|
||||
"gyro_status",
|
||||
"loop_overrun_count",
|
||||
]
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--port", default="/dev/cu.usbmodem1134101")
|
||||
parser.add_argument("--baud", type=int, default=115200)
|
||||
parser.add_argument("--port", help="serial port; auto-detected when omitted")
|
||||
parser.add_argument(
|
||||
"--baud",
|
||||
type=int,
|
||||
default=115200,
|
||||
help="serial API baud rate (ignored by USB Serial/JTAG)",
|
||||
)
|
||||
parser.add_argument("--output", type=Path)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def resolve_port(requested_port: str | None) -> str:
|
||||
if requested_port:
|
||||
return requested_port
|
||||
|
||||
candidates = sorted(glob.glob("/dev/cu.usbmodem*"))
|
||||
if not candidates:
|
||||
raise RuntimeError("no /dev/cu.usbmodem* serial device found")
|
||||
if len(candidates) > 1:
|
||||
joined = ", ".join(candidates)
|
||||
raise RuntimeError(f"multiple serial devices found ({joined}); specify --port")
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
try:
|
||||
port = resolve_port(args.port)
|
||||
except RuntimeError as exc:
|
||||
print(f"Port error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
output = args.output or Path("captures") / datetime.now().strftime(
|
||||
"motion_%Y%m%d_%H%M%S.csv"
|
||||
)
|
||||
@@ -35,30 +80,85 @@ def main() -> int:
|
||||
signal.signal(signal.SIGTERM, request_stop)
|
||||
|
||||
sample_count = 0
|
||||
print(f"Recording {args.port} to {output}; press Ctrl-C to stop", flush=True)
|
||||
missing_sequence_count = 0
|
||||
sequence_reset_count = 0
|
||||
timing_anomaly_count = 0
|
||||
ignored_line_count = 0
|
||||
rejected_record_count = 0
|
||||
previous_sequence = None
|
||||
previous_timestamp_us = None
|
||||
print(f"Recording {port} to {output}; press Ctrl-C to stop", flush=True)
|
||||
|
||||
try:
|
||||
with serial.Serial(args.port, args.baud, timeout=0.25) as sensor, output.open(
|
||||
with serial.Serial(port, args.baud, timeout=0.25) as sensor, output.open(
|
||||
"w", encoding="utf-8", newline=""
|
||||
) as capture:
|
||||
capture.write(",".join(EXPECTED_COLUMNS) + "\n")
|
||||
|
||||
while not stop_requested:
|
||||
raw_line = sensor.readline()
|
||||
if not raw_line:
|
||||
continue
|
||||
|
||||
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n")
|
||||
capture.write(line + "\n")
|
||||
fields = line.split(",")
|
||||
|
||||
if line and line[0].isdigit() and line.count(",") == 13:
|
||||
sample_count += 1
|
||||
if sample_count % 500 == 0:
|
||||
capture.flush()
|
||||
print(f" {sample_count} samples captured", flush=True)
|
||||
if line.startswith("sequence,"):
|
||||
if fields != EXPECTED_COLUMNS:
|
||||
print("Firmware CSV header does not match capture schema", file=sys.stderr)
|
||||
return 3
|
||||
continue
|
||||
if not line or not line[0].isdigit():
|
||||
ignored_line_count += 1
|
||||
continue
|
||||
if len(fields) != len(EXPECTED_COLUMNS):
|
||||
rejected_record_count += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
values = [int(field) for field in fields]
|
||||
except ValueError:
|
||||
rejected_record_count += 1
|
||||
continue
|
||||
|
||||
sequence = values[0]
|
||||
timestamp_us = values[1]
|
||||
if previous_sequence is not None:
|
||||
expected_sequence = (previous_sequence + 1) & 0xFFFFFFFF
|
||||
if sequence != expected_sequence:
|
||||
if sequence > expected_sequence:
|
||||
missing_sequence_count += sequence - expected_sequence
|
||||
else:
|
||||
sequence_reset_count += 1
|
||||
print(
|
||||
f" sequence discontinuity: expected {expected_sequence}, got {sequence}",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
if (
|
||||
previous_timestamp_us is not None
|
||||
and timestamp_us - previous_timestamp_us != 10_000
|
||||
):
|
||||
timing_anomaly_count += 1
|
||||
|
||||
capture.write(line + "\n")
|
||||
previous_sequence = sequence
|
||||
previous_timestamp_us = timestamp_us
|
||||
sample_count += 1
|
||||
if sample_count % 500 == 0:
|
||||
capture.flush()
|
||||
print(f" {sample_count} samples captured", flush=True)
|
||||
except serial.SerialException as exc:
|
||||
print(f"Serial error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Stopped after {sample_count} samples; saved {output}", flush=True)
|
||||
print(
|
||||
f"Stopped after {sample_count} samples; missing={missing_sequence_count}, "
|
||||
f"resets={sequence_reset_count}, timing_anomalies={timing_anomaly_count}, "
|
||||
f"rejected_records={rejected_record_count}, ignored_lines={ignored_line_count}; "
|
||||
f"saved {output}",
|
||||
flush=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user