修复 UART 命令处理并支持 1–4 路传感器

增加只读启动和可调用采集 API,保留单路数据协议。通过 28 项模拟回归测试、Ruff 和文档检查;实机限制见 docs/uart.md。
This commit is contained in:
ykcheungab
2026-10-14 14:17:27 +08:00
parent 14d5b1b086
commit c970272361
5 changed files with 946 additions and 387 deletions
+141
View File
@@ -0,0 +1,141 @@
# UART finger service
`serial/example_uart.py` processes 1–4 finger cameras or recorded videos and
publishes the SDK's normal and shear values over a separate data UART. Python
3.10+, the board's installed `orisys` SDK and `pyserial` are required. Run inside
the board environment prepared by `setup/sdk_setup.sh`; install `pyserial` there
with `python -m pip install pyserial` if needed.
## Launch from the login console
The TTL login console and data UART must use **different device ports**. Start
the script in the login shell; connect the data receiver to the port selected
by `--port`. A shell/getty or another serial program must not own that data port.
The default is `/dev/ttyHS1`, 115200 baud, 8N1, with no flow control. This service
does not configure pin multiplexing or enable the board's UART hardware.
From the board's `dragon-q6a` checkout:
```bash
# Host commands control connection and streaming (default: one camera).
python serial/example_uart.py --port /dev/ttyHS1 --config finger
# Four configured sources; use 1, 2, 3 or 4 entries as required.
python serial/example_uart.py --port /dev/ttyHS1 --sources 0 2 4 6
# A passive receiver needs only to read: connection and streaming start automatically.
python serial/example_uart.py --port /dev/ttyHS1 --sources 0 2 --read-only
# Start streaming immediately, while still accepting UART commands.
python serial/example_uart.py --port /dev/ttyHS1 --sources 0 2 --autostart
```
Use actual camera indices/device paths discovered on the board; `0 2 4 6` is
only an example. Slot IDs are fixed by `--sources` order, not camera index.
Repeat `--config` once per source for different sensor configurations, or supply
one shared configuration. Existing `--camera-id` and `--video` remain available
for a single source. `--sources` replaces those options. `--show` requires a local
GUI and is off by default. Logs go to the login console, replies to the data UART.
In `--read-only` mode, all incoming UART commands are ignored. End the process
with Ctrl-C in the login shell. Startup still emits `OK`/`ERR` records; the receiver
should select `force,` lines. Without `--autostart` or `--read-only`, a passive
reader receives only `OK ready` until a host sends `connect` and `run`.
## Command protocol
Commands are UTF-8, case-insensitive, terminated by LF, CR or CRLF. Incomplete
lines remain buffered across serial timeouts. Lines exceeding 1024 bytes are
discarded through their terminator and produce `ERR command ...` in control mode.
Quote file paths containing spaces. Linux device paths are recommended on Q6A.
| Command | Behavior |
|---|---|
| `search` | List cameras, then `OK search N`; use before connecting. Rejected while streaming. |
| `connect` | Connect slot 1 using its configured source; in single-source mode, prefer the latest search result for a camera. |
| `connect SOURCE` | Legacy form: connect slot 1 to this camera index, device path or file. |
| `connect ID SOURCE` | Connect/replace one slot; stop streaming that slot. Other slots keep their connections. |
| `connect all` | Connect all configured slots; report each success/failure independently. |
| `run [ID\|all]` | Stream the selected slot, or all connected slots when omitted. Requires a connection. |
| `read [ID\|all]` | Publish one newly processed measurement per selected slot; defaults to all configured slots. Does not start streaming. |
| `stop [ID\|all]` | Stop selected streaming, or all when omitted; keep cameras connected. |
| `quit` / `exit` | Exit and release all camera and UART resources. |
`connect 2` means **camera index 2 on slot 1**, preserving the old protocol.
To connect slot 2, send `connect 2 /dev/video2` or use `connect all`.
There are no motor-control commands; these commands control acquisition only.
Single-source measurements retain the original format:
```text
force,1.250000,-2.500000,3.750000
```
When configured with multiple sources, measurements include the slot ID even if
only one source connected successfully:
```text
OK connect 2 /dev/video2 (camera)
OK run 1 2
force,1,1.250000,-2.500000,3.750000
force,2,0.500000,0.100000,-0.200000
ERR run 2 processing failed
OK eof 1
```
Force fields are `FNORMAL`, `FSHEARX`, `FSHEARY` in SDK axes. They may be raw
or calibrated; **do not assume Newtons**. The Python API exposes SDK units per
reading, using `unknown` if an older SDK does not expose them. CSV preserves
the original fields and carries no units, timestamps or cross-sensor synchronization.
Each processing failure or EOF stops only the affected slot. Processing exceptions
produce `ERR`, never fabricated zero force. `--no-loop-video` stops files at EOF;
the default tries one rewind, resets the tracker and retries one frame. Failed
preparation releases the newly opened runtime; replacing a connection releases
the old one before opening the replacement. A failed replacement requires retrying
`connect`. Startup continues with any successfully connected slots and fails if
none connect. Reconnect and run explicitly after a camera failure.
## Callable acquisition API
`serial/finger_api.py` owns acquisition and runtime cleanup; importing it opens
no UART or camera. Place that directory on the caller's module search path:
```python
from finger_api import FingerSensorBank
bank = FingerSensorBank([0, 2], configs=["finger"])
try:
bank.connect(1)
bank.connect(2)
reading = bank.read(1)
if reading is not None:
print(reading.normal, reading.shear_x, reading.shear_y, reading.units)
finally:
bank.close()
```
The caller owns one thread and must close the bank. `read()` returns `None` on
capture failure/EOF and raises on processing failure. It returns an image and
three scalar forces from the same processed frame. One file or camera is read
at a time; the API introduces no workers beyond those owned by the SDK.
## Verification and limits
Hardware-free regression tests cover command fragmentation, CR/LF, legacy output,
1–4 slot identities, selective control, failure isolation, read-only startup and
cleanup. They use in-memory serial and runtime fakes, not physical devices:
```bash
python -m unittest discover -s tests -v
```
Q6A UART wiring, installed ARM SDK compatibility, camera bandwidth, sustained
frame rates and latency remain unverified. The shipped ARM wheel is compiled
`orisys 0.5.0`; local tests do not execute it. SDK capture and processing remain
synchronous from this service's perspective. Commands are checked before each
round; a slow grab, processing call, connection or camera scan can delay them.
Serial writes have a finite timeout (default 1 second), and transport failure
terminates the service with cleanup. At 115200 baud/8N1, the data budget is
11,520 bytes/s, shared by all measurements and replies. Measure actual frame
rates and stop latency on the board before choosing a higher baud rate.