Cover config path anchoring with a test; refresh the camera, TUI and I2C docs
This commit is contained in:
parent
a5ad814260
commit
9023f79c26
|
|
@ -60,7 +60,8 @@ Both used 2×2 binning at `RGB8` (2256×2256, 15.3 MB/frame) and a 250 MB/s
|
|||
full-resolution RGB8 frame is 61 MB, which caps out near 4 fps under that limit.
|
||||
|
||||
The full previous definitions are in git history at
|
||||
`config/camera_modes/rgb/{standard,night}.ini`.
|
||||
the hand-written `config/camera_modes/rgb/{standard,night}.ini`, removed in the
|
||||
same change that made the XML the only thing applied.
|
||||
|
||||
`settle_wait_ms` / `frame_timeout_ms` are *not* camera settings and are not in
|
||||
the XML — they live in `config.ini` under `[Camera.rgb.<profile>]`.
|
||||
|
|
|
|||
|
|
@ -34,8 +34,6 @@ Parsed and validated by `ConfigLoader` ([src/core/Config.cpp](../src/core/Config
|
|||
| `Network` | `mqtt_user` / `mqtt_pw` | string | — | MQTT credentials (see secrets below) |
|
||||
| `Serial` | `device` | string | `/dev/ttyACM0` | Motor-controller serial device |
|
||||
| `Serial` | `baud` | int | `115200` | Serial baud rate |
|
||||
| `Camera` | `id_Cam1`..`id_Cam4` | string | — | Camera IDs (GigE IP or USB `DEV_...`); non-empty ones used in order |
|
||||
| `Camera` | `jxl_distance` / `jxl_effort` | | | Defaults inherited by every camera block |
|
||||
| `Camera.<name>` | `id` | string | — | USB `DEV_` id or GigE IP. Required when enabled |
|
||||
| `Camera.<name>` | `driver` | `vimba` | `vimba` | Backend. Only `vimba` is implemented |
|
||||
| `Camera.<name>` | `label` | string | the block name | Output subdirectory and `CamEvent` `cam` field |
|
||||
|
|
@ -45,9 +43,8 @@ Parsed and validated by `ConfigLoader` ([src/core/Config.cpp](../src/core/Config
|
|||
| `Camera.<name>` | `rotation_deg` | `0`/`90`/`180`/`270` | `0` | Clockwise rotation applied to that camera's saved images |
|
||||
| `Camera.<name>.<profile>` | `settle_wait_ms` | int ≥ 0 | `1000` | Hold after standstill before keeping a frame |
|
||||
| `Camera.<name>.<profile>` | `frame_timeout_ms` | int ≥ 0 | `2000` | Abandon the waypoint after this |
|
||||
| `Camera` | `jxl_distance` | double ≥ 0 | `0.8` | JPEG XL distance (`0` = lossless, `~0.8` = near-lossless) |
|
||||
| `Camera` | `jxl_effort` | int 1..9 | `4` | JPEG XL effort (higher = slower/smaller) |
|
||||
| `Camera` | `rotation_deg` | `0`/`90`/`180`/`270` | `0` | Clockwise rotation applied to saved images (`0` = as the sensor delivers them) |
|
||||
| `Camera` | `jxl_distance` / `jxl_effort` | double ≥ 0 / int 1..9 | `0.8` / `4` | Encoder defaults inherited by every camera block |
|
||||
| `Camera.<name>` | `jxl_distance` / `jxl_effort` | double ≥ 0 / int 1..9 | the `[Camera]` value | Per-camera override of the encoder settings |
|
||||
| `Paths` | `output_dir` | string | `$XDG_DATA_HOME/fire_gimbal_control/images` | Image output dir; supports `~`/`$ENV` |
|
||||
| `Features` | `enable_mqtt` | bool | `true` | Use MQTT (vs null channel) |
|
||||
| `Features` | `enable_camera` | bool | `true` | (reserved) |
|
||||
|
|
@ -117,7 +114,7 @@ back. Set
|
|||
`[Scan] grid_file` to an editable CSV ([config/scan.csv](../config/scan.csv)) to define exact
|
||||
coordinates, or leave it blank to generate `yaw_intervals × pitch_levels` points.
|
||||
|
||||
Camera index → output subfolder defaults to `RGB`, `ACR`, `NIR` (`CameraConfig::labels`).
|
||||
Each camera's output subfolder is its `[Camera.<name>] label` (defaulting to the block name); nothing is derived from the order of the blocks.
|
||||
|
||||
### `[IMU]` — Xsens MTi orientation sensor
|
||||
|
||||
|
|
@ -193,9 +190,25 @@ it.
|
|||
|
||||
Enable with `[Features] enable_env = true` and point `[Env] i2c_device` at the LattePanda's **own
|
||||
native I2C bus** — this is a separate physical link from the motor Leonardo's serial connection, not
|
||||
the Arduino's D2 pin (the original DHT11 plan). Bus number varies by board; confirm with
|
||||
`i2cdetect -y N` (the SHT41 answers at `0x44`/`68`). `i2c_addr` accepts either form — `0x44` hex or
|
||||
`68` decimal. The backend (`Sht41EnvSensor`) polls at `period_ms`, discarding any sample that fails
|
||||
the Arduino's D2 pin (the original DHT11 plan). `i2c_addr` accepts either form — `0x44` hex or
|
||||
`68` decimal.
|
||||
|
||||
**Finding the bus.** The number varies by board, and on this LattePanda the sensor answers on
|
||||
`/dev/i2c-2`, not the `/dev/i2c-1` default. Two traps make this harder than it looks:
|
||||
|
||||
- **`i2cdetect` false-negatives on the SHT4x.** Its probe (quick-write, or a plain read) is not a
|
||||
command the sensor answers, so the address can show empty on the very bus the sensor is on. Probe
|
||||
with a real measurement instead — `i2ctransfer -y N w1@0x44 0xfd`, wait ~10 ms, then
|
||||
`i2ctransfer -y N r6@0x44`; six bytes that pass the CRC is the bus.
|
||||
- **Not every `/dev/i2c-*` is a full I2C bus.** An SMBus-only adapter (`i2cdetect -F N` reports
|
||||
`I2C = no`) cannot carry the SHT4x's transfers at all, so that bus can never work regardless of
|
||||
wiring.
|
||||
|
||||
A failed read is **logged** — once at the start of a failure streak (naming the bus, the address and
|
||||
an `i2ctransfer` probe to run), and again when the sensor recovers, so a wrong bus announces itself
|
||||
instead of leaving a silently empty panel, without reprinting the same line every `period_ms` for
|
||||
the life of the process. A sample that fails its CRC is logged separately each time. The backend (`Sht41EnvSensor`)
|
||||
polls at `period_ms`, discarding any sample that fails
|
||||
the SHT4x CRC-8 check (decode lives in the pure, unit-tested
|
||||
[Sht41Protocol](../include/fgc/sensors/Sht41Protocol.h)); once nothing valid arrives within **2× the
|
||||
period**, `sample()` reports absence rather than a frozen value, which is what turns the panel from
|
||||
|
|
@ -252,6 +265,7 @@ Handled in `Application::Impl::handleCommand`.
|
|||
| `debug` | Toggle debug logging |
|
||||
| `trace <cat> [on\|off]` | Toggle a wire-trace category (`serial`/`mqtt`/`camera`/`control`) |
|
||||
| `trace all` / `trace off` | Enable every category / silence all |
|
||||
| `set camera profile [<camera>] <name>` | Switch a camera to another profile from its `profile_dir` (the camera may be omitted when only one is configured) |
|
||||
| `set camera jxlq <v>` | JPEG XL distance (0 = lossless) |
|
||||
| `set camera jxle <v>` | JPEG XL effort |
|
||||
| `set camera display <0\|1>` | Toggle OpenCV preview window |
|
||||
|
|
@ -260,8 +274,20 @@ Handled in `Application::Impl::handleCommand`.
|
|||
| `set motorctl <cmd>` | Forward a raw command to the motor controller (e.g. `set motorctl MOVE Y 20000`) |
|
||||
| `test [<subsystem> [<leaf>]] [<profile>]` | Run the hardware self-test suite (see below) |
|
||||
| `test list` / `test baseline` | List profiles / pin the last report as the comparison baseline |
|
||||
| `imu status` | One-line orientation + sample age |
|
||||
| `imu config` | The configuration read back from the device (product, firmware, output mode, sample rate, XKF profile) |
|
||||
| `imu profile [<type\|label>]` | Show the available XKF profiles, or switch to one (by numeric type or by name, e.g. `VRU_general`) |
|
||||
| `imu zero` | Heading reset — make the current yaw the zero reference |
|
||||
| `imu norotation [<seconds>]` | Gyro-bias update; the sensor must be still for the whole window |
|
||||
| `refresh` | Re-read the IMU configuration and request a fresh firmware dump |
|
||||
| `help [topic]` | The command catalog, or one section/verb expanded |
|
||||
| `exit` | Quit (Ctrl-D also works) |
|
||||
|
||||
The full catalog with per-command detail lives in `helpCatalog`
|
||||
([src/core/HelpText.cpp](../src/core/HelpText.cpp)) and is what `help` prints — the table above is a
|
||||
summary, and the `gimbal …` positioning/diagnostic verbs are documented there and in the motor
|
||||
vocabulary section below.
|
||||
|
||||
### `test` — hardware self-test suite
|
||||
|
||||
`test` validates the gimbal/IMU/host on real hardware, writes a text report to
|
||||
|
|
@ -297,6 +323,14 @@ one node in [src/ui/TuiUi.cpp](../src/ui/TuiUi.cpp).
|
|||
- **Gimbal** (`g`) — both axes side by side: live telemetry, the decoded firmware register **dump**
|
||||
(`d` requests a fresh one), the homing limits, and the **last calibration** result (per-axis
|
||||
`counts_per_deg` / `zero_count` / R² / age).
|
||||
- **Cameras** (`c`) — a block per configured camera: identity (`name`, `label`, `id`, `driver`,
|
||||
enabled), the active **profile** and the others available, the `profile_dir` and the exact XML
|
||||
applied, the per-profile `settle_wait_ms`, `rotation_deg` and the JPEG XL settings, plus live
|
||||
per-device telemetry (dimensions, streaming, frames, drops) and the scan grid.
|
||||
- **Connectivity** (`m`) — broker, tower, client id, connection state and control mode, the
|
||||
delivery counters (sent / failed / connects / losses), and a **history of the last MQTT
|
||||
messages** — time, topic leaf and payload — so a publish can be confirmed without a broker-side
|
||||
subscriber. The compact Connectivity panel carries the `m` badge that opens it.
|
||||
- **Sensors** (`i`) — both sensors, in headed sections so every reading is attributable.
|
||||
**MTi**: every channel with units — orientation (°), acceleration (m/s²), rate-of-turn
|
||||
(rad/s), magnetic field (a.u.), `TEMP (internal)`, sample counter, plus an **IMU CONFIG** section
|
||||
|
|
@ -321,11 +355,19 @@ everything else) so it persists across restarts; press **`n`** to keep it for th
|
|||
showing. (Until you answer `y`, calibration remains session-only — see
|
||||
[known-issues.md](known-issues.md).)
|
||||
|
||||
Keys (bottom bar): `s` start · `x` stop · `h` home · `g` gimbal view · `i` IMU view · `r` refresh
|
||||
(re-read IMU config + firmware dump) · arrow keys nudge the gimbal (yaw ±5 % / pitch ±10 %) · `:` open
|
||||
a command line (any console/`gimbal …`
|
||||
command) · `?` help · `q` quit. Plain letters are used rather than Ctrl chords so terminal
|
||||
flow-control (`Ctrl-S`/`Ctrl-Q` XON/XOFF) can't swallow them. In TUI mode all log output is diverted
|
||||
**The key bar is contextual**: it advertises the keys that are useful in the current view, not every
|
||||
binding. `s` start and `x` stop lead everywhere — a scan can be started and stopped from any view —
|
||||
and `:` command, `?` help and `q` quit close every set. The overview adds `h` home, `r` refresh,
|
||||
`g i c m` views, `p` pause and the arrow keys (nudge the gimbal, yaw ±5 % / pitch ±10 %); an
|
||||
expanded view instead offers its own (`d` dump in the gimbal view, `r` refresh in sensors, the
|
||||
scroll keys, `Esc` close). Every binding still *works* everywhere; only the advertising changes.
|
||||
`p` freezes the live refresh so text can be selected and copied.
|
||||
|
||||
**`q` asks rather than quitting.** It opens a prompt: **`q`** quit (stop capture, close the camera,
|
||||
end the program), **`b`** background (detach and leave it running — the same as `Ctrl-b d`, shown
|
||||
dimmed and unavailable when the program is not inside a `scripts/fgc` tmux session), **`Esc`**
|
||||
cancel. Plain letters are used rather than Ctrl chords so terminal flow-control
|
||||
(`Ctrl-S`/`Ctrl-Q` XON/XOFF) can't swallow them. In TUI mode all log output is diverted
|
||||
from stdout into the on-screen log pane via a `Logger` sink, so the screen is never corrupted.
|
||||
|
||||
Build without it (`-DWITH_TUI=OFF`) for a smaller, dependency-free binary; `--tui` then warns and
|
||||
|
|
|
|||
|
|
@ -90,8 +90,11 @@ zkms_server_ip = 10.11.12.13 ; the broker address
|
|||
device = /dev/ttyACM0 ; the motor-controller MCU
|
||||
baud = 115200
|
||||
|
||||
[Camera]
|
||||
id_Cam1 = DEV_1AB22C0AADED ; or GigE IPs like 192.168.11.101
|
||||
[Camera.rgb]
|
||||
id = DEV_1AB22C0AADED ; or a GigE IP like 192.168.11.101
|
||||
label = RGB
|
||||
profile_dir = config/camera_profiles/rgb
|
||||
profile = standard
|
||||
```
|
||||
|
||||
**Credentials** go in the environment, not the config file. Create a root-only env file for the service:
|
||||
|
|
@ -158,8 +161,10 @@ scripts/fgc stop # SIGTERM -> the same clean teardown as `exit`
|
|||
scripts/fgc peek # snapshot of the pane without attaching
|
||||
```
|
||||
|
||||
**`Ctrl-b d` detaches. The dashboard's own `q` key exits the program.** That is the one thing to get
|
||||
right while attached.
|
||||
**`Ctrl-b d` detaches. The dashboard's own `q` key opens an exit prompt** offering `q` (quit the
|
||||
program) or `b` (background — detach and leave it running, the same as `Ctrl-b d`), with `Esc` to
|
||||
cancel. `b` is offered only inside a `scripts/fgc` session; outside one there is nothing to detach
|
||||
into and it is shown dimmed.
|
||||
|
||||
`scripts/fgc stop` sends SIGTERM and waits up to 30s; the program handles it with the same shutdown path
|
||||
as the `exit` command (stop capture, close the camera, flush the image queue). Do **not** `kill -9` —
|
||||
|
|
@ -168,7 +173,7 @@ an unclean exit leaves the camera wedged for the next run. If it will not stop,
|
|||
`scripts/fgc start` refuses if a run is already up, and prints the previous run's last output if it died
|
||||
rather than silently discarding it.
|
||||
|
||||
Quitting the dashboard with `q` while attached ends the tmux session too, so you land straight back in
|
||||
Choosing **quit** at that prompt while attached ends the tmux session too, so you land straight back in
|
||||
your shell — `exit` then closes ssh. A run that *fails* is the exception: its pane is kept so you can read
|
||||
why (`remain-on-exit failed`), and `fgc status` shows it until `fgc stop` or the next `fgc start` clears it.
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ Per-file reference for the refactored tree, plus the shared data structures.
|
|||
| [include/fgc/sensors/Sht41Protocol.h](../include/fgc/sensors/Sht41Protocol.h), [src/sensors/Sht41Protocol.cpp](../src/sensors/Sht41Protocol.cpp) | Sensirion SHT4x wire protocol, pure: command bytes, `sht41Crc8` (poly 0x31/init 0xFF), `decodeSht41Measurement` (6-byte response → `Sht41Reading`, CRC-checked, raw→°C/%RH with RH clamped 0..100) |
|
||||
| [include/fgc/CameraProfile.h](../include/fgc/CameraProfile.h), [src/core/CameraProfile.cpp](../src/core/CameraProfile.cpp) | `loadCameraProfile` / `loadCameraProfiles`: one VimbaX settings XML per profile. Hashes the XML, regenerates the `.txt` record when it changes, and reports which settings moved. A missing XML throws — the record is never a fallback |
|
||||
| [include/fgc/CameraSettingsXml.h](../include/fgc/CameraSettingsXml.h), [src/core/CameraSettingsXml.cpp](../src/core/CameraSettingsXml.cpp) | Parses the Viewer export's `<RemoteDevice>` features (ordered, selector-aware), renders the human-readable `.txt` record, reads one back, and diffs two settings lists. Used for display and change detection only — never to configure a camera |
|
||||
| [include/fgc/ChannelTopics.h](../include/fgc/ChannelTopics.h), [src/core/ChannelTopics.cpp](../src/core/ChannelTopics.cpp) | The MQTT wire format in one place: topic builders for the `GGS/FWT/<tower>/…` tree and the `CamEvent`/`EnvEvent`/status payload builders. Pure, so the documented format is unit-tested rather than assembled inline at each publish site |
|
||||
| [include/fgc/ChannelHistory.h](../include/fgc/ChannelHistory.h), [src/core/ChannelHistory.cpp](../src/core/ChannelHistory.cpp) | Ring buffer of recently published messages (time, topic, payload, outcome) plus the delivery counters — sent, failed, connects, losses, last status. Feeds the expanded Connectivity view so a publish can be confirmed without a broker-side subscriber |
|
||||
| [include/fgc/Hash.h](../include/fgc/Hash.h), [src/core/Hash.cpp](../src/core/Hash.cpp) | FNV-1a 64 over a string or a file, with an algorithm-prefixed label. Change detection for the profile records, deliberately not a crypto hash |
|
||||
| [include/fgc/CaptureScheduler.h](../include/fgc/CaptureScheduler.h), [src/core/CaptureScheduler.cpp](../src/core/CaptureScheduler.cpp) | Capture state machine over the interfaces; injectable clock; `setGeometry` adopts a recalibration |
|
||||
| [include/fgc/Application.h](../include/fgc/Application.h), [src/core/Application.cpp](../src/core/Application.cpp) | Factory (real vs mock, headless vs TUI), wiring, control loop, `gimbal …` commands, background-result polling, `buildSnapshot()` |
|
||||
|
|
@ -128,7 +130,7 @@ Surfaced read-only in the TUI's `i` view (IMU CONFIG section); the legacy "scena
|
|||
|
||||
| Artifact | Path | Format |
|
||||
|----------|------|--------|
|
||||
| Captured images | `<output_dir>/<RGB\|ACR\|NIR>/<unix_ms>.jxl` | JPEG XL, rotated per `Camera.rotation_deg` |
|
||||
| Captured images | `<output_dir>/<label>/<unix_ms>.jxl` | JPEG XL, rotated per that camera's `rotation_deg`; `<label>` is `[Camera.<name>] label` |
|
||||
| Demo placeholder | `bin/x64/Release/test_smoke.jxl` | copied verbatim in demo mode |
|
||||
| Diagnostics log | `<data_dir>/fire_gimbal_control/logs/diag_<ts>.log` | raw `DG` stream + parsed summary |
|
||||
| Calibration log | `<data_dir>/fire_gimbal_control/logs/calib_<ts>.log` | per-axis samples + the least-squares fit |
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ monotonicity and encode-throughput timing.
|
|||
The RGB camera itself is integrated and verified on the rig (see
|
||||
[architecture.md](architecture.md#image-capture)), so this leaf is an implementation task rather than a
|
||||
blocked one. Note that a self-test asserting exposure health needs the lens uncovered and the shipped
|
||||
camera modes tuned first — [known-issues.md](known-issues.md) #15–#16.
|
||||
camera profiles tuned first — [known-issues.md](known-issues.md) #15–#16.
|
||||
|
||||
- **`camera / rgb`** — _Healthy:_ enumerates and connects; delivers frames at ~the
|
||||
configured rate; exposure/gain give a usable histogram (not clipped); few
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@
|
|||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
using namespace fgc;
|
||||
|
||||
TEST_CASE("ConfigLoader maps and defaults typed values") {
|
||||
|
|
@ -294,3 +297,50 @@ TEST_CASE("saveMotorCalibration round-trips the full per-axis map") {
|
|||
|
||||
std::remove(path.c_str());
|
||||
}
|
||||
|
||||
TEST_CASE("loadFromFile anchors a relative profile_dir to the config file's directory") {
|
||||
// The config is found through a search path that has nothing to do with the
|
||||
// working directory, so a relative profile_dir must resolve next to the
|
||||
// config.ini itself. Resolving it against the cwd is how a profile directory
|
||||
// silently goes missing when the program is started from elsewhere.
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path dir = fs::temp_directory_path() /
|
||||
("fgc_cfgdir_" + std::to_string(::getpid()));
|
||||
fs::remove_all(dir);
|
||||
fs::create_directories(dir / "config" / "camera_profiles" / "rgb");
|
||||
const fs::path path = dir / "config.ini";
|
||||
{
|
||||
std::ofstream f(path, std::ios::trunc);
|
||||
f << "[Camera.rgb]\n"
|
||||
"id = DEV_1\n"
|
||||
"profile_dir = config/camera_profiles/rgb\n"
|
||||
"[Camera.nir]\n"
|
||||
"id = DEV_2\n"
|
||||
"profile_dir = /opt/profiles/nir\n"
|
||||
"[Camera.swir]\n"
|
||||
"id = DEV_3\n"
|
||||
"profile_dir = ~/profiles/swir\n";
|
||||
}
|
||||
|
||||
AppConfig cfg = ConfigLoader::loadFromFile(path.string());
|
||||
const CameraSpec* rgb = nullptr;
|
||||
const CameraSpec* nir = nullptr;
|
||||
const CameraSpec* swir = nullptr;
|
||||
for (const auto& c : cfg.camera.cameras) {
|
||||
if (c.name == "rgb") rgb = &c;
|
||||
if (c.name == "nir") nir = &c;
|
||||
if (c.name == "swir") swir = &c;
|
||||
}
|
||||
REQUIRE(rgb);
|
||||
REQUIRE(nir);
|
||||
REQUIRE(swir);
|
||||
|
||||
CHECK(rgb->profile_dir == (dir / "config" / "camera_profiles" / "rgb").string());
|
||||
// An absolute path is the operator's explicit choice and is left alone.
|
||||
CHECK(nir->profile_dir == "/opt/profiles/nir");
|
||||
// ~ expands to the home directory, not to a literal "~" subdirectory.
|
||||
CHECK(swir->profile_dir.rfind("~", 0) != 0);
|
||||
CHECK(swir->profile_dir.find("/profiles/swir") != std::string::npos);
|
||||
|
||||
fs::remove_all(dir);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue