Compare commits
2 Commits
14707c62ae
...
16f23f36bb
| Author | SHA1 | Date |
|---|---|---|
|
|
16f23f36bb | |
|
|
4fe9997557 |
|
|
@ -53,36 +53,38 @@ Shared state is mutex-guarded: latest `MotorTelemetry` (serial), `ControlCommand
|
||||||
|
|
||||||
1. **Startup** — `main()` parses CLI, resolves + loads config, constructs `Application`, which builds the
|
1. **Startup** — `main()` parses CLI, resolves + loads config, constructs `Application`, which builds the
|
||||||
motor/channel/camera (real or mock), the `ImagePipeline`, and the `CaptureScheduler`.
|
motor/channel/camera (real or mock), the `ImagePipeline`, and the `CaptureScheduler`.
|
||||||
2. **Telemetry** — the real motor controller streams `$;...;` lines; `parseTelemetryLine` turns each into a
|
2. **Telemetry** — the real motor controller streams firmware `ST Y:...[ P:...]` lines; `parseTelemetryLine`
|
||||||
`MotorTelemetry` snapshot. The mock synthesizes one.
|
turns each into a per-axis `MotorTelemetry` snapshot (state + encoder counts + flags). The mock synthesizes
|
||||||
|
one. Encoder counts are mapped to/from degrees by `Geometry` (`[Motor]` calibration).
|
||||||
3. **Control input** — `IControlChannel::poll()` returns the latest `ControlCommand` (control code + target
|
3. **Control input** — `IControlChannel::poll()` returns the latest `ControlCommand` (control code + target
|
||||||
heading), clearing its "available" flags so each update is acted on once.
|
heading), clearing its "available" flags so each update is acted on once.
|
||||||
4. **Capture cycle** (`CaptureScheduler::tick`, per 10 ms):
|
4. **Capture cycle** (`CaptureScheduler::tick`, per 10 ms) — a move → settle → trigger machine:
|
||||||
- ControlCode 0: when the interval elapses, send `p` to advance/stop, then trigger the cameras.
|
- ControlCode 0: `MOVE <yaw>,<pitch>` to the next `ScanGrid` waypoint (ping-pong).
|
||||||
- ControlCode 1: send `kd<target_heading>` to drive to the requested heading, then trigger.
|
- ControlCode 1: `MOVE` yaw to `target_HDG` (pitch held), converted to counts via `Geometry`.
|
||||||
|
- Trigger the cameras once **both axes report standstill at the target**, then advance the grid.
|
||||||
5. **Frame handling** — a triggered camera delivers a `Frame` to the callback, which `submit()`s it to the
|
5. **Frame handling** — a triggered camera delivers a `Frame` to the callback, which `submit()`s it to the
|
||||||
`ImagePipeline`. The worker rotates it 90° CCW, encodes JPEG XL (or copies the demo image), writes
|
`ImagePipeline`. The worker rotates it 90° CCW, encodes JPEG XL (or copies the demo image), writes
|
||||||
`<output_dir>/<label>/<unix_ms>.jxl`, and publishes a `CamEvent`.
|
`<output_dir>/<label>/<unix_ms>.jxl`, and publishes a `CamEvent` (yaw + pitch from the encoders).
|
||||||
|
|
||||||
## Capture state machine
|
## Capture state machine
|
||||||
|
|
||||||
```
|
```
|
||||||
interval elapsed AND capture active AND is_moving==1
|
interval elapsed AND capture active AND not already moving
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌──────────────────────┐ ControlCode 0 → "p"
|
┌──────────────────────┐ ControlCode 0 → next ScanGrid (yaw,pitch)
|
||||||
│ stop / point gimbal │ ControlCode 1 → "kd<target_heading>"
|
│ MOVE <yaw>,<pitch> │ ControlCode 1 → target_HDG (pitch held)
|
||||||
└──────────┬───────────┘ arm trigger_after_stopping, reset timer
|
└──────────┬───────────┘ deg→counts via Geometry; set moving, reset timer
|
||||||
│ (>100 ms later, is_moving==1)
|
│ (both axes standstill AND |xenc − target| ≤ tol)
|
||||||
▼
|
▼
|
||||||
┌──────────────────────┐
|
┌──────────────────────┐
|
||||||
│ camera.trigger() │ on success: disarm
|
│ camera.trigger() │ on success: clear moving, advance grid (ControlCode 0)
|
||||||
└──────────────────────┘
|
└──────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
The trigger predicate (`is_moving == 1`) is preserved from the original; see
|
Triggering only at the settled target replaces the original's trigger-while-moving behaviour (see
|
||||||
[known-issues.md](known-issues.md) for the open question about its semantics. The scheduler is unit-tested with
|
[known-issues.md](known-issues.md) #7). The scheduler is unit-tested with mock doubles and an injected clock
|
||||||
mock doubles and an injected clock ([tests/test_scheduler.cpp](../tests/test_scheduler.cpp)).
|
([tests/test_scheduler.cpp](../tests/test_scheduler.cpp)).
|
||||||
|
|
||||||
## Why this shape
|
## Why this shape
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,19 +28,19 @@ doctest unit-test suite (`ctest`).
|
||||||
| # | Issue | Status |
|
| # | Issue | Status |
|
||||||
|---|-------|--------|
|
|---|-------|--------|
|
||||||
| 13 | `[Motor]` degrees↔counts calibration | The `config.example.ini` values are **placeholders**. Calibrate `*_counts_per_deg` / `*_zero_count` against real `xenc` readings after homing on the rig. |
|
| 13 | `[Motor]` degrees↔counts calibration | The `config.example.ini` values are **placeholders**. Calibrate `*_counts_per_deg` / `*_zero_count` against real `xenc` readings after homing on the rig. |
|
||||||
| 14 | Settle tolerance / timing | `kSettleTolCounts` (600) and the per-interval timing in [CaptureScheduler.cpp](../src/core/CaptureScheduler.cpp) are untested against hardware; tune against observed `ST` behaviour. |
|
| 14 | Capture sweep untested on hardware | Homing was verified live (see below), but the `MOVE → settle → trigger` sweep was **not** run with `--start`. `kSettleTolCounts` (600) and the per-interval timing in [CaptureScheduler.cpp](../src/core/CaptureScheduler.cpp) still need tuning against observed `ST` behaviour, alongside #13. |
|
||||||
|
|
||||||
## Verification caveats
|
## Verification caveats
|
||||||
|
|
||||||
- **Real MQTT wrapper** now compiles and links: `cmake -B build -DWITH_MQTT=ON` fetches and builds Paho
|
- **Full build verified on the LattePanda**: a `WITH_VIMBA=ON WITH_MQTT=ON` build compiles and links on the
|
||||||
(C++ wrapper plus its bundled C library) from official Eclipse upstream and links `MqttControlChannel`
|
device (real Vimba X SDK + Paho fetched). The MQTT wrapper also builds on the dev box (GCC 16 / CMake 4 — see
|
||||||
into the binary. Verified on GCC 16 / CMake 4 (see `cmake/Paho.cmake` for the toolchain-compat shims).
|
`cmake/Paho.cmake` for the toolchain-compat shims).
|
||||||
- **Real Serial/Vimba wrappers** were verified by code review, not compilation (no Vimba X SDK on the
|
- **Serial protocol verified live against real firmware** (LattePanda, this session): `ENABLE`/`HOME`/`SPEED`
|
||||||
development machine). They are faithful adaptations of the original code. First Vimba compile happens on
|
reach the firmware, the `ST` telemetry parses with zero unparsed lines across a full session, and a live
|
||||||
a machine with the SDK via `WITH_VIMBA=ON`.
|
`--init` drove a clean re-home of both axes to `READY`. **Still unverified on hardware:** the capture sweep
|
||||||
- **Makefile parity** was never re-checked because the original Makefile build also can't run without the SDKs.
|
(#14) and real-camera (Vimba) frame capture — earlier tests used `--mock-camera`.
|
||||||
The Makefile has been removed in favour of CMake; if you need to confirm byte-for-byte behaviour, do a full
|
- **Makefile parity** is moot: the Makefile was removed in favour of CMake, and the full
|
||||||
`WITH_VIMBA=ON WITH_MQTT=ON` build on a tower PC.
|
`WITH_VIMBA=ON WITH_MQTT=ON` CMake build now runs on the device.
|
||||||
- **Demo mode** copies `bin/x64/Release/test_smoke.jxl`, resolved relative to the working directory. Run from a
|
- **Demo mode** copies `bin/x64/Release/test_smoke.jxl`, resolved relative to the working directory. Run from a
|
||||||
directory where that path exists, or extend `ImagePipeline::Params::demo_image`.
|
directory where that path exists, or extend `ImagePipeline::Params::demo_image`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ Per-file reference for the refactored tree, plus the shared data structures.
|
||||||
|
|
||||||
| File | Contents |
|
| File | Contents |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| [include/fgc/Config.h](../include/fgc/Config.h), [src/core/Config.cpp](../src/core/Config.cpp) | Typed `AppConfig` (General/Network/Serial/Camera/Paths/Features) + `ConfigLoader` (INI parse, env overrides, validation) |
|
| [include/fgc/Config.h](../include/fgc/Config.h), [src/core/Config.cpp](../src/core/Config.cpp) | Typed `AppConfig` (General/Network/Serial/Camera/Paths/Features/Logging/Motor/Scan) + `ConfigLoader` (INI parse, env overrides, validation) |
|
||||||
| [include/fgc/Paths.h](../include/fgc/Paths.h), [src/core/Paths.cpp](../src/core/Paths.cpp) | `~`/`$ENV` expansion, executable dir, config search order, default output dir |
|
| [include/fgc/Paths.h](../include/fgc/Paths.h), [src/core/Paths.cpp](../src/core/Paths.cpp) | `~`/`$ENV` expansion, executable dir, config search order, default output dir |
|
||||||
| [include/fgc/Logger.h](../include/fgc/Logger.h), [src/core/Logger.cpp](../src/core/Logger.cpp) | Leveled, thread-safe logger; `LOG_TRACE..LOG_ERROR` macros |
|
| [include/fgc/Logger.h](../include/fgc/Logger.h), [src/core/Logger.cpp](../src/core/Logger.cpp) | Leveled, thread-safe logger + per-category wire trace; `LOG_TRACE..LOG_ERROR`, `LOG_TRACE_CAT` |
|
||||||
| [include/fgc/TelemetryParser.h](../include/fgc/TelemetryParser.h), [src/core/TelemetryParser.cpp](../src/core/TelemetryParser.cpp) | `parseTelemetryLine` → `std::optional<MotorTelemetry>` |
|
| [include/fgc/Geometry.h](../include/fgc/Geometry.h), [src/core/Geometry.cpp](../src/core/Geometry.cpp) | Per-axis degrees↔encoder-counts affine map (`[Motor]` calibration) |
|
||||||
|
| [include/fgc/ScanGrid.h](../include/fgc/ScanGrid.h), [src/core/ScanGrid.cpp](../src/core/ScanGrid.cpp) | Capture waypoints (CSV or generated) + ping-pong cursor (`[Scan]`) |
|
||||||
|
| [include/fgc/TelemetryParser.h](../include/fgc/TelemetryParser.h), [src/core/TelemetryParser.cpp](../src/core/TelemetryParser.cpp) | `parseTelemetryLine` (firmware `ST` line) → `std::optional<MotorTelemetry>` |
|
||||||
| [include/fgc/CommandParser.h](../include/fgc/CommandParser.h), [src/core/CommandParser.cpp](../src/core/CommandParser.cpp) | `parseCommand` whitespace tokenizer → `Command` |
|
| [include/fgc/CommandParser.h](../include/fgc/CommandParser.h), [src/core/CommandParser.cpp](../src/core/CommandParser.cpp) | `parseCommand` whitespace tokenizer → `Command` |
|
||||||
| [include/fgc/CaptureScheduler.h](../include/fgc/CaptureScheduler.h), [src/core/CaptureScheduler.cpp](../src/core/CaptureScheduler.cpp) | Capture state machine over the interfaces; injectable clock |
|
| [include/fgc/CaptureScheduler.h](../include/fgc/CaptureScheduler.h), [src/core/CaptureScheduler.cpp](../src/core/CaptureScheduler.cpp) | Capture state machine over the interfaces; injectable clock |
|
||||||
| [include/fgc/Application.h](../include/fgc/Application.h), [src/core/Application.cpp](../src/core/Application.cpp) | Factory (real vs mock), wiring, control loop, console commands |
|
| [include/fgc/Application.h](../include/fgc/Application.h), [src/core/Application.cpp](../src/core/Application.cpp) | Factory (real vs mock), wiring, control loop, console commands |
|
||||||
|
|
@ -51,17 +53,19 @@ Per-file reference for the refactored tree, plus the shared data structures.
|
||||||
|
|
||||||
## Data structures
|
## Data structures
|
||||||
|
|
||||||
### `MotorTelemetry` ([IMotorController.h](../include/fgc/IMotorController.h))
|
### `MotorTelemetry` / `AxisTelemetry` ([IMotorController.h](../include/fgc/IMotorController.h))
|
||||||
`encoder`, `encoder_err`, `sgt_val`, `sgt_stat`, `is_moving`, `control_status`, `heading` (float),
|
Per-axis `yaw` and `pitch` segments (`pitch_present` flag), each an `AxisTelemetry`: `state`
|
||||||
`deviation_warn`, `humidity`, `temperature`, `fan_pwm`. Parsed from the `$;...;` line (humidity is field 9,
|
(`AxisState` B/R/H/A/E), `xactual`, `xenc` (encoder counts), `drv_status`, `sg`/`cs`/`pwm`, and flags
|
||||||
temperature field 10 — see [known-issues.md](known-issues.md)).
|
`standstill`/`stall`/`overtemp`/`endstop_l`/`endstop_r`, with `moving()`/`ready()` helpers. Parsed from the
|
||||||
|
firmware `ST Y:...[ P:...]` line; degrees are derived via `Geometry`.
|
||||||
|
|
||||||
### `ControlCommand` ([IControlChannel.h](../include/fgc/IControlChannel.h))
|
### `ControlCommand` ([IControlChannel.h](../include/fgc/IControlChannel.h))
|
||||||
`control_code` (0 = auto sweep, 1 = directed) + `target_heading`, each with an `*_available` flag.
|
`control_code` (0 = scan-grid sweep, 1 = directed to `target_HDG`) + `target_heading`, each with an
|
||||||
|
`*_available` flag.
|
||||||
|
|
||||||
### `CamEvent` ([IControlChannel.h](../include/fgc/IControlChannel.h))
|
### `CamEvent` ([IControlChannel.h](../include/fgc/IControlChannel.h))
|
||||||
`tower`, `camera` (RGB/ACR/NIR), `heading_decideg` (heading×10), `timestamp_ms`. Serialized to the CamEvent
|
`tower`, `camera` (RGB/ACR/NIR), `heading_decideg` (yaw×10), `pitch_decideg` (pitch×10), `timestamp_ms`.
|
||||||
JSON payload (see [mqtt-api.md](mqtt-api.md)).
|
Serialized to the CamEvent JSON payload (see [mqtt-api.md](mqtt-api.md)).
|
||||||
|
|
||||||
### `Frame` ([ICameraSource.h](../include/fgc/ICameraSource.h))
|
### `Frame` ([ICameraSource.h](../include/fgc/ICameraSource.h))
|
||||||
Owned pixel buffer + `width`, `height`, `channels` (1 or 3), `timestamp_ms`, `cam_id`.
|
Owned pixel buffer + `width`, `height`, `channels` (1 or 3), `timestamp_ms`, `cam_id`.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue