支持通过 UART 手动逐台添加相机再启动输出
保留已连接相机,添加失败不占用编号,并要求输出停止后才能新增。通过 23 项回归测试、真实 SDK 的 1–4 路逐台添加验证、Ruff 和文档检查。
This commit is contained in:
+109
-100
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user