Compare commits

...

2 Commits

3 changed files with 41 additions and 35 deletions

View File

@ -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

View File

@ -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`.

View File

@ -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`.