支持通过 UART 手动逐台添加相机再启动输出

保留已连接相机,添加失败不占用编号,并要求输出停止后才能新增。通过 23 项回归测试、真实 SDK 的 1–4 路逐台添加验证、Ruff 和文档检查。
This commit is contained in:
ykcheungab
2026-10-14 15:49:58 +08:00
parent 07339c3374
commit 133a98e418
5 changed files with 422 additions and 330 deletions
+109 -100
View File
@@ -1,145 +1,154 @@
# UART finger service
Start the service with an explicit list of 1-4 camera sources. It opens and
warms up **every source first**, then opens the data UART and starts publishing.
If any source fails to prepare, all opened sources are closed and the process
exits with status 1. No partial camera set is published.
Start the service first, then manually add 1-4 camera sources through UART.
Each successful `add` opens and warms up one source. Send `start` when the set
is ready. There is no camera discovery, hot-plug monitoring or automatic connection.
## Connect the cameras and start once
## Start the service and add cameras
Plug the USB cameras into the Q6A. In its TTL login shell, identify their actual
capture device paths before launching; one USB camera can expose more than one
`/dev/video*` node, so do not assume every node represents a different camera.
For example, `v4l2-ctl --list-devices` shows device groups. Select one usable
capture node per camera. Use the board environment prepared by `setup/sdk_setup.sh`;
it needs Python 3.10+, the `orisys` SDK and `pyserial`.
Run from the board's `dragon-q6a` directory. These paths are examples, not detected
hardware identities:
In the Q6A TTL login shell, activate the board environment prepared by
`setup/sdk_setup.sh`. It needs Python 3.10+, the `orisys` SDK and `pyserial`.
From the board's `dragon-q6a` directory:
```bash
# One camera.
python serial/example_uart.py --port /dev/ttyHS1 --sources /dev/video0
# Two cameras.
python serial/example_uart.py --port /dev/ttyHS1 --sources /dev/video0 /dev/video2
# Three cameras.
python serial/example_uart.py --port /dev/ttyHS1 --sources /dev/video0 /dev/video2 /dev/video4
# Four cameras.
python serial/example_uart.py --port /dev/ttyHS1 --sources /dev/video0 /dev/video2 /dev/video4 /dev/video6
python serial/example_uart.py --port /dev/ttyHS1 --config finger
```
The first source is sensor 1, the second sensor 2, and so on. The number of listed
sources determines the sensor count. Camera indices such as `--sources 0 2` are
also accepted. Sources must be distinct. A shared `--config finger` is the
default; repeat `--config` once per source to use different configurations.
The login console prints each sensor's source/config mapping during preparation.
There is no UART connection command: physical connection and source selection
happen before startup, and software connection happens automatically.
The TTL login console and data UART must use **different ports**. The data
UART defaults to `/dev/ttyHS1`, 115200 baud, 8N1 without flow control. It must
not be occupied by getty or another serial program. This script does not enable
UART pins or configure the board's hardware. Logs stay in the login console;
protocol records go to the data UART. `--show` is optional desktop debugging.
For a passive receiver, append `--read-only` to the same startup command. Startup
and output are identical, but incoming UART commands are discarded. Exit using
Ctrl-C in the login shell. The receiver should filter `force,` records because
startup and failures also produce status records.
## One protocol for every sensor count
After all sources are ready, the data UART reports `OK ready N`, then immediately
streams each complete processing round in source order. Every measurement has
five CSV fields, including a sensor ID even when only one camera is configured:
The service opens only the data UART and replies `OK ready 0`. No camera is
opened and no measurement is sent until the host adds a source and starts output.
Send these lines through the data UART, waiting for each reply:
```text
OK ready 2
add /dev/video0
OK added 1
add /dev/video2
OK added 2
start
OK start
force,1,1.250000,-2.500000,3.750000
force,2,0.500000,0.100000,-0.200000
```
The fields are `force,sensor_id,FNORMAL,FSHEARX,FSHEARY`. Axes follow the SDK;
units depend on calibration and must not be assumed to be Newtons. CSV carries
no timestamp or units. Frames are processed sequentially, not synchronized
across cameras. All inputs for a round must succeed before that round is
published. A transport failure can still interrupt transmission between lines.
Use actual capture nodes; the paths above are examples. One USB camera can expose
multiple `/dev/video*` nodes, so do not assume every node represents a different
camera. `v4l2-ctl --list-devices` shows device groups. Select one usable capture
node per camera. Camera indices such as `add 0` are also accepted. Sources must
be distinct; `0` and `/dev/video0` are treated as the same camera.
Control mode accepts only four commands, each acting on the entire camera set:
The first successful addition is sensor 1, the next sensor 2, up to sensor 4.
No sensor count is declared in advance. A failed addition replies `ERR add ...`,
closes its newly opened runtime if one exists, and leaves existing cameras and
IDs unchanged. It does not consume an ID. The service remains available for retry.
| Command | Result |
The TTL login console and data UART must use **different ports**. Data defaults
to `/dev/ttyHS1`, 115200 baud, 8N1 without flow control. It must not be occupied
by getty or another serial program. The script does not configure UART pins.
Logs stay in the login console; protocol records go to the data UART.
`--show` is optional local desktop debugging.
## Commands and output
| Command | Behavior |
|---|---|
| `stop` | Pause continuous output and reply `OK stop`; keep cameras open. |
| `start` | Resume continuous output and reply `OK start`. |
| `read` | Process and publish one complete round; preserve streaming state on success. |
| `add SOURCE [CONFIG]` | Open and prepare one source while output is stopped; reply `OK added ID`. Optional config overrides the startup `--config` for this sensor only. |
| `start` | Begin continuous output for all added sensors; requires at least one sensor. Reply `OK start`. |
| `stop` | Pause continuous output for the whole set; keep all cameras open. Reply `OK stop`. |
| `read` | Publish one complete processing round; requires a sensor and preserves streaming state on success. |
| `quit` | Reply `OK quit`, close all cameras and UART, then exit. |
Commands take no arguments. They are case-insensitive UTF-8 lines terminated by
LF, CR or CRLF. Partial lines survive read timeouts; commands exceeding 1024
bytes are discarded through their terminator. Unknown commands or arguments
produce `ERR`. The former protocol and source-selection options have been
removed; receivers must use this single CSV format and explicit `--sources`.
To extend the camera set after streaming has started, pause before adding:
A capture or processing failure produces `ERR read sensor ID: ...` and pauses
output for the whole set. Measurements are never replaced with zeros. Sources
remain open; `start` retries processing, or restart the service to reopen them.
For recorded files, `--no-loop-video` reports `OK eof sensor ID: video ended`
and pauses the whole set at EOF. Otherwise, one rewind and tracker reset is
attempted per read. Ctrl-C always ends the process from the login console.
```text
stop
OK stop
add /dev/video4
OK added 3
start
OK start
```
`add` while streaming replies `ERR add stop streaming first` and opens no source.
A fifth source or duplicate source is rejected. Removing/replacing cameras requires
quitting and starting a fresh service. `start`, `stop`, `read` and `quit` take no
arguments and always act on the full set. The older commands and automatic
startup options are removed.
Commands are case-insensitive UTF-8 lines terminated by LF, CR or CRLF. Partial
lines survive read timeouts. Lines exceeding 1024 bytes are discarded through
their terminator. Quote source/config paths containing spaces:
```text
add "/path/test clip.mp4" "/path/my config.json"
```
Every measurement has five CSV fields, even with only one sensor:
`force,sensor_id,FNORMAL,FSHEARX,FSHEARY`. Axes follow the SDK; units depend on
calibration and must not be assumed to be Newtons. CSV carries no units or
timestamp. The receiver can remain passive after sending setup commands and
`start`; it needs no further requests to receive continuous data. Filter `force,`
lines to distinguish measurements from replies and errors.
Frames are processed sequentially, not synchronized across cameras. All sources
must succeed before a processing round is published. A capture/processing failure
produces `ERR read sensor ID: ...` and stops continuous output for the full set,
without fabricating zero values or transmitting a partial processing round.
A transport failure can still interrupt transmission between lines. `start`
retries processing; restart the service to reopen an unavailable camera.
For recorded files, `--no-loop-video` replies `OK eof sensor ID: video ended` and
pauses the whole set at EOF. Otherwise one rewind and tracker reset is attempted
per read. Ctrl-C in the login shell ends the service and releases resources.
## Callable acquisition API
`serial/finger_api.py` has no UART dependency and opens no resources on import.
Put its directory on the caller's module search path. Use the same connection-first
lifecycle as the service:
Place its directory on the caller's module search path. Adding sources is explicit:
```python
from finger_api import FingerSensorBank
bank = FingerSensorBank([0, 2], configs=["finger"])
bank = FingerSensorBank(config="finger")
try:
bank.connect_all()
first_id = bank.add(0)
second_id = bank.add(2, config="finger")
for reading in bank.read_all():
print(reading.sensor_id, reading.normal, reading.shear_x, reading.shear_y, reading.units)
finally:
bank.close()
```
One thread owns the bank. `connect_all()` rolls back every opened runtime on
failure. `read_all()` returns a tuple only when every source succeeds; it raises
`EOFError` for file EOF and `RuntimeError` for capture/processing failure. Each
reading contains the unrolled image, three scalar forces and SDK units from its
own runtime. Older SDKs without a units property report `unknown`. Reconnection
requires closing the bank before calling `connect_all()` again.
One thread owns the bank. `add()` returns a 1-based ID only after successful
preparation; it raises on failure and preserves previously added runtimes.
`read_all()` requires at least one source and returns a tuple only when all
sources succeed. File EOF raises `EOFError`; capture/processing failure raises
`RuntimeError`. Each reading contains its own unrolled image, three scalar forces
and SDK units; older SDKs without a units property report `unknown`. `close()`
attempts every runtime and clears the camera set. The API caller must pause its
own processing loop before adding; the UART service enforces this for commands.
## Backend evidence and remaining limits
The SDK's `FingerRuntime.open()` constructs a separate capture object,
`FrameUnwarper`, `TactileProcessor` and `Sensor` for each source. Live sources
have separate SDK capture workers. The service holds all runtimes open together
and reads them sequentially. SDK implementation is unchanged.
The SDK's `FingerRuntime.open()` creates separate capture, unwarping and sensor
state for each source. SDK code is unchanged. Verification on 2026-10-14 used
local SDK revision `3953747f1726b197a3f327a38c65d9878e333e12`: 1, 2, 3 and 4
recorded sources were added incrementally, with complete readings after each
addition and three final rounds for each count. Existing runtime identities were
preserved; capture handles and sensor objects were distinct, and optical-flow
arrays did not share memory. Resources were released after each case.
Verification on 2026-10-14 used local SDK revision
`3953747f1726b197a3f327a38c65d9878e333e12`: real SDK instances processed 1, 2, 3
and 4 distinct recorded inputs, three complete rounds for each count. Capture
handles and sensor objects were distinct; optical-flow arrays did not share
memory. All capture resources were released after each case. Run the same check
with a recording containing at least 12 frames; put generated clips and caches
in an isolated task scratch directory:
To rerun with a recording containing at least 12 frames, put generated clips and
caches in an isolated task scratch directory:
```bash
python tests/check_backend.py --recording /path/to/recording.avi --scratch .scratch/uart-connect-first/backend-run
python tests/check_backend.py --recording /path/to/recording.avi --scratch .scratch/uart-manual-add/backend-run
python -m unittest discover -s tests -v
```
Unit tests separately verify startup ordering, full rollback, a uniform protocol,
removed commands/options, command framing and failure behavior. These tests do
not prove Q6A USB bandwidth or the shipped compiled ARM `orisys 0.5.0` wheel.
Run the backend check in the board's installed environment to verify that SDK.
Four live cameras, serial wiring, sustained frame rates and command latency
still require hardware validation. A slow capture/processing call delays command
handling. Serial writes time out after a configurable interval (default 1 second).
Unit tests separately verify empty startup, explicit additions, failure isolation,
IDs/limits, stop-before-add, command framing, complete-round output and cleanup.
The shipped compiled ARM `orisys 0.5.0` wheel and Q6A physical camera/UART setup
remain unverified. Run the backend check inside the board's installed environment
to verify that SDK. USB bandwidth, sustained frame rates and command latency
require hardware validation. Camera opening/warmup and synchronous reads can delay
command handling. Serial writes have a configurable timeout, default 1 second.
At 115200 baud/8N1, all data and replies share 11,520 bytes/s.