Compare commits

..

No commits in common. "16f23f36bbfd84c8821be1353ab6761aa85ef979" and "14707c62ae694c6fb148a0a50b385f4881b7bcec" have entirely different histories.

3 changed files with 35 additions and 41 deletions

View File

@ -53,38 +53,36 @@ Shared state is mutex-guarded: latest `MotorTelemetry` (serial), `ControlCommand
1. **Startup**`main()` parses CLI, resolves + loads config, constructs `Application`, which builds the
motor/channel/camera (real or mock), the `ImagePipeline`, and the `CaptureScheduler`.
2. **Telemetry** — the real motor controller streams firmware `ST Y:...[ P:...]` lines; `parseTelemetryLine`
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).
2. **Telemetry** — the real motor controller streams `$;...;` lines; `parseTelemetryLine` turns each into a
`MotorTelemetry` snapshot. The mock synthesizes one.
3. **Control input**`IControlChannel::poll()` returns the latest `ControlCommand` (control code + target
heading), clearing its "available" flags so each update is acted on once.
4. **Capture cycle** (`CaptureScheduler::tick`, per 10 ms) — a move → settle → trigger machine:
- ControlCode 0: `MOVE <yaw>,<pitch>` to the next `ScanGrid` waypoint (ping-pong).
- 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.
4. **Capture cycle** (`CaptureScheduler::tick`, per 10 ms):
- ControlCode 0: when the interval elapses, send `p` to advance/stop, then trigger the cameras.
- ControlCode 1: send `kd<target_heading>` to drive to the requested heading, then trigger.
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
`<output_dir>/<label>/<unix_ms>.jxl`, and publishes a `CamEvent` (yaw + pitch from the encoders).
`<output_dir>/<label>/<unix_ms>.jxl`, and publishes a `CamEvent`.
## Capture state machine
```
interval elapsed AND capture active AND not already moving
interval elapsed AND capture active AND is_moving==1
┌──────────────────────┐ ControlCode 0 → next ScanGrid (yaw,pitch)
MOVE <yaw>,<pitch> │ ControlCode 1 → target_HDG (pitch held)
└──────────┬───────────┘ deg→counts via Geometry; set moving, reset timer
│ (both axes standstill AND |xenc target| ≤ tol)
┌──────────────────────┐ ControlCode 0 → "p"
stop / point gimbal │ ControlCode 1 → "kd<target_heading>"
└──────────┬───────────┘ arm trigger_after_stopping, reset timer
│ (>100 ms later, is_moving==1)
┌──────────────────────┐
│ camera.trigger() │ on success: clear moving, advance grid (ControlCode 0)
│ camera.trigger() │ on success: disarm
└──────────────────────┘
```
Triggering only at the settled target replaces the original's trigger-while-moving behaviour (see
[known-issues.md](known-issues.md) #7). The scheduler is unit-tested with mock doubles and an injected clock
([tests/test_scheduler.cpp](../tests/test_scheduler.cpp)).
The trigger predicate (`is_moving == 1`) is preserved from the original; see
[known-issues.md](known-issues.md) for the open question about its semantics. The scheduler is unit-tested with
mock doubles and an injected clock ([tests/test_scheduler.cpp](../tests/test_scheduler.cpp)).
## Why this shape

View File

@ -28,19 +28,19 @@ doctest unit-test suite (`ctest`).
| # | 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. |
| 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. |
| 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. |
## Verification caveats
- **Full build verified on the LattePanda**: a `WITH_VIMBA=ON WITH_MQTT=ON` build compiles and links on the
device (real Vimba X SDK + Paho fetched). The MQTT wrapper also builds on the dev box (GCC 16 / CMake 4 — see
`cmake/Paho.cmake` for the toolchain-compat shims).
- **Serial protocol verified live against real firmware** (LattePanda, this session): `ENABLE`/`HOME`/`SPEED`
reach the firmware, the `ST` telemetry parses with zero unparsed lines across a full session, and a live
`--init` drove a clean re-home of both axes to `READY`. **Still unverified on hardware:** the capture sweep
(#14) and real-camera (Vimba) frame capture — earlier tests used `--mock-camera`.
- **Makefile parity** is moot: the Makefile was removed in favour of CMake, and the full
`WITH_VIMBA=ON WITH_MQTT=ON` CMake build now runs on the device.
- **Real MQTT wrapper** now compiles and links: `cmake -B build -DWITH_MQTT=ON` fetches and builds Paho
(C++ wrapper plus its bundled C library) from official Eclipse upstream and links `MqttControlChannel`
into the binary. Verified on GCC 16 / CMake 4 (see `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
development machine). They are faithful adaptations of the original code. First Vimba compile happens on
a machine with the SDK via `WITH_VIMBA=ON`.
- **Makefile parity** was never re-checked because the original Makefile build also can't run without the SDKs.
The Makefile has been removed in favour of CMake; if you need to confirm byte-for-byte behaviour, do a full
`WITH_VIMBA=ON WITH_MQTT=ON` build on a tower PC.
- **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`.

View File

@ -6,12 +6,10 @@ Per-file reference for the refactored tree, plus the shared data structures.
| 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/Logging/Motor/Scan) + `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) + `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/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/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/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/TelemetryParser.h](../include/fgc/TelemetryParser.h), [src/core/TelemetryParser.cpp](../src/core/TelemetryParser.cpp) | `parseTelemetryLine``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/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 |
@ -53,19 +51,17 @@ Per-file reference for the refactored tree, plus the shared data structures.
## Data structures
### `MotorTelemetry` / `AxisTelemetry` ([IMotorController.h](../include/fgc/IMotorController.h))
Per-axis `yaw` and `pitch` segments (`pitch_present` flag), each an `AxisTelemetry`: `state`
(`AxisState` B/R/H/A/E), `xactual`, `xenc` (encoder counts), `drv_status`, `sg`/`cs`/`pwm`, and flags
`standstill`/`stall`/`overtemp`/`endstop_l`/`endstop_r`, with `moving()`/`ready()` helpers. Parsed from the
firmware `ST Y:...[ P:...]` line; degrees are derived via `Geometry`.
### `MotorTelemetry` ([IMotorController.h](../include/fgc/IMotorController.h))
`encoder`, `encoder_err`, `sgt_val`, `sgt_stat`, `is_moving`, `control_status`, `heading` (float),
`deviation_warn`, `humidity`, `temperature`, `fan_pwm`. Parsed from the `$;...;` line (humidity is field 9,
temperature field 10 — see [known-issues.md](known-issues.md)).
### `ControlCommand` ([IControlChannel.h](../include/fgc/IControlChannel.h))
`control_code` (0 = scan-grid sweep, 1 = directed to `target_HDG`) + `target_heading`, each with an
`*_available` flag.
`control_code` (0 = auto sweep, 1 = directed) + `target_heading`, each with an `*_available` flag.
### `CamEvent` ([IControlChannel.h](../include/fgc/IControlChannel.h))
`tower`, `camera` (RGB/ACR/NIR), `heading_decideg` (yaw×10), `pitch_decideg` (pitch×10), `timestamp_ms`.
Serialized to the CamEvent JSON payload (see [mqtt-api.md](mqtt-api.md)).
`tower`, `camera` (RGB/ACR/NIR), `heading_decideg` (heading×10), `timestamp_ms`. Serialized to the CamEvent
JSON payload (see [mqtt-api.md](mqtt-api.md)).
### `Frame` ([ICameraSource.h](../include/fgc/ICameraSource.h))
Owned pixel buffer + `width`, `height`, `channels` (1 or 3), `timestamp_ms`, `cam_id`.