From 50273779a3db0e877f58c2c0858519363fc972a9 Mon Sep 17 00:00:00 2001 From: pgdalmeida Date: Fri, 25 Sep 2026 14:59:32 +0200 Subject: [PATCH] Rewrite the README around running a tower, with screenshots and diagrams --- README.md | 708 +++++++++++++++++++--------- docs/images/readme/camera.png | Bin 0 -> 136726 bytes docs/images/readme/connectivity.png | Bin 0 -> 238151 bytes docs/images/readme/console.png | Bin 0 -> 306764 bytes docs/images/readme/dashboard.png | Bin 0 -> 187463 bytes docs/images/readme/gimbal.png | Bin 0 -> 183274 bytes docs/images/readme/help.png | Bin 0 -> 216707 bytes docs/images/readme/sensors.png | Bin 0 -> 122070 bytes 8 files changed, 498 insertions(+), 210 deletions(-) create mode 100644 docs/images/readme/camera.png create mode 100644 docs/images/readme/connectivity.png create mode 100644 docs/images/readme/console.png create mode 100644 docs/images/readme/dashboard.png create mode 100644 docs/images/readme/gimbal.png create mode 100644 docs/images/readme/help.png create mode 100644 docs/images/readme/sensors.png diff --git a/README.md b/README.md index 4749299..30c7cf6 100644 --- a/README.md +++ b/README.md @@ -1,269 +1,557 @@ # Fire Gimbal Control -Real-time control software for an automated **fire-watch gimbal**. A C++17 program runs on a tower-mounted -PC, rotates a pan gimbal carrying up to four industrial cameras, captures a 360° panorama for wildfire -detection, compresses each frame to JPEG XL, and reports to a ground station over MQTT. +**The program that runs a fire-watch tower.** It points a camera around the +horizon on a motorised gimbal, keeps one well-exposed picture at every stop, +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`. +![The dashboard of tower 111, live](docs/images/readme/dashboard.png) +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. -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 -the filesystem as `.jxl`, and telemetry flows over MQTT. There is no database. +## Contents -## 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 │ - └───────▲─────────────────▲──────────────────▲────────────────────────┘ - │ IMotorController │ IControlChannel │ ICameraSource - ┌───────────┴──────┐ ┌────────┴─────────┐ ┌──────┴────────────────┐ - real │ SerialMotorCtrl │ │ MqttControlChannel│ │ VimbaCameraSource │ (WITH_VIMBA/WITH_MQTT) - mock │ MockMotorCtrl │ │ NullControlChannel│ │ MockCameraSource │ - └──────────────────┘ └──────────────────┘ └──────────┬────────────┘ - frames - ▼ - ImagePipeline → .jxl + CamEvent +--- + +## What it does + +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 +(**pitch**). This program, `fire_gimbal_control`, runs on the tower's +LattePanda PC and does the watching: + +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 + 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
what the tower IS"] --> APP + CFG["config.ini
what the tower DOES"] --> APP + APP["fire_gimbal_control
on the LattePanda"] -- "USB serial
commands + settings" --> MCU["motor board
firmware"] + MCU -- "status, 4x a second" --> APP + MCU -- SPI --> DRV["TMC5160
stepper drivers"] --> MOT["yaw + pitch
motors"] + CAM["camera
(USB3)"] -- frames --> APP + APP --> JXL["pictures
.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,
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
(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
to the waypoint"] + B --> C{"both axes
stopped?"} + C -- no --> C + C -- yes --> D["hold still for settle_wait_ms
(1 s by day, 2.5 s at night)"] + D --> E["keep the newest frame
the camera delivered"] + E --> F["encode to JPEG XL
save to the output folder"] + F --> G["publish a CamEvent
(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 -cmake -B build # configure (needs the Vimba X SDK + Paho for a full build) -cmake --build build # -> build/fire_gimbal_control - -# Development build with NO proprietary SDKs (mocks only): -cmake -B build -DWITH_VIMBA=OFF -DWITH_MQTT=OFF -cmake --build build +scripts/fgc start --init --start # start it: home the gimbal, then sweep +scripts/fgc attach # open the dashboard (Ctrl-b d to leave it running) +scripts/fgc status # is it running? since when? which PID? +scripts/fgc peek # a text snapshot of the dashboard, without attaching +scripts/fgc stop # a clean shutdown - camera and motors stopped properly +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 - -On a tower the program must outlive the ssh session that started it, so **`scripts/fgc`** runs it -detached inside a tmux session: +**At boot**, a tower with the boot service starts itself the same way - through +`fgc`, so the dashboard is still there to attach to: ```bash -scripts/fgc start --start # start detached; returns immediately -scripts/fgc attach # watch the dashboard — Ctrl-b d leaves it running -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 +sudo systemctl status fire-gimbal-control # is the service running? +sudo systemctl restart fire-gimbal-control # restart it (it homes the gimbal again) ``` -Disconnecting from ssh while attached — or closing the laptop — is invisible to the program: tmux owns -the pty. **When attached, the dashboard's `q` key exits the program; `Ctrl-b d` is what detaches.** +`--init` homes the gimbal at every start, which a tower in service needs: the +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` -and `--stop` drive it afterwards without re-syncing. +**Only one copy can run at a time.** Two would fight over the motor board and +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 -`/run/user//fire_gimbal_control.lock` (or `/tmp/fire_gimbal_control-.lock`) at startup and -exits with status 2 and `another instance is already running (PID N)` if it is taken — two instances -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: +**The log** is kept per run in +`~/.local/share/fire_gimbal_control/logs/fgc_-