Rewrite the README around running a tower, with screenshots and diagrams
708
README.md
|
|
@ -1,269 +1,557 @@
|
||||||
# Fire Gimbal Control
|
# Fire Gimbal Control
|
||||||
|
|
||||||
Real-time control software for an automated **fire-watch gimbal**. A C++17 program runs on a tower-mounted
|
**The program that runs a fire-watch tower.** It points a camera around the
|
||||||
PC, rotates a pan gimbal carrying up to four industrial cameras, captures a 360° panorama for wildfire
|
horizon on a motorised gimbal, keeps one well-exposed picture at every stop,
|
||||||
detection, compresses each frame to JPEG XL, and reports to a ground station over MQTT.
|
saves it, and tells the ground station about it - all day, unattended, on a
|
||||||
|
small PC at the top of a mast.
|
||||||
|
|
||||||
A single codebase serves all fire-watch towers (FWT); the tower identity comes from `config.ini`.
|

|
||||||
|
<sub>The dashboard of tower 111, captured live on 2026-09-25: both axes homed
|
||||||
|
and ready, the camera mid-sweep at waypoint 2 of 15, the orientation and
|
||||||
|
ambient sensors reporting, messages flowing to the broker, and the last
|
||||||
|
self-test green.</sub>
|
||||||
|
|
||||||
The code is organized around an **SDK-independent core** (`fgc_core`: config, logging, capture scheduler,
|
---
|
||||||
parsers) and **swappable I/O implementations** behind three interfaces — motor controller, control channel,
|
|
||||||
and camera source — each with a real and a mock/null version. This makes the system deployable on any machine
|
|
||||||
and runnable **without hardware or an MQTT broker** for development.
|
|
||||||
|
|
||||||
State is held in memory (mutex-guarded), configuration is read once from an INI file, images are written to
|
## Contents
|
||||||
the filesystem as `.jxl`, and telemetry flows over MQTT. There is no database.
|
|
||||||
|
|
||||||
## Architecture at a glance
|
- [What it does](#what-it-does)
|
||||||
|
- [How the pieces fit together](#how-the-pieces-fit-together)
|
||||||
|
- [A scan, step by step](#a-scan-step-by-step)
|
||||||
|
- [Running a tower](#running-a-tower)
|
||||||
|
- [The dashboard](#the-dashboard)
|
||||||
|
- [The plain console](#the-plain-console)
|
||||||
|
- [Everyday commands](#everyday-commands)
|
||||||
|
- [Is the tower healthy?](#is-the-tower-healthy)
|
||||||
|
- [Setting up and updating a tower](#setting-up-and-updating-a-tower)
|
||||||
|
- [For developers](#for-developers)
|
||||||
|
- [Documentation](#documentation)
|
||||||
|
|
||||||
```
|
---
|
||||||
┌──────────────────────── fgc_core (no SDKs) ─────────────────────────┐
|
|
||||||
│ Config · Logger · CaptureScheduler · TelemetryParser · CommandParser │
|
## What it does
|
||||||
└───────▲─────────────────▲──────────────────▲────────────────────────┘
|
|
||||||
│ IMotorController │ IControlChannel │ ICameraSource
|
A fire-watch tower is a mast with a camera on a **gimbal** - a mount with two
|
||||||
┌───────────┴──────┐ ┌────────┴─────────┐ ┌──────┴────────────────┐
|
motors, one turning left and right (**yaw**), one tilting up and down
|
||||||
real │ SerialMotorCtrl │ │ MqttControlChannel│ │ VimbaCameraSource │ (WITH_VIMBA/WITH_MQTT)
|
(**pitch**). This program, `fire_gimbal_control`, runs on the tower's
|
||||||
mock │ MockMotorCtrl │ │ NullControlChannel│ │ MockCameraSource │
|
LattePanda PC and does the watching:
|
||||||
└──────────────────┘ └──────────────────┘ └──────────┬────────────┘
|
|
||||||
frames
|
1. **Sweeps a scan grid.** A list of directions - "0 deg, 10 deg up", "45 deg,
|
||||||
▼
|
10 deg up", and so on - that together cover the view. Tower 111's grid has
|
||||||
ImagePipeline → .jxl + CamEvent
|
15 stops.
|
||||||
|
2. **Keeps one good picture per stop.** The camera runs continuously with its
|
||||||
|
own auto-exposure; when the gimbal has stopped and settled, the newest frame
|
||||||
|
is kept.
|
||||||
|
3. **Saves it as JPEG XL**, a compact modern image format, one file per picture.
|
||||||
|
4. **Tells the ground station** over MQTT - which camera, which direction, and
|
||||||
|
when - so the picture can be fetched and checked for smoke.
|
||||||
|
|
||||||
|
It does this unattended: it starts at boot, homes the gimbal, and sweeps until
|
||||||
|
it is told to stop. A person only needs to look in when something is wrong -
|
||||||
|
and the dashboard, the log and the self-tests are there to show what.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How the pieces fit together
|
||||||
|
|
||||||
|
Two programs cooperate, on two different chips:
|
||||||
|
|
||||||
|
| | The **firmware** | The **host program** (this repository) |
|
||||||
|
|---|---|---|
|
||||||
|
| Runs on | the small motor board (an RP2040 on the newer IOTA towers, an ATmega32U4 "Leonardo" on the older Delta ones) | the LattePanda PC |
|
||||||
|
| Lives in | its own repository, `GGS/fwt_firmware` on the same git server, checked out beside this one as `../firmware` | here |
|
||||||
|
| Its job | the **muscles and reflexes**: drive the two stepper-motor drivers, find the endstops, hold a position, stop the moment something is wrong | the **brain**: decide where to point and when to take a picture, own every setting, run the camera, talk to the ground station |
|
||||||
|
|
||||||
|
The firmware keeps **no settings of its own**. Every number that describes the
|
||||||
|
motors - the currents, the speeds, the travel, how a stall is detected - lives
|
||||||
|
on the PC, in a file called `hardware.ini`, and the program hands it to the
|
||||||
|
firmware each time they connect. Until it has them, the firmware **refuses to
|
||||||
|
move** - a motor running on guessed values can cook its driver or push the
|
||||||
|
gimbal through its endstop, and both have happened on this project.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
HW["hardware.ini<br/>what the tower IS"] --> APP
|
||||||
|
CFG["config.ini<br/>what the tower DOES"] --> APP
|
||||||
|
APP["fire_gimbal_control<br/>on the LattePanda"] -- "USB serial<br/>commands + settings" --> MCU["motor board<br/>firmware"]
|
||||||
|
MCU -- "status, 4x a second" --> APP
|
||||||
|
MCU -- SPI --> DRV["TMC5160<br/>stepper drivers"] --> MOT["yaw + pitch<br/>motors"]
|
||||||
|
CAM["camera<br/>(USB3)"] -- frames --> APP
|
||||||
|
APP --> JXL["pictures<br/>.jxl files"]
|
||||||
|
APP -- MQTT --> BROKER["broker on the tower"] --> GS["ground station"]
|
||||||
```
|
```
|
||||||
|
|
||||||
See [docs/architecture.md](docs/architecture.md) for the full design.
|
### The two files
|
||||||
|
|
||||||
## Build
|
Everything that makes one tower different from another is in two files:
|
||||||
|
|
||||||
|
| File | Where | What goes in it | Who writes it |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `hardware.ini` | `/etc/fire_gimbal_control/` | **What the tower IS** - the board, which axes and sensors are fitted, how the motor board is wired, each motor's currents, travel and speeds, the camera's id | the installer, once; a person when the hardware changes |
|
||||||
|
| `config.ini` | beside the program | **What the tower DOES** - its name, the scan grid, capture settings, the broker, logging, and calibration such as which way is north | the installer (a first copy), the calibration commands, a person |
|
||||||
|
|
||||||
|
Where both set the same thing, `hardware.ini` wins, and the program says so at
|
||||||
|
start-up. Full reference: [docs/configuration.md](docs/configuration.md) and
|
||||||
|
[config/hardware.example.ini](config/hardware.example.ini).
|
||||||
|
|
||||||
|
### How they start talking
|
||||||
|
|
||||||
|
Every time the program connects to the motor board - at boot, after a restart,
|
||||||
|
after the firmware is updated - they go through the same short conversation:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant F as Firmware (motor board)
|
||||||
|
participant H as fire_gimbal_control (PC)
|
||||||
|
Note over F: power-up: stop both drivers,<br/>refuse HOME / MOVE
|
||||||
|
F->>H: BOOT build=...
|
||||||
|
F->>H: NEED Y steps_per_rev run_irun ... (what it is missing)
|
||||||
|
H->>F: SET Y steps_per_rev 177000
|
||||||
|
H->>F: SET Y ... (every value, from hardware.ini)
|
||||||
|
Note over F: all values in: axis ready to home
|
||||||
|
H->>F: DUMP
|
||||||
|
F->>H: DUMP ... (every value it now holds, and the pins it was built with)
|
||||||
|
Note over H: compare with hardware.ini<br/>(this is `test hardware motor`)
|
||||||
|
H->>F: HOME
|
||||||
|
F->>H: ST Y:A,... P:A,... (both axes READY)
|
||||||
|
```
|
||||||
|
|
||||||
|
So a tower is re-tuned by **editing `hardware.ini` and restarting the program** -
|
||||||
|
no firmware update needed. Only the **wiring** (which pin drives which driver)
|
||||||
|
is built into the firmware, and even that is read from the tower's own
|
||||||
|
`hardware.ini` when the firmware is built.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A scan, step by step
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A([next waypoint]) --> B["move yaw and pitch<br/>to the waypoint"]
|
||||||
|
B --> C{"both axes<br/>stopped?"}
|
||||||
|
C -- no --> C
|
||||||
|
C -- yes --> D["hold still for settle_wait_ms<br/>(1 s by day, 2.5 s at night)"]
|
||||||
|
D --> E["keep the newest frame<br/>the camera delivered"]
|
||||||
|
E --> F["encode to JPEG XL<br/>save to the output folder"]
|
||||||
|
F --> G["publish a CamEvent<br/>(camera, heading, pitch, time)"]
|
||||||
|
G --> A
|
||||||
|
```
|
||||||
|
|
||||||
|
The camera never stops running. It free-runs at its profile's frame rate with
|
||||||
|
its own auto-exposure following the light, and almost every frame is thrown
|
||||||
|
away - their job is to keep the exposure right, so that the one frame kept at
|
||||||
|
each stop is already correctly exposed and was taken with the gimbal at rest.
|
||||||
|
|
||||||
|
Which frame rate and exposure limits apply comes from a **camera profile**: a
|
||||||
|
settings file exported from the camera itself. `standard` is for daylight,
|
||||||
|
`night` for low light; switch with `set camera profile night`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running a tower
|
||||||
|
|
||||||
|
On a tower the program runs **detached, inside `tmux`**, so it keeps running
|
||||||
|
when you close your ssh session. `scripts/fgc` does that for you:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cmake -B build # configure (needs the Vimba X SDK + Paho for a full build)
|
scripts/fgc start --init --start # start it: home the gimbal, then sweep
|
||||||
cmake --build build # -> build/fire_gimbal_control
|
scripts/fgc attach # open the dashboard (Ctrl-b d to leave it running)
|
||||||
|
scripts/fgc status # is it running? since when? which PID?
|
||||||
# Development build with NO proprietary SDKs (mocks only):
|
scripts/fgc peek # a text snapshot of the dashboard, without attaching
|
||||||
cmake -B build -DWITH_VIMBA=OFF -DWITH_MQTT=OFF
|
scripts/fgc stop # a clean shutdown - camera and motors stopped properly
|
||||||
cmake --build build
|
scripts/fgc restart # stop, then start again
|
||||||
```
|
```
|
||||||
|
|
||||||
Dependencies and the Vimba X SDK setup are in [docs/build-and-setup.md](docs/build-and-setup.md).
|
> **Tip - detach, don't quit.** Inside the dashboard, `Ctrl-b d` leaves the
|
||||||
|
> program running. The dashboard's own `q` asks whether you want to **quit** the
|
||||||
|
> program or **background** it; background does the same as `Ctrl-b d`.
|
||||||
|
|
||||||
## Run
|
**At boot**, a tower with the boot service starts itself the same way - through
|
||||||
|
`fgc`, so the dashboard is still there to attach to:
|
||||||
On a tower the program must outlive the ssh session that started it, so **`scripts/fgc`** runs it
|
|
||||||
detached inside a tmux session:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scripts/fgc start --start # start detached; returns immediately
|
sudo systemctl status fire-gimbal-control # is the service running?
|
||||||
scripts/fgc attach # watch the dashboard — Ctrl-b d leaves it running
|
sudo systemctl restart fire-gimbal-control # restart it (it homes the gimbal again)
|
||||||
scripts/fgc status # running? since when? which PID?
|
|
||||||
scripts/fgc stop # SIGTERM -> clean camera/motor teardown
|
|
||||||
scripts/fgc peek # snapshot of the pane without attaching
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Disconnecting from ssh while attached — or closing the laptop — is invisible to the program: tmux owns
|
`--init` homes the gimbal at every start, which a tower in service needs: the
|
||||||
the pty. **When attached, the dashboard's `q` key exits the program; `Ctrl-b d` is what detaches.**
|
motor board forgets its position whenever it loses power. On tower 111 the
|
||||||
|
service starts it with `--init --start`, so after a power cut it is back to
|
||||||
|
capturing within about a minute, with nobody touching it.
|
||||||
|
|
||||||
From a dev box, `./deploy.sh --run` builds on the device and starts it this way; `--attach`, `--status`
|
**Only one copy can run at a time.** Two would fight over the motor board and
|
||||||
and `--stop` drive it afterwards without re-syncing.
|
the camera, so a second one refuses to start and says which process holds the
|
||||||
|
lock. After a crash or a power cut there is nothing to clean up.
|
||||||
|
|
||||||
**Only one instance may run at a time.** The binary takes an exclusive `flock` on
|
**The log** is kept per run in
|
||||||
`/run/user/<uid>/fire_gimbal_control.lock` (or `/tmp/fire_gimbal_control-<uid>.lock`) at startup and
|
`~/.local/share/fire_gimbal_control/logs/fgc_<date>-<time>.<n>.log`, more
|
||||||
exits with status 2 and `another instance is already running (PID N)` if it is taken — two instances
|
detailed than what the dashboard shows:
|
||||||
would fight over the serial port and the camera. The check is in the binary, so a
|
|
||||||
bare `./build/fire_gimbal_control` is refused too. Nothing needs cleaning up after a crash or a power
|
|
||||||
cut: the kernel drops the lock when the process dies. `--no-lock` bypasses it for a second mock-only
|
|
||||||
instance during development.
|
|
||||||
|
|
||||||
For development, or for a foreground run you intend to babysit, use `run.sh` directly:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Real deployment (needs cameras, motor MCU on the configured serial port, MQTT broker):
|
tail -F "$(ls -t ~/.local/share/fire_gimbal_control/logs/fgc_*.log | head -1)"
|
||||||
scripts/run.sh --start
|
|
||||||
scripts/run.sh --init --start # find endstops first
|
|
||||||
|
|
||||||
# Development, no hardware or broker:
|
|
||||||
scripts/run.sh --mock-serial --mock-camera --no-mqtt --start --log-level debug
|
|
||||||
|
|
||||||
# Same, but with the full-screen terminal dashboard:
|
|
||||||
scripts/run.sh --tui --mock-serial --mock-camera --no-mqtt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`run.sh` resolves the binary relative to the repo, so it works from any checkout; extra args are forwarded to
|
More in [docs/deployment.md](docs/deployment.md).
|
||||||
`fire_gimbal_control`. On launch the program starts the motor controller, opens the camera(s), starts the
|
|
||||||
image pipeline, connects the control channel (continuing in **degraded mode** if the broker is unreachable),
|
|
||||||
then enters a control loop. Without `--start` it idles until you type `start`. Stop with `exit` or Ctrl-D.
|
|
||||||
|
|
||||||
Config is resolved from `--config <path>`, then `$FGC_CONFIG`, `./config.ini`, the executable's directory, and
|
---
|
||||||
`$XDG_CONFIG_HOME/fire_gimbal_control/config.ini`. Copy the template to get started:
|
|
||||||
|
## The dashboard
|
||||||
|
|
||||||
|
The dashboard (the "TUI", for terminal user interface) is what `scripts/fgc
|
||||||
|
attach` opens. It updates live, and everything on it is read-only unless you
|
||||||
|
press a key that acts.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
| Panel | What it shows |
|
||||||
|
|---|---|
|
||||||
|
| **Header** | the tower's name, the program's build, `LIVE` (or `PAUSED`), how long it has been up |
|
||||||
|
| **GIMBAL** `g` | each axis: its state (`READY`, `HOMING`, `ERROR`...), the angle it is at and the one it is heading for, the motor and encoder counts, and flags - `STILL`/`MOVE`, `STALL`, over-temperature `OT`, endstops `L`/`R` |
|
||||||
|
| **SENSORS** `i` | the orientation sensor (roll, pitch, yaw) and the ambient temperature and humidity, each marked `live` or not |
|
||||||
|
| **CAMERA** `c` | whether it is capturing and how fast, the active profile, and the last picture: which camera, where it pointed, how long ago |
|
||||||
|
| **CONNECTIVITY** `m` | the broker connection, the tower name the messages carry, the control mode |
|
||||||
|
| **LOG** | the latest log lines; warnings in yellow, errors in red. `PgUp` scrolls back, `End` follows again |
|
||||||
|
| **ACTIVITY** | what is running right now (a sweep, a homing, a test) and the result of the last one |
|
||||||
|
| **Key bar** | the keys that do something in this view |
|
||||||
|
|
||||||
|
### Keys
|
||||||
|
|
||||||
|
| Key | Does |
|
||||||
|
|---|---|
|
||||||
|
| `s` / `x` | start / stop the capture sweep |
|
||||||
|
| `h` | home the gimbal |
|
||||||
|
| `r` | refresh: ask the firmware for a full status dump, re-read the sensor setup |
|
||||||
|
| `g` `i` `c` `m` | open the full-screen gimbal, sensors, camera or connectivity view; press it again to close |
|
||||||
|
| `?` | the built-in help: every command, with examples |
|
||||||
|
| `:` | type a command, exactly as in the [command list](#everyday-commands) |
|
||||||
|
| `p` | pause the display, so you can select and copy text (the program keeps running) |
|
||||||
|
| `PgUp` `PgDn` `Home` `End` | scroll the log |
|
||||||
|
| arrows | nudge the gimbal by hand - only when no sweep is running |
|
||||||
|
| `Space` | take one picture now - only when no sweep is running |
|
||||||
|
| `H` | set up north: aim at known landmarks and type their bearings - only when no sweep is running |
|
||||||
|
| `Esc` | close a view; with no view open, **cancel** whatever is running (a test, a homing, a calibration) |
|
||||||
|
| `q` | quit or background the program - it asks which |
|
||||||
|
| `Ctrl-b d` | leave the dashboard and keep the program running (a `tmux` key) |
|
||||||
|
|
||||||
|
### The full-screen views
|
||||||
|
|
||||||
|
**Gimbal** (`g`) - both axes in detail: positions, the motor driver's live
|
||||||
|
registers decoded into words, how well the encoder is tracking, the homed
|
||||||
|
limits and the calibration. The first time it opens it asks the firmware for a
|
||||||
|
full dump; `d` refreshes it.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Sensors** (`i`) - the orientation sensor's full output (acceleration, turn
|
||||||
|
rate, magnetic field, its internal temperature) and configuration, and the
|
||||||
|
ambient sensor with the bus it is on.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Camera** (`c`) - the camera itself (model, frame size, exposure, gain,
|
||||||
|
temperature, frames delivered and dropped, stream restarts), its profile and
|
||||||
|
encoding, and the whole scan grid with the current waypoint marked.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Connectivity** (`m`) - the broker connection, message counters, the topics
|
||||||
|
the tower listens and speaks on, and the most recent messages, exactly as sent.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Help** (`?`) - every command, grouped, with an explanation and an example.
|
||||||
|
The same text is available anywhere as `help` or `help <topic>`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The plain console
|
||||||
|
|
||||||
|
Without the dashboard, the program prints its log lines to the terminal and
|
||||||
|
reads commands from the keyboard, one per line. That is what you get with
|
||||||
|
`--no-tui`, with `[UI] enable_tui = false` in `config.ini`, or whenever the
|
||||||
|
output goes somewhere that is not a terminal - a pipe, a file, a script.
|
||||||
|
|
||||||
|

|
||||||
|
<sub>Tower 111's own log from its start on 2026-09-24 - the plain console prints
|
||||||
|
exactly these lines. It loads its settings, opens the motor board, the sensors
|
||||||
|
and the camera, hands the firmware its settings, homes, starts sweeping, and
|
||||||
|
answers a typed `test hardware`.</sub>
|
||||||
|
|
||||||
|
Type any command from the [list below](#everyday-commands) and press Enter;
|
||||||
|
`exit` (or `Ctrl-D`) stops the program cleanly. Nothing here needs a mouse or a
|
||||||
|
large window, which makes it the easiest way to run the program by hand while
|
||||||
|
bringing a tower up.
|
||||||
|
|
||||||
|
For scripts, `--test` runs one self-test and exits with its verdict:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp config/config.example.ini config.ini # then edit
|
./build/fire_gimbal_control --no-mqtt --test hardware
|
||||||
|
echo $? # 0 = PASS, 4 = FAIL, 5 = could not start (the log says why)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Everyday commands
|
||||||
|
|
||||||
|
The same commands work at the `:` prompt in the dashboard and in the plain
|
||||||
|
console. The ground station, over MQTT, has a narrower remote control: it can
|
||||||
|
switch the tower between the automatic sweep and pointing at a bearing it
|
||||||
|
names (`ControlCode`, `target_HDG` - see [docs/mqtt-api.md](docs/mqtt-api.md)).
|
||||||
|
|
||||||
|
| Command | Does |
|
||||||
|
|---|---|
|
||||||
|
| `start` / `stop` | start / stop the capture sweep (the gimbal stays homed and powered) |
|
||||||
|
| `gimbal home` | find the endstops and centre - needed after every power-up |
|
||||||
|
| `gimbal move <yaw>,<pitch>` | point at a **true bearing** (0 = north) and an elevation, in degrees |
|
||||||
|
| `gimbal nudge <yaw\|pitch> <+/-percent>` | a small move by hand |
|
||||||
|
| `snapshot` | take one picture now (only when no sweep is running) |
|
||||||
|
| `test [<subsystem> [<test>]]` | run self-tests - see [below](#is-the-tower-healthy) |
|
||||||
|
| `gimbal heading setup` | tie the yaw axis to true north by sighting landmarks |
|
||||||
|
| `set camera profile <name>` | switch the camera profile, e.g. `night` |
|
||||||
|
| `set fps <n>` | how many pictures per second the sweep aims for |
|
||||||
|
| `camera reset` | reboot the camera and put it back on its profile - for a camera that has stopped sending pictures |
|
||||||
|
| `gimbal dump` | show everything the firmware is holding |
|
||||||
|
| `debug` | more detail in the log, or back to normal |
|
||||||
|
| `trace <serial\|mqtt\|camera\|control\|all\|off>` | log every message to and from a part, word for word |
|
||||||
|
| `help [<topic>]` | the full list, with examples |
|
||||||
|
| `exit` | stop everything and quit |
|
||||||
|
|
||||||
|
The complete list, with every option, is in the built-in `help` and in
|
||||||
|
[docs/configuration.md](docs/configuration.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Is the tower healthy?
|
||||||
|
|
||||||
|
One command checks almost everything, and changes nothing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/install-tower --verify
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
== verify ==
|
||||||
|
ok usbfs_memory_mb 1000 (want >= 1000)
|
||||||
|
ok camera USB3 LPM 0 (want 0)
|
||||||
|
ok config files load cleanly, 0 shadowed key(s)
|
||||||
|
ok hardware.ini complete 0 TODO(s)
|
||||||
|
ok firmware pins running build=a0b1e4a ...; declared ... Y=8,7 P=1,0
|
||||||
|
ok test hardware PASS
|
||||||
|
ok test camera PASS
|
||||||
|
ok boot service enabled
|
||||||
|
ok user lingering yes
|
||||||
|
|
||||||
|
>> all checks pass
|
||||||
|
```
|
||||||
|
|
||||||
|
<sub>Tower 111, 2026-09-24.</sub>
|
||||||
|
|
||||||
|
If the program is running, the tests run **inside** it - the sweep pauses for
|
||||||
|
a few seconds and carries on afterwards. The two tests that matter most:
|
||||||
|
|
||||||
|
- **`test hardware`** - is this the tower `hardware.ini` describes? The declared
|
||||||
|
axes answer, the firmware holds exactly the declared settings and was built
|
||||||
|
for the declared wiring, the orientation and ambient sensors (if fitted) are
|
||||||
|
really there, the camera opened, and the USB settings the camera needs are in
|
||||||
|
place.
|
||||||
|
- **`test camera`** - does the camera work, whatever it is looking at? It never
|
||||||
|
judges the picture - fog, night or a lens cap would all make a bad picture out
|
||||||
|
of a healthy camera. Instead it uses the camera's own settings:
|
||||||
|
- **frames** - whole pictures arrive, in time;
|
||||||
|
- **dark** - at the shortest exposure and lowest gain, every pixel goes black;
|
||||||
|
- **bright** - at the highest gain and a long exposure, the picture becomes
|
||||||
|
clearly different from the dark one. That can only happen if the sensor is
|
||||||
|
alive and took the settings.
|
||||||
|
|
||||||
|
Afterwards the camera goes back on its profile.
|
||||||
|
|
||||||
|
A test that could not run - no ambient sensor fitted, say - says **not
|
||||||
|
checked**, never PASS. Every run is written to
|
||||||
|
`~/.local/share/fire_gimbal_control/logs/test_*.log` and compared with the one
|
||||||
|
before. More: [docs/test-command.md](docs/test-command.md).
|
||||||
|
|
||||||
|
Other test groups need a homed gimbal and move it: `test gimbal` (homing
|
||||||
|
repeatability, encoder, friction, backlash, balance) and `test imu`. `test host`
|
||||||
|
checks the PC itself (temperature, disk, memory, load).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setting up and updating a tower
|
||||||
|
|
||||||
|
**A new tower** - or one set up before any of this existed - is installed by
|
||||||
|
one script, run on the tower:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/install-tower --dry-run # look, ask, show the plan; change nothing
|
||||||
|
scripts/install-tower # the same, then do it after one "yes"
|
||||||
|
```
|
||||||
|
|
||||||
|
It installs the packages and USB settings, the camera SDK, builds this program
|
||||||
|
and the firmware, writes the two files (asking only what it cannot find out
|
||||||
|
for itself), flashes the firmware, runs the tests, and sets up the boot
|
||||||
|
service. It **never guesses a motor value**: anything nobody has measured is
|
||||||
|
written as a `; TODO`, and the firmware is not flashed until it is filled in.
|
||||||
|
It only homes the gimbal after someone says they are standing at it. Re-running
|
||||||
|
it on a working tower is safe; it changes only what is out of date. The whole
|
||||||
|
story: [docs/tower-installation.md](docs/tower-installation.md).
|
||||||
|
|
||||||
|
**Updating** is done from a development machine, with each repository's
|
||||||
|
`deploy.sh`, and always in this order:
|
||||||
|
|
||||||
|
1. **this program first** - `./deploy.sh` here copies the code to the tower and
|
||||||
|
builds it there; restart it to use the new version;
|
||||||
|
2. **the firmware second** - `./deploy.sh` in the firmware repository builds it
|
||||||
|
on the tower and flashes the motor board.
|
||||||
|
|
||||||
|
Host first, because a new firmware asks for its settings the moment it starts,
|
||||||
|
and an older program would not know how to answer. Details:
|
||||||
|
[docs/deployment.md](docs/deployment.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For developers
|
||||||
|
|
||||||
|
### The shape of the code
|
||||||
|
|
||||||
|
The program is a core that needs no hardware or proprietary SDKs, plus a real
|
||||||
|
and a stand-in version of each part that touches the outside world. That is
|
||||||
|
what lets the whole thing run on any Linux machine, with nothing attached.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
core["fgc_core - no hardware, no SDKs<br/>Config · CaptureScheduler · TestRunner<br/>parsers for telemetry, DUMP and commands"]
|
||||||
|
core --> I1[IMotorController] & I2[IControlChannel] & I3[ICameraSource]
|
||||||
|
I1 --> R1[SerialMotorController] & M1[MockMotorController]
|
||||||
|
I2 --> R2[MqttControlChannel] & M2[NullControlChannel]
|
||||||
|
I3 --> R3["VimbaCameraSource<br/>(Allied Vision Vimba X)"] & M3[MockCameraSource]
|
||||||
|
R3 --> P[ImagePipeline] --> J[".jxl files + CamEvent"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Real on the left of each pair, stand-in on the right. There are stand-ins for
|
||||||
|
the orientation and ambient sensors too. The full design is in
|
||||||
|
[docs/architecture.md](docs/architecture.md).
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build # full build: needs Vimba X, fetches Paho MQTT
|
||||||
|
cmake --build build -j"$(nproc)" # -> build/fire_gimbal_control, build/fgc-config
|
||||||
|
|
||||||
|
cmake -B build -DWITH_VIMBA=OFF -DWITH_MQTT=OFF # no proprietary SDKs: stand-ins only
|
||||||
|
```
|
||||||
|
|
||||||
|
Dependencies (OpenCV, Boost, libjxl, the Vimba X SDK) are in
|
||||||
|
[docs/build-and-setup.md](docs/build-and-setup.md).
|
||||||
|
|
||||||
|
### Run with nothing attached
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build/fire_gimbal_control --mock-serial --mock-camera --no-mqtt --tui
|
||||||
|
```
|
||||||
|
|
||||||
|
The stand-in motor board, camera and broker behave like the real ones closely
|
||||||
|
enough to drive the dashboard, run a sweep and run most self-tests.
|
||||||
|
`[Features] mock_needcfg = true` makes the stand-in motor board ask for its
|
||||||
|
settings the way the real firmware does.
|
||||||
|
|
||||||
### Command-line options
|
### Command-line options
|
||||||
|
|
||||||
| Flag | Effect |
|
| Flag | Effect |
|
||||||
|------|--------|
|
|---|---|
|
||||||
| `-c, --config <path>` | Use a specific `config.ini` (otherwise the search path above). |
|
| `-c, --config <path>` | use this `config.ini` (otherwise `$FGC_CONFIG`, then `./config.ini`, ...) |
|
||||||
| `-i, --init` | Run the endstop-finding init sequence before entering the loop. |
|
| `--hardware <path>` | use this `hardware.ini` (otherwise `$FGC_HARDWARE`, then `/etc/fire_gimbal_control/hardware.ini`, then beside `config.ini`) |
|
||||||
| `-s, --start` | Begin capturing immediately on launch (else wait for the `start` command). |
|
| `-i, --init` | home the gimbal at start |
|
||||||
| `-d, --demo` | Demo mode: copy a placeholder `.jxl` instead of encoding live frames. |
|
| `-s, --start` | start sweeping at once |
|
||||||
| `--no-mqtt` | Disable MQTT; use a null control channel (overrides `enable_mqtt`). |
|
| `--test <selection>` | run one self-test and exit: 0 PASS, 4 FAIL, 5 could not start |
|
||||||
| `--mock-camera` | Use a simulated camera — no Vimba hardware needed. |
|
| `--tui` / `--no-tui` | the dashboard / the plain console (overrides `[UI] enable_tui`) |
|
||||||
| `--mock-serial` | Use a simulated motor controller — no serial hardware needed. |
|
| `--mock-serial`, `--mock-camera`, `--mock-env` | stand-ins instead of the motor board, camera, ambient sensor |
|
||||||
| `--tui` / `--no-tui` | Show the full-screen terminal dashboard / force the headless console (overrides `[UI] enable_tui`). |
|
| `--no-mqtt` | no broker |
|
||||||
| `--no-lock` | Skip the single-instance lock (dev only — for a second mock-only instance). |
|
| `--degraded` | start even though `hardware.ini` declares a part that is missing (field triage) |
|
||||||
| `--lock-file <path>` | Use a specific lockfile instead of the default per-uid path. |
|
| `-d, --demo` | save a placeholder picture instead of encoding live frames |
|
||||||
| `--log-level <lvl>` | `trace`, `debug`, `info`, `warn`, `error`, or `off`. |
|
| `--log-level <level>` | `trace`, `debug`, `info`, `warn`, `error`, `off` - for the screen |
|
||||||
| `--trace <cats>` | Verbatim wire tracing; comma list of `serial,mqtt,camera,control,all,none`. |
|
| `--log-file-level <level>`, `--no-log-file` | the log file's level / no log file this run |
|
||||||
| `-h, --help` | Print options and exit. |
|
| `--trace <parts>` | log every message to and from `serial`, `mqtt`, `camera`, `control`, `all` |
|
||||||
|
| `--no-lock`, `--lock-file <path>` | skip / move the single-instance lock (development only) |
|
||||||
|
| `-h, --help` | all of the above |
|
||||||
|
|
||||||
CLI flags override the matching `[Features]` / `[Logging]` keys in `config.ini`.
|
### Watching the wires
|
||||||
|
|
||||||
### Interactive commands
|
Two separate knobs control what is logged:
|
||||||
|
|
||||||
While running, the program reads commands from stdin (one per line):
|
- **the level** (`--log-level`, `debug`, `[Logging] level`) - how much ordinary
|
||||||
|
detail. At `info`, a running sweep is nearly silent;
|
||||||
|
- **traces** (`--trace`, `trace <part>`, `[Logging] trace`) - every single
|
||||||
|
message exchanged with a part, word for word, whatever the level. Off by
|
||||||
|
default; the camera one is busy.
|
||||||
|
|
||||||
| Command | Action |
|
```text
|
||||||
|---------|--------|
|
|
||||||
| `start` / `stop` | Start / stop the capture cycle (the gimbal stays powered and homed). |
|
|
||||||
| `gimbal move <yaw>,<pitch>` | Aim at an absolute **true bearing** (0 = north) and elevation, in degrees. |
|
|
||||||
| `gimbal home [y\|p]` | Run the endstop-finding home sequence. |
|
|
||||||
| `gimbal dump` | Request a firmware state dump (shown in the gimbal `g` view). |
|
|
||||||
| `gimbal diag [y\|p\|all]` | Run the firmware motor self-test (results in the activity strip + `logs/`). |
|
|
||||||
| `gimbal heading setup` | Tie yaw to true north by sighting known landmarks (TUI key `H`; **no IMU needed**). |
|
|
||||||
| `gimbal heading offset <deg>` | Shift the whole yaw map by a known amount, and save it. |
|
|
||||||
| `gimbal pitch setup` | Level pitch against gravity (auto-homes if needed; **needs the IMU**). |
|
|
||||||
| `snapshot` | Take one image now and publish its CamEvent (TUI: `Space`; manual mode only). |
|
|
||||||
| `gimbal …` | Other motor controls — `steps`/`nudge`/`jog`/`stop`/`speed`/`reset`/`enable`/`disable`/`setpos`/`status`. |
|
|
||||||
| `gimbal raw "<command>"` | **Low-level:** send the quoted text to the firmware verbatim (newline added). Bypasses wrappers/unit conversion — use with care. |
|
|
||||||
| `debug` | Toggle debug-level logging on/off. |
|
|
||||||
| `set fps <n>` | Capture rate, in images per second. |
|
|
||||||
| `set camera fps <n>` | Camera sensor frame rate. |
|
|
||||||
| `set camera profile [<cam>] <name>` | Switch a camera's profile (any `<name>.xml` in its `profile_dir`). |
|
|
||||||
| `set camera jxlq <d>` | JPEG XL quality as butteraugli **distance** (lower = higher quality / larger files). |
|
|
||||||
| `set camera jxle <n>` | JPEG XL encoder effort (higher = slower, smaller). |
|
|
||||||
| `set camera display <0\|1>` | Toggle the local preview window. |
|
|
||||||
| `trace <cat> [on\|off]` | Enable/disable a wire-trace category live (e.g. `trace serial`, `trace mqtt off`). |
|
|
||||||
| `trace all` / `trace off` | Enable every category / silence all of them. |
|
|
||||||
| `help [topic]` | List commands / expand one section. |
|
|
||||||
| `exit` | Stop everything and quit (Ctrl-D also works). |
|
|
||||||
|
|
||||||
**Image capture** runs the camera continuously and keeps one frame per waypoint. The camera free-runs
|
|
||||||
at the active mode's rate with its own auto-exposure tracking the light; almost every frame is
|
|
||||||
discarded, and their job is simply to keep that exposure controller converged. At each waypoint the
|
|
||||||
gimbal settles, holds still for the mode's `settle_wait_ms`, and the newest frame is kept — so the saved
|
|
||||||
image was exposed at rest and already correctly metered, with nothing waiting on a convergence loop.
|
|
||||||
|
|
||||||
Which rate and which exposure limits apply comes from a **camera profile**: a settings XML exported from
|
|
||||||
the VimbaX Viewer and handed straight to the Vimba X SDK, so the camera gets exactly the combination the
|
|
||||||
GUI validated. `standard` runs 10 Hz for daylight, `night` runs 1 Hz with long exposures; switch live with
|
|
||||||
`set camera profile <name>`. Details in
|
|
||||||
[docs/architecture.md](docs/architecture.md#image-capture); the mode-file format is `[Camera]` in
|
|
||||||
[docs/configuration.md](docs/configuration.md).
|
|
||||||
|
|
||||||
The **Xsens MTi IMU** (set `[Features] enable_imu` + `[IMU] device`) drives the Sensors panel's live
|
|
||||||
roll/pitch/yaw and powers `gimbal pitch setup`; the **SHT41** (`[Features] enable_env` + `[Env] i2c_device`,
|
|
||||||
or `--mock-env`) supplies the panel's ambient temperature and humidity — the MTi's device-internal
|
|
||||||
temperature is expanded-view-only, so the main window shows one unambiguous temperature. In the TUI,
|
|
||||||
`g` and `i` open full-screen **gimbal** and **sensors** views (the latter covering both the MTi and
|
|
||||||
the SHT41, each reading labelled with its source), and an **activity strip** below the log shows the
|
|
||||||
running calibration/diagnostics/scan plus the last result. See [docs/configuration.md](docs/configuration.md).
|
|
||||||
|
|
||||||
A typical first bring-up: `scripts/run.sh --init` to home the gimbal, then type `start` once you've confirmed
|
|
||||||
telemetry looks sane; adjust `set fps` / `set camera jxlq` live as needed. See
|
|
||||||
[docs/configuration.md](docs/configuration.md) for the full `config.ini` reference.
|
|
||||||
|
|
||||||
### Verbosity & wire tracing
|
|
||||||
|
|
||||||
Two independent knobs control output:
|
|
||||||
|
|
||||||
- **Log level** (`--log-level`, the `debug` console toggle, `[Logging] level`) gates ordinary
|
|
||||||
messages: `info` (default) shows discrete events only; `debug` adds per-image saves; `trace` is
|
|
||||||
the most verbose. At `info` an active capture is nearly silent.
|
|
||||||
- **Wire-trace categories** (`--trace`, the `trace` console command, `[Logging] trace`) log
|
|
||||||
**every** message exchanged with a subsystem, verbatim, and are **independent of the level** —
|
|
||||||
`--trace serial` shows all firmware serial traffic even at `info`. Categories are **off by
|
|
||||||
default** (the camera one is high-rate). Only `--log-level off` silences them.
|
|
||||||
|
|
||||||
Lines are tagged by category with a `TX`/`RX` direction, so a single subsystem is easy to follow
|
|
||||||
or `grep`:
|
|
||||||
|
|
||||||
```
|
|
||||||
14:02:01.234 [SERIAL] TX MOVE -90,0
|
14:02:01.234 [SERIAL] TX MOVE -90,0
|
||||||
14:02:01.250 [SERIAL] RX ST Y:A,982,969,80084000,0,8,8,Se P:A,...
|
14:02:01.250 [SERIAL] RX ST Y:A,982,969,80084000,0,8,8,Se P:A,...
|
||||||
14:02:02.000 [MQTT] PUB GGS/FWT/Tower/StatusCode 0
|
14:02:02.000 [MQTT] PUB GGS/FWT/Tower/StatusCode 0
|
||||||
14:02:02.500 [CAMERA] TX trigger cam0
|
14:02:02.500 [CAMERA] TX trigger cam0
|
||||||
14:02:02.910 [CAMERA] RX frame cam0 1936x1216 7064576B
|
14:02:02.910 [CAMERA] RX frame cam0 1936x1216 7064576B
|
||||||
14:02:02.000 [CONTROL] sweep -> grid yaw=-90 pitch=0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Example — watch only firmware serial traffic on the device:
|
### Tests
|
||||||
`./deploy.sh --run` with `RUN_ARGS="--start --trace serial"`.
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cmake -B build -DWITH_VIMBA=OFF -DWITH_MQTT=OFF -DBUILD_TESTING=ON
|
cmake -B build-test -DWITH_VIMBA=OFF -DWITH_MQTT=OFF -DBUILD_TESTING=ON
|
||||||
cmake --build build
|
cmake --build build-test -j"$(nproc)"
|
||||||
ctest --test-dir build --output-on-failure
|
ctest --test-dir build-test --output-on-failure
|
||||||
```
|
```
|
||||||
|
|
||||||
The unit tests cover the core (config, paths, telemetry/command parsers, capture scheduler) and need no SDKs.
|
Over 360 unit tests, needing no hardware and no SDKs: configuration and the two
|
||||||
|
files, the parsers, the capture scheduler, the self-test checks against the
|
||||||
|
stand-ins, the firmware handshake, the camera checks, the config-file editor.
|
||||||
|
|
||||||
## Deploy to the LattePanda
|
### Repository layout
|
||||||
|
|
||||||
The LattePanda is the machine wired to the gimbal and camera, so it builds and runs there. `deploy.sh`
|
```
|
||||||
rsyncs the codebase over ssh and runs the cmake build remotely (mirrors `../firmware/deploy.sh`).
|
CMakeLists.txt build (WITH_VIMBA, WITH_MQTT, WITH_TUI, BUILD_TESTING)
|
||||||
|
main.cpp entry point: command line -> Application
|
||||||
```bash
|
include/fgc/ headers: the interfaces, config, scheduler, tests, ...
|
||||||
cp .deploy.env.example .deploy.env # then edit REMOTE_HOST / REMOTE_DIR
|
mock/ the stand-in implementations
|
||||||
./deploy.sh --check-deps # verify the device has OpenCV/Boost/libjxl
|
src/
|
||||||
./deploy.sh # rsync + remote configure + build
|
core/ the core: Application, config, scheduler, parsers, tests, ...
|
||||||
./deploy.sh --run # ... then run it over ssh (RUN_ARGS, ctrl-c to stop)
|
camera/ Vimba X camera, image pipeline, JPEG XL encoder
|
||||||
./deploy.sh --clean --run # wipe remote build dir first, then build + run
|
serial/ motor board and orientation-sensor links
|
||||||
|
mqtt/ MQTT control channel
|
||||||
|
sensors/ ambient sensor (SHT41) over I2C
|
||||||
|
ui/ the dashboard and the plain console
|
||||||
|
tests/ unit tests (doctest)
|
||||||
|
tools/fgc_config.cpp fgc-config: edits config.ini / hardware.ini safely
|
||||||
|
config/ config.example.ini, hardware.example.ini, scan grids, camera profiles
|
||||||
|
hardware/ per-board setup kits (LattePanda IOTA, LattePanda 3 Delta)
|
||||||
|
scripts/ fgc, install-tower, the boot service, camera-profile export
|
||||||
|
docs/ everything longer than a README
|
||||||
|
ini.c, ini.h the bundled INI parser (inih)
|
||||||
```
|
```
|
||||||
|
|
||||||
`config.ini` and `images/` are device-local and never overwritten by a deploy; the first deploy seeds
|
---
|
||||||
`config.ini` from `config/config.example.ini` if the device has none. One-time device prerequisites
|
|
||||||
(Ubuntu): `sudo apt-get install -y build-essential cmake git libopencv-dev libboost-program-options-dev
|
|
||||||
libjxl-dev`, plus the Vimba X SDK under `/opt/VimbaX` for `-DWITH_VIMBA=ON`.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
| Document | Contents |
|
| Document | Read it for |
|
||||||
|----------|----------|
|
|---|---|
|
||||||
| [docs/architecture.md](docs/architecture.md) | Components, interfaces, threading, data flow, capture state machine |
|
| [docs/tower-installation.md](docs/tower-installation.md) | installing a tower, step by step, and what each step is for |
|
||||||
| [docs/build-and-setup.md](docs/build-and-setup.md) | CMake, dependencies, options, Vimba X / Paho, host setup |
|
| [docs/deployment.md](docs/deployment.md) | running and updating a tower: `deploy.sh`, `fgc`, the boot service, the log, troubleshooting |
|
||||||
| [docs/deployment.md](docs/deployment.md) | Running and updating a tower: `deploy.sh`, `scripts/fgc`, the boot service, the log |
|
| [docs/configuration.md](docs/configuration.md) | every key in `config.ini`, every flag, every command |
|
||||||
| [docs/configuration.md](docs/configuration.md) | `config.ini` keys, CLI flags, console commands |
|
| [docs/test-command.md](docs/test-command.md) | the self-tests: groups, profiles, reports, baselines |
|
||||||
| [docs/mqtt-api.md](docs/mqtt-api.md) | MQTT topic catalog and payloads |
|
| [docs/mqtt-api.md](docs/mqtt-api.md) | the MQTT topics and what each message contains |
|
||||||
| [docs/test-command.md](docs/test-command.md) | The `test` hardware self-test suite: subsystems, profiles, reports, baselines |
|
| [docs/known-issues.md](docs/known-issues.md) | what is known to be wrong or unfinished |
|
||||||
| [docs/modules-reference.md](docs/modules-reference.md) | Per-file reference and data structures |
|
| [docs/architecture.md](docs/architecture.md) | the design: parts, threads, data flow, the capture cycle |
|
||||||
| [docs/known-issues.md](docs/known-issues.md) | Status of past issues + remaining caveats |
|
| [docs/modules-reference.md](docs/modules-reference.md) | file by file |
|
||||||
| [docs/roadmap.md](docs/roadmap.md) | Forward-looking project roadmap |
|
| [docs/build-and-setup.md](docs/build-and-setup.md) | building, dependencies, the camera SDK |
|
||||||
| [docs/hardware-config-plan.md](docs/hardware-config-plan.md) | Splitting `hardware.ini` out of `config.ini`, and the firmware config handshake |
|
| [docs/hardware-config-plan.md](docs/hardware-config-plan.md) | why `hardware.ini` exists, and the firmware handshake |
|
||||||
| [docs/hardware-porting-plan.md](docs/hardware-porting-plan.md) | Porting the tower to the LattePanda IOTA / RP2040 |
|
| [docs/hardware-porting-plan.md](docs/hardware-porting-plan.md) | moving towers to the LattePanda IOTA |
|
||||||
| [docs/lattepanda-iota.md](docs/lattepanda-iota.md) | Hardware facts for the IOTA board |
|
| [docs/lattepanda-iota.md](docs/lattepanda-iota.md) | the IOTA board |
|
||||||
| [docs/test-roadmap.md](docs/test-roadmap.md) | Planned tests blocked on not-yet-integrated components |
|
| [docs/roadmap.md](docs/roadmap.md), [docs/test-roadmap.md](docs/test-roadmap.md) | what comes next |
|
||||||
|
|
||||||
## Repository layout
|
---
|
||||||
|
|
||||||
```
|
|
||||||
CMakeLists.txt Build (options: WITH_VIMBA, WITH_MQTT, BUILD_TESTING)
|
|
||||||
cmake/ FindVmb.cmake (Vimba X), Paho.cmake (FetchContent)
|
|
||||||
config/ config.example.ini, scan.csv (real config.ini is gitignored)
|
|
||||||
include/fgc/ Public headers: interfaces, Config, Logger, scheduler, impls
|
|
||||||
mock/ Mock/null implementations
|
|
||||||
src/
|
|
||||||
core/ Config, Logger, Paths, parsers, Geometry, ScanGrid, CaptureScheduler, Application
|
|
||||||
serial/ SerialMotorController
|
|
||||||
mqtt/ MqttControlChannel
|
|
||||||
camera/ VimbaCameraSource, ImagePipeline, JpegXlEncoder
|
|
||||||
main.cpp Thin entry point (CLI -> Application)
|
|
||||||
tests/ doctest unit tests
|
|
||||||
scripts/ run.sh + systemd unit template
|
|
||||||
ini.c / ini.h Bundled inih INI parser
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ownership
|
## Ownership
|
||||||
|
|
||||||
Internal tooling for the GGS fire-watch tower network. No license file is present in the repository.
|
Internal tooling for the GGS fire-watch tower network. No license file is
|
||||||
|
present in the repository.
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 119 KiB |