473 lines
25 KiB
Markdown
473 lines
25 KiB
Markdown
# Hardware Configuration Plan
|
||
|
||
Splitting `config.ini` by lifetime, introducing an assembly-time `hardware.ini`, and letting the firmware
|
||
ask the host for the parameters it needs instead of carrying its own copies.
|
||
|
||
Draft for review, 2026-09-11. Covers `software/` and `firmware/`.
|
||
|
||
**This work stands on its own.** It fixes a live bug (§5), removes a class of silent degradation (§10)
|
||
and is worth doing whether or not the LattePanda IOTA port goes ahead. It is also a **prerequisite** for
|
||
that port — see [hardware-porting-plan.md](hardware-porting-plan.md).
|
||
|
||
## 0. The bindings this has to capture
|
||
|
||
Read live from the tower (`ggs@10.11.12.111`), not from the docs — the board is a **LattePanda 3 Delta**,
|
||
not the Sigma `docs/deployment.md:3` claims:
|
||
|
||
| Function | Binding | Backing hardware |
|
||
|---|---|---|
|
||
| Motor MCU | `/dev/serial/by-id/usb-Arduino_LLC_Arduino_Leonardo-if00` | USB `2341:8036`, onboard ATmega32U4 |
|
||
| IMU (Xsens MTi) | `/dev/ttyS0` | onboard RS-232 header |
|
||
| Ambient (SHT41) | `/dev/i2c-2` @ `0x44` | Synopsys DesignWare LPSS I2C |
|
||
| Camera | `DEV_1AB22C049CD1` | USB3 `1ab2:0001` Alvium 1800 U-2040c |
|
||
| Modem | `/dev/ttyUSB0-3` | USB `2c7c:0801` Quectel RM520N-GL, M.2 B-key |
|
||
|
||
Multiple rigs are in production, **mostly without an IMU**, so every change here has to be invisible on
|
||
rollout across a live mixed fleet.
|
||
|
||
---
|
||
|
||
|
||
## 1. Why split at all
|
||
|
||
The organising principle is **who writes this, and how often**:
|
||
|
||
| | Author | Changes | Goes to |
|
||
|---|---|---|---|
|
||
| **Assembly** | a human, once | only if someone physically changes the machine | `hardware.ini` |
|
||
| **Operation** | a human, continuously | whenever you tune | `config.ini` |
|
||
| **Calibration** | the program, on operator confirmation | from time to time — it assigns real-world orientation and drifts out of true | `config.ini` |
|
||
|
||
Three things it buys:
|
||
|
||
1. **Building a new tower is writing one file.**
|
||
2. **It survives deploys structurally.** `deploy.sh:234` keeps `config.ini` out of rsync by an exclude
|
||
rule someone has to maintain; `deploy.sh:227` records that `--delete` already destroyed a freshly
|
||
exported camera XML once. A file outside the deploy tree cannot be lost that way.
|
||
3. **It gives every fact exactly one home** — which is A.5 and A.6, and the largest part of this work.
|
||
|
||
## 2. Two files
|
||
|
||
```
|
||
/etc/fire_gimbal_control/hardware.ini # assembly — human, frozen, never deployed
|
||
<deploy tree>/config.ini # operation + calibration
|
||
```
|
||
|
||
Calibration stays in `config.ini`: it changes over time, so it is not assembly data. `--push-config`
|
||
stays as it is — a dev convenience for the tower under development.
|
||
|
||
## 3. What goes in `hardware.ini`
|
||
|
||
```ini
|
||
[Board]
|
||
model = lattepanda-3-delta ; or: lattepanda-iota
|
||
firmware_env = leonardo ; or: iota_rp2040
|
||
|
||
[Hardware]
|
||
axes = yaw, pitch ; or: yaw
|
||
imu = none ; or: mti — most of the fleet is 'none'
|
||
env = sht41 ; or: none
|
||
cameras = rgb
|
||
|
||
[Serial]
|
||
device = /dev/serial/by-id/usb-Arduino_LLC_Arduino_Leonardo-if00
|
||
[IMU]
|
||
device = /dev/ttyS0
|
||
[Env]
|
||
i2c_device = /dev/i2c-2
|
||
i2c_addr = 0x44
|
||
[Camera.rgb]
|
||
id = DEV_1AB22C049CD1
|
||
|
||
; ── Drivetrain. A different motor or gear ratio changes all of these. ──
|
||
[Motor.yaw]
|
||
steps_per_rev = 177000 ; full mechanical travel reference
|
||
global_scaler = 80 ; TMC GLOBALSCALER — motor current
|
||
enc_deviation = 1500 ; yaw homing stall threshold (see A.5)
|
||
sw_mode = 0x08A0 ; hardware stop switches off (no physical endstops)
|
||
normal_speed = 30000
|
||
homing_speed = 12000
|
||
center_speed = 25000
|
||
soft_margin = 5000
|
||
|
||
[Motor.pitch]
|
||
steps_per_rev = 500000
|
||
global_scaler = 50
|
||
sw_mode = 0x08A3 ; endstops enabled
|
||
normal_speed = 60000
|
||
homing_speed = 150000
|
||
center_speed = 60000
|
||
soft_margin = 5000
|
||
```
|
||
|
||
`tower_name` belongs here too — assigned once, at assembly.
|
||
|
||
## 4. How the files find each other
|
||
|
||
**Recommendation: a fixed location, not a pointer in `config.ini`.**
|
||
|
||
```
|
||
--hardware <path> # explicit, for dev and testing
|
||
$FGC_HARDWARE # service environment
|
||
/etc/fire_gimbal_control/hardware.ini # the normal answer on a tower
|
||
<dir of config.ini>/hardware.ini # fallback, so a dev checkout just works
|
||
```
|
||
|
||
with the resolved path logged at startup. **`/etc/fire_gimbal_control/` is already your convention** —
|
||
`deployment.md §4` puts `credentials.env` there.
|
||
|
||
## 5. The duplication, and the bug it is already causing
|
||
|
||
> ### ⚠️ The host is silently undoing the firmware's speed reduction
|
||
> `Application.cpp:1040–1041`, at the end of every `--init`, unconditionally sends:
|
||
> ```
|
||
> SPEED Y 50000
|
||
> SPEED P 150000
|
||
> ```
|
||
> Hardcoded in host source. Meanwhile `firmware/src/config.h` sets `normal_speed` to **30000** for yaw —
|
||
> commented *"gentler top speed (was 50000)"* — and **60000** for pitch — *"(was 150000)"*.
|
||
>
|
||
> **The host sends exactly the old values the firmware deliberately moved away from.** The reduction has
|
||
> been clobbered on every init since it was made. Live on the fleet today.
|
||
|
||
Today the same fact exists in up to three places: `hardware.ini` (proposed), the firmware's compile-time
|
||
`MOTOR_CONFIG` table, and host source. The fix is not to synchronise them but to **delete two of them**.
|
||
|
||
### The gear ratio reaches further than speeds
|
||
|
||
Three more values change with the new yaw drivetrain, and none is a speed:
|
||
|
||
- **`enc_deviation` is the one that bears on damage.** Yaw homing detects the endstop when
|
||
`|XACTUAL − XENC|` exceeds it. `firmware/docs/hardware-notes.md` records the free-running gap as ~500
|
||
against a limit of 1500, and that *"lower = snappier (less grind)"*. **A different gear ratio changes
|
||
that free-running gap**, so it must be re-measured on the new tower — too low and homing false-triggers
|
||
mid-travel, too high and it grinds longer before noticing. Tuning homing speed alone solves half of it.
|
||
- **`steps_per_rev`** is what `HOMING_MIN_TRAVEL_SLACK` validates each home against.
|
||
- **`yaw_counts_per_deg` must be recalibrated.** The 983.33 default is derived from
|
||
`steps_per_rev = 177000`; on a different ratio it is wrong, so `gimbal heading setup` is mandatory.
|
||
|
||
## 6. The config handshake — the firmware asks
|
||
|
||
**The firmware should declare what it needs and refuse to move until it has it.** That removes the
|
||
firmware's parameter table as a competing definition, rather than trying to keep it in sync.
|
||
|
||
### Why this is a small change, not a new architecture
|
||
|
||
Both halves already exist:
|
||
|
||
- **The firmware already treats some parameters as host-owned.** `motor.h:70` reads *"host-owned homing
|
||
parameters (config.ini, sent as CENTER / MARGIN)"*.
|
||
- **The host already verifies the firmware adopted them.** `Application.cpp:1099` errors with *"the
|
||
firmware did not adopt CENTER (is it older than this host?) … Refusing to derive travel limits;
|
||
headings would be wrong"*, and `:1108` warns *"firmware reported no soft_margin (update firmware)"*.
|
||
|
||
So the pattern, the direction of ownership, and even the version-skew handling are established. What is
|
||
missing is that the firmware **guesses** when the host says nothing, instead of asking.
|
||
|
||
### The sequence
|
||
|
||
1. **Boot: pins and SPI up, then stop the motors.** Unconditional, no config needed — see the safety
|
||
note below.
|
||
2. **State `NEEDCFG`.** Motors stay disabled; `HOME` / `MOVE` / `ENABLE` are rejected with
|
||
`ERR need config`. Reported in `ST` and `DUMP`, so the dashboard shows *why* nothing moves.
|
||
3. **On host connection, ask.** `main.cpp` already reprints the boot banner whenever the host opens the
|
||
port — the natural hook. Emit alongside it:
|
||
```
|
||
BOOT build=abc123
|
||
NEED Y steps_per_rev global_scaler sw_mode enc_deviation normal_speed homing_speed center_speed center margin
|
||
NEED P ...
|
||
```
|
||
4. **Host answers from `hardware.ini`** with a generic `SET <axis> <key> <value>`. `SPEED`, `CENTER` and
|
||
`MARGIN` stay as aliases so old hosts and hand-typed sessions keep working.
|
||
5. **When an axis's required set is complete**, the firmware applies the parameterised init block and
|
||
moves that axis to `ST_RESET`. `DUMP` reports every value it is holding, so the existing adoption
|
||
check generalises from "did CENTER land?" to "does the firmware hold what `hardware.ini` says?" —
|
||
which is `test hardware motor` in A.9.
|
||
|
||
### ⚠️ The safety constraint this must not break
|
||
|
||
`applyInitBlock` ends with, and `hardware-notes.md` explains why:
|
||
|
||
```cpp
|
||
tmc_.write(TMC_VMAX, 0);
|
||
tmc_.write(TMC_RAMPMODE, RAMP_VELOCITY_POS); // stop residual motion
|
||
tmc_.write(TMC_GSTAT, 0x07); // clear power-up latches
|
||
```
|
||
|
||
*"The TMC has its own supply and keeps running across an MCU-only reset (e.g. a re-flash), so it may
|
||
still be executing a velocity command from the previous session."* If the firmware waited for host
|
||
config before touching the driver, **a re-flash could leave the motor grinding until the host connects.**
|
||
|
||
So split the init block in two:
|
||
|
||
| Phase | Contents | When |
|
||
|---|---|---|
|
||
| **Safety init** | `VMAX=0` + velocity mode, `GSTAT` clear, and the constant register writes | at boot, unconditionally |
|
||
| **Parameterised init** | `GLOBALSCALER`, `XACTUAL = center`, `SW_MODE`, `ENC_DEVIATION` | once `NEEDCFG` is satisfied |
|
||
|
||
Those four are exactly the writes in `applyInitBlock` that read from `cfg_` or a tunable constant; the
|
||
rest are already fixed values.
|
||
|
||
### Standalone / bench operation
|
||
|
||
`hardware-notes.md`'s bring-up checklist drives the firmware by hand over `deploy.sh --monitor`, so a
|
||
firmware that refuses to move without a host would break it. Add an explicit
|
||
**`USE DEFAULTS`** command that loads a built-in **limp-home** set and logs loudly.
|
||
|
||
The distinction matters: limp-home values are **deliberately conservative — slow speeds, generous
|
||
margins — and never intended to match any real tower.** They are not a second definition of the
|
||
drivetrain, which is what today's `normal_speed = 30000` is. Nothing reaches production on them, and
|
||
`test hardware motor` fails if a tower is running them.
|
||
|
||
**Prefer this to a timeout fallback.** A firmware that waits *n* seconds and then adopts defaults means a
|
||
slow host silently produces a tower running on the wrong values — precisely the silent-degradation class
|
||
this whole design is removing. Make it an explicit act.
|
||
|
||
### What remains compile-time
|
||
|
||
Only **pin assignments** and the **axis count**: pins because SPI must be up before anything can be
|
||
asked or answered, the axis count because it sizes the axis array. Both are still defined once in
|
||
`hardware.ini` and *generated* into `firmware/src/config.h` at build — one definition, realised at build
|
||
time instead of over the wire. Everything else becomes runtime, which shrinks the build-time set far
|
||
below what I proposed last turn.
|
||
|
||
### Rollout order matters
|
||
|
||
A new firmware sitting in `NEEDCFG` in front of an old host would never be configured. So: **host first,
|
||
then firmware.** The host should treat a firmware that never sends `NEED` as an old one and fall back to
|
||
today's push-and-verify path, logging that it did.
|
||
|
||
## 7. Build identity in the TUI
|
||
|
||
No version negotiation — firmware and software are deployed in tandem. This section is display only.
|
||
|
||
### What already exists
|
||
|
||
Almost all of it. Both halves stop one step short.
|
||
|
||
**Firmware build id — travels end to end and is already on screen:**
|
||
|
||
- `firmware/platformio.ini` injects `GIT_REV` at build time, *"so the flashed firmware carries a build
|
||
identity"*, from `git describe --always --dirty`.
|
||
- The firmware reports it in `BOOT build=<rev>` (`main.cpp:39`) and `DUMP BEGIN build=<rev>` (`:103`).
|
||
- The host parses it into `DumpData.build` (`DumpParser.h:46`) and **already renders it in the gimbal
|
||
pane** — `TuiUi.cpp:999` draws `build <rev> up <ms> reset:<x>` on that overlay's header row.
|
||
|
||
Nothing to do here. It works today.
|
||
|
||
**Software build id — the slot exists and is filled with a placeholder:**
|
||
|
||
- `HeaderView` has a `build` field (`UiSnapshot.h:185`), and `TuiUi.cpp:1324` already renders
|
||
`text("build " + s.header.build) | dim` in the main header, right after the tower name — exactly where
|
||
you suggested putting it.
|
||
- `deploy.sh:212` computes `GIT_REV="$(git describe --always --dirty)"` and `:288` passes it into the
|
||
remote CMake invocation.
|
||
- But **`CMakeLists.txt` never reads `$ENV{GIT_REV}`**, so the value is passed and dropped, and
|
||
`Application.cpp:610` hardcodes `s.header.build = "dev"`.
|
||
|
||
Two edits close it:
|
||
|
||
1. `CMakeLists.txt` reads `$ENV{GIT_REV}` into a compile definition, defaulting to `"unknown"` — exactly
|
||
as `firmware/src/main.cpp:14` already does.
|
||
2. `Application.cpp:610` uses it instead of the `"dev"` literal.
|
||
|
||
```
|
||
FIREWATCH TOWER NorthRidge build 1a572cb LIVE up 412s
|
||
```
|
||
|
||
### One log line at startup
|
||
|
||
So an unattended tower records what it is running, where `journalctl` will find it. There is already a
|
||
house pattern — `Application.cpp:526` logs the IMU's identity the same way:
|
||
|
||
```
|
||
INFO Firmware: build a1b2c3d (from the BOOT banner, once, at connection)
|
||
```
|
||
|
||
Identity only. Nothing compares it to anything.
|
||
|
||
> **Two small notes.** The repo has **no tags**, so `git describe --always --dirty` currently yields a
|
||
> bare hash (`1a572cb-dirty`) — an honest build identity, but not a version. Tagging releases would make
|
||
> the same line read `v1.4.0-2-g1a572cb` at no extra cost, if you ever want it. And `$ENV{GIT_REV}` is
|
||
> read at CMake **configure** time, so a plain `cmake --build` after edits keeps the configure-time
|
||
> value; `deploy.sh:288` reconfigures on every deploy, so the deployed path stays fresh — and this
|
||
> matches how the firmware already behaves.
|
||
|
||
### Why no compatibility check
|
||
|
||
A maintained protocol number is one more thing to remember to bump, and a **forgotten bump is worse than
|
||
no number** — it asserts a compatibility that was never verified. The cases it would have caught are
|
||
already covered, behaviourally:
|
||
|
||
- **The handshake describes its own generation.** A firmware that never sends `NEED` (§6) is an old
|
||
one; the host falls back to today's push-and-verify path on that evidence alone. No number needed.
|
||
- **`test hardware motor`** (§11) asks the firmware to `DUMP` and confirms it is actually holding the
|
||
`hardware.ini` values — what the firmware *did*, not what it *claimed*.
|
||
- **`Application.cpp:1101` and `:1108`** already detect the two concrete incompatibilities anyone has hit
|
||
so far, and keep doing so unchanged.
|
||
|
||
## 8. Generated at install, not hand-written from scratch
|
||
|
||
```
|
||
hardware/
|
||
lattepanda-3-delta/
|
||
packages.txt # apt packages
|
||
preconditions/ # udev rules, sysctl.d, kernel-cmdline snippet
|
||
install.sh # apply preconditions; PROBE the device and emit hardware.ini
|
||
NOTES.md # quirks, verified-on date
|
||
lattepanda-iota/
|
||
...
|
||
```
|
||
|
||
`install.sh` fills in what it can detect — board model from DMI, the serial `by-id` path, the working
|
||
I2C bus (probed with a real SHT41 measurement, since `i2cdetect` false-negatives on the SHT4x), the
|
||
camera's `DEV_` id — and leaves a human to enter `axes`, `imu`, `env` and the drivetrain block, which no
|
||
probe can distinguish from a *fault*. Which is the whole of §10.
|
||
|
||
## 9. Calibration write-back
|
||
|
||
### Already implemented — the confirmation prompt
|
||
|
||
Nothing is written without a yes:
|
||
|
||
- `Application.cpp:944` raises the activity-strip prompt *"Save the new [Motor] calibration to config as
|
||
the default? (y / n)"* whenever `calib_save_pending_` is set.
|
||
- `Application.cpp:1260` / `:1726` set it after `gimbal heading setup` / `heading fit` / `pitch setup`.
|
||
- `Application.cpp:1847` implements `save` and `discard` as **commands**, so the answer works over the
|
||
TUI keypress (`TuiUi.cpp:1599`), stdin, or MQTT.
|
||
|
||
Worth checking on the rig: the headless/systemd path should log the values and expire, not hang.
|
||
|
||
### To add — keep the previous value as a comment
|
||
|
||
`updateIniSectionKeys` replaces a matching line **in place**, so the old value is lost. Keep exactly one
|
||
generation:
|
||
|
||
```ini
|
||
[Motor]
|
||
;prev yaw_heading_at_center = 0.0 ; 2026-09-11T14:32Z, gimbal heading setup
|
||
yaw_heading_at_center = 127.4
|
||
```
|
||
|
||
Two rules keep it tidy: **only record a key whose value actually changed** (`saveMotorGeometry` rewrites
|
||
all eight every save, so without this the file gains eight lines of noise per calibration), and
|
||
**replace, don't accumulate** — an existing `;prev` for that key is overwritten, so the file grows once
|
||
and then holds steady. Reverting is uncommenting; `;prev` lines are comments and inih ignores them.
|
||
|
||
This lands in `updateIniSectionKeys` + `saveMotorGeometry`, and `saveCalibration()` is the only caller.
|
||
|
||
## 10. Declaring presence
|
||
|
||
The existing code degrades gracefully everywhere — `TestRunner` skips the pitch and IMU leaves, CamEvent
|
||
falls back to encoder pitch, `HeadingFit` is IMU-free by design, `CaptureScheduler` sends the single-axis
|
||
`MOVE Y …` form. It adapts to *what it finds* and never states *what it should have found*, so **a
|
||
failure is indistinguishable from a configuration**: a 2-axis tower whose pitch stops enumerating becomes
|
||
a yaw-only tower, captures one elevation band, and reports itself healthy.
|
||
|
||
| Declared | Found | Result |
|
||
|---|---|---|
|
||
| present | present | OK |
|
||
| **present** | **absent** | **refuse to start**, naming the subsystem; `--degraded` overrides, loudly |
|
||
| absent | present | warn — the file is stale |
|
||
|
||
## 11. `test hardware`
|
||
|
||
| Leaf | Checks |
|
||
|---|---|
|
||
| `test hardware axes` | every axis in `axes` appears in telemetry, and no others |
|
||
| `test hardware motor` | `DUMP` reports the drivetrain values `hardware.ini` specifies — catches a stale flash, an unanswered `NEED`, or a tower left on limp-home defaults |
|
||
| `test hardware imu` | declared → device opens, `0xFA`-framed stream arrives |
|
||
| `test hardware env` | declared → a **real I2C** adapter (`i2cdetect -F` says `I2C = yes`, not SMBus-only) and a CRC-valid SHT41 measurement — known-issues trap #2, documented but never automated |
|
||
| `test hardware camera` | every camera in `cameras` is found; `usbfs_memory_mb ≥ 1000`; USB3 LPM off on the `1ab2` port |
|
||
|
||
## 12. What stops being declared twice
|
||
|
||
| Today | After |
|
||
|---|---|
|
||
| ~~Production speeds hardcoded in `Application.cpp` contradict `firmware/src/config.h` — and win~~ **fixed** | `[Motor.<axis>] scan_speed_deg_s` in `hardware.ini`, in degrees per second, converted to VMAX per tower |
|
||
| Firmware `MOTOR_CONFIG` duplicates every drivetrain value the host also knows | firmware holds none of them until told |
|
||
| Firmware `NUM_AXES` hardcoded; host infers axes from telemetry | generated from `[Hardware] axes` |
|
||
| ~~`[Features] enable_imu` / `enable_env`~~ **fixed** | derived from `[Hardware] imu` / `env`; the flags remain only for towers without a `hardware.ini`, and a flag that contradicts the declaration is warned about and ignored |
|
||
|
||
`mock_camera` / `mock_serial` / `mock_imu` / `mock_env` stay in `config.ini` — dev-box substitution is
|
||
operational, and it is what lets all of this be exercised with no hardware.
|
||
|
||
## 13. Migration
|
||
|
||
With no `hardware.ini`, synthesise one in memory from the current `config.ini` plus today's firmware
|
||
defaults, and log the file it *would* have written. Ship `install.sh --extract` to split an existing
|
||
device config. Reconciliation starts **warn-only**.
|
||
|
||
Deploy host-before-firmware (A.6). Fix the `Application.cpp:1040` literals in the same change — on the
|
||
existing fleet that drops yaw 50000 → 30000, a real behaviour change that wants a deliberate rig check
|
||
rather than a silent correction.
|
||
|
||
## 14. Firmware changes, collected
|
||
|
||
- **Per board** → `platform_leonardo.h` / `platform_iota.h`: pin map plus the three AVR-isms
|
||
(`MCUSR`, `__heap_start`/`__brkval`) behind `platformResetCause()` / `platformFreeRam()`.
|
||
- **Generated** → pins and `NUM_AXES` from `hardware.ini`.
|
||
- **Deleted** → the drivetrain fields of `MOTOR_CONFIG`; `config.h` keeps only mechanism-wide thresholds.
|
||
- **New** → `NEEDCFG` state, `NEED` request line, generic `SET <axis> <key> <value>`, `USE DEFAULTS`,
|
||
split init block, drivetrain values reported in `DUMP`. The existing `GIT_REV` in `BOOT`/`DUMP` is
|
||
unchanged — no version numbers are added (§7).
|
||
|
||
---
|
||
|
||
# Sequencing
|
||
|
||
| Phase | Work | Gate |
|
||
|---|---|---|
|
||
| **1** | **Host**: split `hardware.ini` out of `config.ini` (§3), resolution chain (§4), presence reconciliation (§10), `test hardware` (§11), calibration `;prev` comments (§9), build identity (§7), `install.sh --extract` (§8). **Delete the `Application.cpp:1040` speed literals** (§5). Roll out across the live fleet, warn-only | `test hardware` green on every tower; header shows a real build id; an unplugged pitch axis *fails* instead of degrading silently |
|
||
| **2** | **Firmware**: `NEEDCFG` + `NEED` / `SET` / `USE DEFAULTS`, split init block, drivetrain values out of `MOTOR_CONFIG` and into `DUMP` (§6). **Host first, then firmware** | A firmware with no host sits in `NEEDCFG` and moves nothing; with the host it reaches `READY` holding exactly the `hardware.ini` values; an old firmware that never sends `NEED` is detected as such and falls back |
|
||
|
||
Phase 1 is deployable on its own. Phase 2 requires it.
|
||
|
||
## Progress
|
||
|
||
Branch `worktree-hardware-config` (worktree at `.claude/worktrees/hardware-config`).
|
||
286 tests pass, up from a 204 baseline.
|
||
|
||
| § | Item | State |
|
||
|----|------|-------|
|
||
| 7 | Build identity — CMake reads `$ENV{GIT_REV}`; `Application.cpp` stops hardcoding `"dev"` | **done**, verified embedded in the binary |
|
||
| 9 | Calibration `;prev` comments, one generation, changed keys only, stamped with the command | **done**, 7 tests |
|
||
| 3 | `hardware.ini`: `[Board]` / `[Hardware]` typed, layered over `config.ini`, shadowed keys warned | **done**, 5 tests |
|
||
| 4 | Resolution chain `--hardware` / `$FGC_HARDWARE` / `/etc/...` / beside `config.ini` | **done**, 2 tests |
|
||
| 8 | `config/hardware.example.ini` documenting the shape | **done**, and the `hardware/lattepanda-iota/` setup kit now exists too (`install.sh`, `packages.txt`, three precondition files) |
|
||
| 10 | Presence reconciliation (declared vs found) | **done**, 13 tests. `HardwarePresence.h` compares `[Hardware]` against what came up; warn-only by default, refusing under `[Features] strict_presence` with `--degraded` as the field-triage escape (decision 4) |
|
||
| 11 | `test hardware` | **done**, 11 tests. Five leaves (`axes` / `motor` / `imu` / `env` / `camera`) under a new `hardware` subsystem; included in a bare `test`, and deliberately not gated on a homed gimbal. Nothing declared reports SKIP, never PASS |
|
||
| 5 | Delete the `Application.cpp` speed literals | **done**, 13 tests. Decision 2 answered: speed is declared as `[Motor.<axis>] scan_speed_deg_s` (default **8.0 deg/s**) and converted per tower through `counts_per_deg` (`AxisSpeed.h`). The literals sent 36.4 deg/s against an intended 21.8 |
|
||
| 6 | Config handshake — **host half** | **done**, 13 tests. `FirmwareConfig.h` parses `NEED` and plans the `SET` lines; unanswered keys and unknown keys are reported separately. `[Features] mock_needcfg` makes the simulated controller run the handshake, so the host half is verified with no firmware. **The firmware half is not written** |
|
||
| 6 | Config handshake — **firmware half** | **written, NOT compile-verified.** `NEEDCFG` state (`ST` code `C`), `NEED` emitted with the banner, `SET <axis> <key> <value>`, `USE DEFAULTS`, and the init block split so the residual-motion stop still runs at boot. Parser verified locally (30 native tests, g++); the rest type-checks against stubs on both board branches but has never been built by PlatformIO or run on hardware |
|
||
|
||
Presence reconciliation (§10) changes nothing on a tower without a `hardware.ini`: absent is
|
||
silent and `hardware.declared` stays false. Verified by running the binary both ways.
|
||
|
||
> **§5 is the exception, and it is deliberate.** `scan_speed_deg_s` defaults to **8.0 deg/s** and
|
||
> applies whether or not a `hardware.ini` exists, because the literals it replaced were themselves
|
||
> unconditional. **Every tower changes speed on upgrade**: the Delta fleet goes from 36.4 deg/s to
|
||
> 8.0, so a full revolution takes ~45 s instead of ~16.5 s. Check that against `image_interval` and
|
||
> the scan grid's waypoint count before deploying, and set `scan_speed_deg_s` per tower if 8.0 is
|
||
> not right for it. An axis whose `counts_per_deg` is untrustworthy is left on the firmware default
|
||
> instead.
|
||
|
||
---
|
||
|
||
# Decisions needed before work starts
|
||
|
||
1. **How do the files find each other?** Recommendation: a fixed `/etc/fire_gimbal_control/hardware.ini`,
|
||
matching the existing `credentials.env` convention, rather than a pointer in `config.ini` (§4).
|
||
2. ~~**The `SPEED Y 50000` / `SPEED P 150000` literals contradict the firmware's tuned-down defaults and
|
||
win.** Is the intended yaw production speed 30000 or 50000?~~ **ANSWERED, and the question turned out to
|
||
be mis-framed.** 30000 was intended *for tower 111* — but a VMAX is not a portable quantity: the same
|
||
30000 is 21.8 deg/s on the Delta and 6.0 deg/s on the IOTA. Speed is now declared in **degrees per
|
||
second** (`[Motor.<axis>] scan_speed_deg_s`, default 8.0) and converted per tower through
|
||
`counts_per_deg`. The literals were 36.4 deg/s.
|
||
3. **`NEEDCFG` — refuse to move, or fall back after a timeout?** Recommendation: refuse, with an explicit
|
||
`USE DEFAULTS` for bench work. A timeout lets a slow host produce a silently mis-tuned tower (§6).
|
||
4. **How strict is reconciliation, and when?** Recommendation: warn-only through the fleet rollout, then
|
||
fatal, with `--degraded` as the field-triage escape (§10).
|
||
5. **Do any live rigs differ in axis count**, or is the variation only IMU/env? If every tower is 2-axis
|
||
today, `axes` guards against future drift rather than fixing something current, and phase 1 shrinks.
|