14 KiB
Known Issues — Status
This tracks the reproduction blockers and robustness issues identified in the original code and what the refactor did about them.
Resolved
| # | Original issue | Resolution |
|---|---|---|
| 1 | Hardcoded config path (/home/ggs/...) |
Config search order: --config → $FGC_CONFIG → ./config.ini → exe dir → XDG (src/core/Paths.cpp) |
| 2 | Hardcoded image output path | [Paths] output_dir with ~/$ENV expansion + sensible default |
| 3 | Startup scripts with ~/projects/... |
Replaced by path-independent scripts/run.sh + systemd unit |
| 4 | Vimba X required to build | WITH_VIMBA CMake option (default ON); OFF builds a mock-only binary |
| 5 | No hardware path; exits if MQTT down | Mock implementations + runtime toggles; MQTT failure now logs and continues |
| 8 | Plaintext MQTT credentials | $FGC_MQTT_USER/$FGC_MQTT_PW env override; config.ini gitignored |
| 9 | MQTT busy-wait (while(running);) |
Gone — Paho async client + set_automatic_reconnect; no spin thread |
| 10 | parser() missing return |
Old parser removed; parseTelemetryLine returns std::optional cleanly |
| 11 | Fragile Boost.Spirit command grammar | Replaced by parseCommand whitespace tokenizer (src/core/CommandParser.cpp) |
| 12 | Two divergent config.ini files |
Single committed config/config.example.ini; real configs gitignored |
| 6 | Telemetry field order: humidity before temperature | Obsolete — migrated to the current firmware's ST protocol (encoder counts, no environmental fields); humidity/temp/fan are gone |
| 7 | Trigger fires while is_moving == 1 (not when stopped) |
Fixed by the protocol migration — capture is now move → settle → trigger; the camera fires only once both axes report standstill at the target (CaptureScheduler.cpp) |
Also added along the way: a leveled logger, typed/validated config, an SDK-independent core library, and a
doctest unit-test suite (ctest).
Open / needs hardware confirmation
| # | 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 on hardware | Mechanically verified on the rig (2026-08-07). A full --init --start run homed both axes and completed MOVE → settle → trigger for 15/15 waypoints with 0 dropped frames, 0 stalls and 0 acquisition failures. kSettleTolCounts (600) and the per-interval timing in CaptureScheduler.cpp have not been tuned against observed ST behaviour yet, and the settle tolerance has not been checked against a sharp image (see #16). |
| 15 | Exposure convergence unverified | The quality gate has never metered a properly-lit scene. On the rig the lens was covered, so every frame pinned at the exposure/gain ceilings and returned saturated; the correction path ran (dark → corrected → saturated) but never converged onto target_mean. Re-run one sweep with the lens uncovered and confirm the mean lands within mean_tolerance in ≤2 attempts at most waypoints. |
| 16 | Blur check never exercised | The relative-sharpness test needs a reference score from a genuinely good frame at that angle; the stored values were all noise-level (sharpness ~1–26 on black frames). blur_relative_floor (0.5) is therefore an untuned guess. Verify on a lit scene, and specifically that a deliberate disturbance (nudge the rig during a shot, or shorten settle_delay_ms to 0) is actually caught. |
| 17 | degraded path never fired on hardware |
Every rig capture either passed or hit saturated, so the budget-exhausted branch — best-effort frame + degraded flag through log/TUI/CamEvent — is covered only by unit tests. Confirm once thresholds are tuned. |
| 18 | [Camera] exposure_max_us / gain_max_db unset on the device |
The deployed config.ini leaves both at 0, so the gate falls back to hardcoded 20 ms / 12 dB. Those need real values from a lit scene, and matter more than usual on this rig: the long optical zoom needs more light while tolerating less exposure time (blur scales with focal length). |
| 19 | USB preconditions do not survive a reboot | usbcore.usbfs_memory_mb reverts to 16 (needs 1000) and USB3 LPM re-enables on the camera's port. Both were wrong on the LattePanda when tested on 2026-08-07, and frame delivery stalls without them regardless of application code. Needs the udev rule + kernel cmdline persistence described under Camera acquisition below. |
Camera acquisition (USB3) — root cause + approach
Real-camera capture (Alvium 1800 U-2040c, Sony IMX541, 20.4 MP) was brought up on the LattePanda this
session. Two problems were root-caused (both host/USB3-side, reproduced in Allied Vision's own
vmbpy — the camera keeps acquiring while the host stops receiving; no kernel errors):
- Camera shipped in hardware-trigger mode (
TriggerSource=Line0) → produced no frames. Fixed by configuring software trigger explicitly in-session in VimbaCameraSource.cpp (relying on the camera's persisted user set alone did not work). - Acquisition stall — frame delivery stops after a few frames:
- USB3 hardware LPM (U1/U2) was enabled on the camera's link (the camera logs
Enable of device-initiated U2 failed) → silent bulk-transfer stalls under load. Disable per-port: write0to/sys/devices/.../usb2/2-0:1.0/usb2-port<N>/usb3_lpm_permit, then re-enumerate (the per-devicepower/usb3_hardware_lpm_u*files are read-only). Make persistent via a udev rule matched on idVendor1ab2. - The host xHCI cannot reliably move large single frames. Measured (LPM off, paced ~1 fps): RGB8 full-res 61 MB stalls in 3–5 frames; ≤ ~20 MB frames sustain when paced (BayerRG8 full-res 20 MB → 21 frames/20 s; 3.8 MB → 41/20 s). Freerun at max rate stalls even for medium frames.
- Also raise
usbcore.usbfs_memory_mbto 1000 (persist via kernel cmdline; resets to 16 on reboot).
- USB3 hardware LPM (U1/U2) was enabled on the camera's link (the camera logs
Approach (config-driven, see [Camera]/[Capture] in configuration.md): RGB8 +
2×2 binning (~5 MP, ~15 MB) keeps de-Bayering and white balance on-camera (the on-camera pipeline
applies white balance before de-Bayering). DeviceLinkThroughputLimit is kept conservative (~250 MB/s;
≥450 caused incomplete frames). Full 20 MP is possible via BayerRG8 (20 MB) + a host de-Bayer step if
reduced resolution proves insufficient.
Acquisition is now per-waypoint software trigger ([Capture] mode = trigger), not the paced stream.
Note what the measurements above do and do not show: they bound frame size and pacing, not trigger
mode. The earlier conclusion that "software trigger stalls this host" was drawn from an implementation
that triggered only cameras[0], never set TriggerSource/TriggerMode at all, fired a blind 4×400 ms
burst, and ran before the binning fix — i.e. it was pushing 61 MB frames down the path that stalls for
unrelated reasons. One frame per waypoint is a lower USB load than a 1 fps stream. mode = freerun
restores the old paced-stream behaviour if a host ever does misbehave under trigger.
Status (rig-tested 2026-08-07). The [Camera]/[Capture] schema, in-session imaging config,
near-lossless JPEG XL, the quality gate and unit tests are in place. On the LattePanda with the real Alvium:
Confirmed working — software trigger sustains a full sweep (15/15 waypoints, 0 dropped, 0 stalls, 0
acquisition failures, across four runs), which settles the load-bearing assumption behind mode = trigger.
Camera configuration was verified independently through Allied Vision's own vmbpy (RGB8, binning 2,
2256×2256, ExposureAuto=Off, GainAuto=Off, TriggerMode=On, TriggerSource=Software,
TriggerSelector=FrameStart), and ExposureTime writes land within quantisation (want 20000 → readback 19980.6). Real-motor integration, per-angle store persistence + warm start, and clean shutdown
(camera not left wedged) all verified.
Not confirmed — the lens was covered for the whole test, so every frame was black (mean ~1/255) and the
gate could only ever report saturated. Everything that depends on seeing a real scene is still open: see
#15–#18 in Open / needs hardware confirmation above, plus #19 for the USB preconditions.
A useful diagnostic for the "is it the optics or the code?" question: with frame status validated and gain 0,
a 2-second exposure produced mean 3.6/255 (brightest pixel 80). A lit room at gain 0 should be well
exposed near 10–30 ms, so that is ~1000× too little light — conclusively optical, not software. vmbpy
scripts for this are worth keeping; note that wrapping them in timeout SIGTERMs Python and skips
stop_streaming(), which leaves the camera unusable until the next USB reset.
Verification caveats
- Full build verified on the LattePanda: a
WITH_VIMBA=ON WITH_MQTT=ONbuild 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 — seecmake/Paho.cmakefor the toolchain-compat shims). - Serial protocol verified live against real firmware (LattePanda, this session):
ENABLE/HOME/SPEEDreach the firmware, theSTtelemetry parses with zero unparsed lines across a full session, and a live--initdrove a clean re-home of both axes toREADY. - Real-camera capture verified on the rig (2026-08-07): a live
--init --startrun with the real Alvium and real motor captured 15/15 waypoints, one software-triggered frame each, 0 dropped and 0 stalls. What this does not cover is image content — the lens was covered, so exposure/blur behaviour on a real scene remains open (#15–#17). - Makefile parity is moot: the Makefile was removed in favour of CMake, and the full
WITH_VIMBA=ON WITH_MQTT=ONCMake build now runs on the device. - Demo mode copies
bin/x64/Release/test_smoke.jxl, resolved relative to the working directory. Run from a directory where that path exists, or extendImagePipeline::Params::demo_image.
Possible follow-ups (not done)
- Spurious
could not set AcquisitionFrameRateEnable=falsewarning on every camera start.AcquisitionFrameRateEnableandTriggerModelock each other: whileTriggerMode=Onthe frame-rate control is read-only, and vice versa. Re-configuring an already-triggered camera therefore tries to write a locked feature that is already at the wanted value. All four state transitions were traced and the ordering inconfigureCamerais correct — this is cosmetic — but it is misleading noise that would mask a genuine failure of that constraint. Fix by skipping the write when the value already matches. - Per-angle white balance. WB is deliberately left on the camera (
BalanceWhiteAuto=Continuous) since it is correctable in post; storing and seeding it per angle like exposure is the natural next step once exposure gating is proven on a lit scene. - Decaying sharpness reference.
ExposureStorekeeps the last accepted sharpness as the blur reference. If a scene changes character (fog rolling in), that baseline is stale in the wrong direction. A recent-window or decaying value would be more robust — worth revisiting with real data from #16. - Graceful shutdown on SIGINT (currently exit via
exit/Ctrl-D; a pendinggetlinecan delay shutdown). - Make the camera index→label map fully config-driven. (JPEG XL distance/effort and the on-camera imaging
settings are now config-driven under
[Camera]— see configuration.md.) - Reintroduce optional image upload to the ground station, config-driven (the old hardcoded NFS/SMB upload was removed).
gimbal calibpersistence: the fittedcounts_per_deg/zero_countare always applied to the live session (display, manual moves, MQTT heading, and the capture scheduler) and written tologs/calib_*.log. In the TUI, the activity strip then promptsSave … as the new default? (y/n)—ywrites the full per-axis[Motor]map (*_counts_per_deg,*_zero_count,*_min_deg/*_max_deg) intoconfig.ini(persists across restarts),nkeeps them session-only. In the headless console there is no prompt, so calibration stays session-only there; copy the logged values into[Motor]by hand to keep them.gimbal nudgemoves a fixed fraction of the homed endstop-to-endstop travel (from the firmware dump), so it is independent of the degrees↔counts calibration. If no dump has been captured yet (not homed / nogimbal dump), nudge requests one and does nothing that press — it never falls back to the configured degree clamps (which, if*_min_deg/*_max_degwere unset, produced absurd ±100000° steps).- Ambient temperature/humidity (SHT41,
IEnvSensor/Sht41EnvSensor/MockEnvSensor) is fully integrated and validated against the physical sensor on the LattePanda — config-gated ([Features] enable_env,[Env]), Sensors panel + expandediview + MQTTEnvtopic wired. It is off by default (enable_env = false), so a config without an[Env]section shows the panel aspending— that status means "no driver constructed", not "sensor unreachable"; an enabled-but-silent sensor readsno fixinstead. Dev machines without the hardware should use--mock-env(impliesenable_env) rather than editing config. [Env] i2c_addrused to be parsed with a decimal-onlystd::stoi, so the0x44form every datasheet andi2cdetectuses silently became address0x00and the sensor never answered. It now accepts decimal or0xhex (getIntAutoBaseinConfig.cpp, covered by a test). Other integer config keys stay decimal-only on purpose, so a leading zero never turns into octal.