Files
dragon-q6a/serial/example_uart.py
T
ykcheungab a723e96dcc 支持传感器独立校准文件与批量配置启动
保留逐台添加,并通过显式配置一次连接全组;失败时撤销本批新增连接。通过 35 项回归测试、真实 SDK 校准与 1–4 路验证、Ruff 和文档检查。
2026-10-14 16:11:15 +08:00

273 lines
10 KiB
Python

#!/usr/bin/env python3
"""Headless UART finger service for 1-4 sensors; see ../docs/uart.md.
The login console and data UART must be separate ports. Cameras are selected
explicitly by UART commands or a startup setup file. All force records include a sensor ID.
Importing this module opens no devices; acquisition is owned by finger_api.
"""
from __future__ import annotations
import argparse
import logging
import math
import os
import shlex
import sys
from collections import deque
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
from finger_api import FingerSensorBank, parse_source
logger = logging.getLogger(__name__)
class UartFingerController:
"""Single-threaded command service; the caller owns serial and final cleanup."""
def __init__(
self, ser, *, config="finger", loop_video=True,
show=False, verbose=False,
):
self.ser = ser
self.bank = FingerSensorBank(config=config, verbose=verbose)
self.loop_video = loop_video
self.show = show
self.verbose = verbose
self._streaming = False
self._should_quit = False
self._partial = bytearray()
self._lines = deque()
self._discard_line = False
def reply(self, line):
# SDK exceptions can contain newlines; every reply must be one record.
text = " ".join(str(line).splitlines()) + "\n"
data = text.encode("utf-8")
if self.ser.write(data) != len(data):
raise OSError("incomplete UART write")
if self.verbose:
print(f"<< {text.rstrip()}", file=sys.stderr, flush=True)
def close_runtime(self):
self._streaming = False
try:
self.bank.close()
finally:
if self.show:
import cv2
cv2.destroyAllWindows()
def _poll_command_line(self, *, blocking=True):
if self._lines:
return self._lines.popleft()
waiting = self.ser.in_waiting
if not waiting and not blocking:
return None
# Never readline(): a timeout may return only half of a command.
raw = self.ser.read(min(waiting, 4096) if waiting else 1)
for byte in raw:
if byte in (10, 13):
if self._discard_line:
self._discard_line = False
self._partial.clear()
self.reply("ERR command line too long (max 1024 bytes)")
elif self._partial:
self._lines.append(self._partial.decode("utf-8", errors="replace").strip())
self._partial.clear()
elif not self._discard_line:
self._partial.append(byte)
if len(self._partial) > 1024:
self._partial.clear()
self._discard_line = True
return self._lines.popleft() if self._lines else None
def _read_round(self):
try:
readings = self.bank.read_all(loop_video=self.loop_video)
except EOFError as exc:
self._streaming = False
self.reply(f"OK eof {exc}")
return
except Exception as exc:
logger.exception("Acquisition round failed")
self._streaming = False
self.reply(f"ERR read {exc}")
return
for reading in readings:
self.reply(
f"force,{reading.sensor_id},{reading.normal:.6f},"
f"{reading.shear_x:.6f},{reading.shear_y:.6f}"
)
if self.show:
self._show(reading)
if self._should_quit:
return
def _show(self, reading):
import cv2
import orisys
tactile = self.bank.runtimes[reading.sensor_id].tactile
try:
arrows = orisys.util.draw_arrows(
reading.image, tactile.flow, threshold=0.4, grid_spacing=10,
arrow_scale=1.0, valid_mask=tactile.valid_mask,
)
cv2.imshow(f"UV Unrolled {reading.sensor_id}", arrows)
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
self._streaming = False
self._should_quit = True
self.reply("OK quit")
elif key == ord("r"):
tactile.reset()
except Exception:
logger.exception("Sensor %s display failed", reading.sensor_id)
def _dispatch(self, line):
try:
parts = shlex.split(line)
except ValueError as exc:
self.reply(f"ERR command {exc}")
return True
if not parts:
return True
command = parts[0].lower()
if command == "add":
if self._streaming:
self.reply("ERR add stop streaming first")
else:
parser = argparse.ArgumentParser(prog="add", add_help=False, exit_on_error=False)
parser.add_argument("source")
parser.add_argument("config", nargs="?")
parser.add_argument("--calibration")
try:
args = parser.parse_args(parts[1:])
except (argparse.ArgumentError, SystemExit):
self.reply("ERR command expected add SOURCE [CONFIG] [--calibration PATH]")
return True
try:
sensor_id = self.bank.add(
parse_source(args.source), config=args.config, cal_path=args.calibration,
)
except Exception as exc:
logger.exception("Sensor addition failed")
self.reply(f"ERR add {exc}")
else:
self.reply(f"OK added {sensor_id}")
elif command == "load":
if len(parts) != 2:
self.reply("ERR command expected load SETUP.json")
elif self._streaming:
self.reply("ERR load stop streaming first")
else:
try:
sensor_ids = self.bank.add_setup(parts[1])
except Exception as exc:
logger.exception("Setup loading failed")
self.reply(f"ERR load {exc}")
else:
self.reply("OK loaded " + " ".join(map(str, sensor_ids)))
elif command not in {"start", "stop", "read", "quit"}:
self.reply(f"ERR unknown {command}")
elif len(parts) != 1:
self.reply("ERR command start|stop|read|quit take no arguments")
elif command == "start":
if not self.bank.runtimes:
self.reply("ERR start no sensors added")
else:
self._streaming = True
self.reply("OK start")
elif command == "stop":
self._streaming = False
self.reply("OK stop")
elif command == "read":
self._read_round()
elif command == "quit":
self._streaming = False
self._should_quit = True
self.reply("OK quit")
return not self._should_quit
def serve(self, *, start=False):
if start and not self.bank.runtimes:
raise RuntimeError("no sensors added")
self.reply(f"OK ready {len(self.bank.sources)}")
self._streaming = start
while not self._should_quit:
for _ in range(16):
line = self._poll_command_line(blocking=not self._streaming)
if line is None:
break
if not self._dispatch(line):
return
if self._streaming:
self._read_round()
def open_serial(port, baud, timeout, write_timeout=1.0):
try:
import serial
except ImportError as exc:
raise RuntimeError("pyserial is required: python -m pip install pyserial") from exc
return serial.Serial(
port=port, baudrate=baud, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE, timeout=timeout, write_timeout=write_timeout,
exclusive=True,
)
def main(argv=None):
parser = argparse.ArgumentParser(description="Manual UART finger service: add, start, stop, read, quit")
parser.add_argument("--port", default="/dev/ttyHS1")
parser.add_argument("--baud", type=int, default=115200)
parser.add_argument("--timeout", type=float, default=0.1, help="idle read timeout in seconds")
parser.add_argument("--write-timeout", type=float, default=1.0)
parser.add_argument("--config", "-c", default="finger", help="default config for add commands")
parser.add_argument("--setup", help="JSON file listing sources, configs and calibrations to connect")
parser.add_argument("--start", action="store_true", help="start output after loading --setup")
parser.add_argument("--no-loop-video", dest="loop_video", action="store_false")
parser.add_argument("--show", action="store_true", help="local desktop debug only")
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args(argv)
if args.start and not args.setup:
parser.error("--start requires --setup")
if args.baud <= 0:
parser.error("baud must be positive")
if any(not math.isfinite(t) or t <= 0 for t in (args.timeout, args.write_timeout)):
parser.error("timeouts must be finite and positive")
controller = UartFingerController(
None, config=args.config, loop_video=args.loop_video,
show=args.show, verbose=args.verbose,
)
ser = None
result = 0
try:
if args.setup:
controller.bank.add_setup(args.setup)
ser = open_serial(args.port, args.baud, args.timeout, args.write_timeout)
controller.ser = ser
print(f"UART service on {args.port} @ {args.baud}; {len(controller.bank.runtimes)} sensors ready", file=sys.stderr, flush=True)
controller.serve(start=args.start)
except KeyboardInterrupt:
print("Interrupted.", file=sys.stderr)
except Exception:
logger.exception("UART service failed on %s", args.port)
result = 1
finally:
try:
controller.close_runtime()
except Exception:
logger.exception("Runtime cleanup failed")
result = 1
finally:
if ser is not None:
ser.close()
return result
if __name__ == "__main__":
raise SystemExit(main())