Write the drivetrain block at install, since the firmware now demands it

This commit is contained in:
pgdalmeida 2026-09-18 17:06:34 +02:00
parent 842d95eeca
commit 6f88b00856
Signed by: pedro.almeida
GPG Key ID: D4A6C394DF13F1D7
5 changed files with 158 additions and 10 deletions

View File

@ -435,11 +435,12 @@ Branch `worktree-hardware-config` (worktree at `.claude/worktrees/hardware-confi
| 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) |
| 8 | `install.sh` writes the drivetrain block | **done.** One `[Motor.<axis>]` per declared axis, with `--r-sense` / `--run-current` / `--homing-current` / `--travel-deg` / `--steps-per-rev` / `--sw-mode` for what an installer knows and commented `; TODO` lines for what nothing can probe. Needed because the firmware now refuses to move undeclared |
| 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 |
| 6 | Config handshake — **firmware half** | **done and built** on both targets 2026-09-18 (iota_rp2040 and leonardo, `./deploy.sh --compile-only`); behaviour still unproven on a running rig. `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. 33 native tests pass under `pio test -e native`. **Behaviour on a running rig is the one check still outstanding** |
| 6 | Adoption check generalised | **done**, 9 tests. `DUMP` echoes every host-owned value plus a `cfg_rx` bitmask; `test hardware motor` checks all of them, and reports an absent field as unconfirmable rather than as a mismatch against -1. Round trip NEED -> SET -> DUMP -> PASS verified against `mock_needcfg` |
Presence reconciliation (§10) changes nothing on a tower without a `hardware.ini`: absent is

View File

@ -299,17 +299,25 @@ commit `aab1925`.
| 2 | **Phase 2 gate: RP2040 + TMC5160-BOB at 3.3 V** | **PASSED** on tower 121, 2026-09-15: `IOIN Y=0x3000001E version=0x30`, encoder reading (`X_ENC`), one axis, `speed=3000` |
| 2 | `src/platform.h` — reset cause and free RAM behind one interface per MCU family | **done** |
| 2 | `Serial.begin()` moved ahead of all hardware init, so a wedged image stays flashable | **done** |
| 2 | `[env:iota_rp2040]` PlatformIO target (arduino-pico core, 8 MB flash) | **done and built** on tower 121, 2026-09-15: 49452 B flash (0.6 % of 8 MB), 9068 B RAM. `leonardo` still builds (19254 B) and the 22 native tests still pass |
| 2 | `[env:iota_rp2040]` PlatformIO target (arduino-pico core, 8 MB flash) | **done and built.** Latest, 2026-09-18, with the config handshake: **iota_rp2040** 51716 B flash (0.6 % of 8 MB), 9108 B RAM (3.5 %); **leonardo** 22646 B flash (**79 % of 28 KB**), 939 B RAM (36.7 %). Both from `./deploy.sh --compile-only` against towers 121 and 111; 33 native tests pass (`pio test -e native`) |
| 2 | Per-board pin map + `NUM_AXES` in `platform_leonardo.h` / `platform_iota.h`, as-built wiring | **done** — compile-time, not yet generated from `hardware.ini` |
| 2 | Pins + `NUM_AXES` generated from `hardware.ini` | **not started** — needs the config plan's phase 2 first |
| 3 | Yaw homing on the real drivetrain | **done** — the endstop was being sheared through at full torque; homing at 189 mA holds. See below |
| 1 | Everything else | blocked on [hardware-config-plan.md](hardware-config-plan.md) |
> **Not compile-verified.** PlatformIO is not installed on the dev box and the firmware is built
> *remotely on the tower* by `firmware/deploy.sh`. What was verified locally: both branches of
> `platform.h` syntax-check against stub headers — the AVR branch under `gnu++11` (what the Arduino AVR
> core uses) and the RP2040 branch under `gnu++17` (what arduino-pico uses) — and an unrecognised
> architecture fails on the `#error` rather than compiling silently.
> **Where the firmware gets built.** PlatformIO is not installed on the dev box; the firmware is built
> *remotely on a tower* by `firmware/deploy.sh --compile-only`. Locally, the Arduino-free sources
> (`command_parse.cpp` and its native tests) compile and run under plain g++, and the rest
> syntax-checks against stub headers on both branches — the AVR under `gnu++11` and the RP2040 under
> `gnu++17`, with an unrecognised architecture failing on the `#error` rather than compiling silently.
> That catches typos and signatures; it says nothing about size or behaviour, so a remote build is
> still the gate.
>
> **Flash headroom is now the constraint on the Delta fleet.** The ATmega32U4 build sits at **79 % of
> its 28 KB** (6026 B left), up from 19254 B at the phase 2 gate — that growth is everything since:
> `BOOTSEL`, `CURRENT`, the reduced homing current, `STOP` ending a homing, and the config handshake.
> RP2040 flash is a non-issue at 0.6 % of 8 MB. Deleting the drivetrain fields from `MOTOR_CONFIG`
> (config plan §14) now buys space as well as removing a duplicate definition.
>
> To actually build it: `cd firmware && ./deploy.sh --compile-only` compiles on the device without
> flashing. That rsyncs over the tower's firmware source tree, so it is a deliberate step, not one to

View File

@ -230,8 +230,26 @@ Then prove the program finds it **with nothing in the environment** - no
### The drivetrain block
`install.sh` does not write this one - no probe can read a sense resistor off a
board - so add it by hand, one `[Motor.<axis>]` block per axis:
**`install.sh` now writes this block**, one `[Motor.<axis>]` per declared axis —
it has to, because the firmware refuses to move until it has been told these
(see the config handshake in [configuration.md](configuration.md)). What it still
cannot do is *probe* them: no reading of a board tells you its sense resistor.
So pass what you know and it writes the values; omit anything and it writes a
commented `; TODO` line naming what to measure:
```bash
./install.sh --stdout --axes yaw \
--r-sense 0.075 --run-current 756 --homing-current 200 \
--travel-deg 340 --steps-per-rev 1210778 --sw-mode 0
```
A `TODO` line is **commented, not a placeholder value**, and deliberately: these
keys are numeric, and a non-numeric value makes the program refuse to start with
a message about a config key. Left absent, it starts and names exactly which keys
that axis is missing and that it will not move — the useful failure.
The block it writes looks like this:
```ini
[Motor.yaw]

View File

@ -20,6 +20,22 @@
# --env none (default: none) ambient sensor model, or none
# --cameras rgb (default: rgb) expected [Camera.<name>] blocks
#
# The drivetrain, per axis. The firmware REFUSES TO MOVE until it has been told
# these (docs/configuration.md, the config handshake), so an axis left undeclared
# is an axis that will sit in NEEDCFG. Pass what you know; what you omit is
# written as a commented line naming what has to be measured:
#
# --r-sense 0.075 sense resistor on that axis's TMC board (ohms)
# --run-current 756 coil current for normal motion (mA RMS)
# --homing-current 200 coil current while hunting an endstop (mA RMS)
# --travel-deg 340 mechanical angle between the hard stops
# --steps-per-rev 1210778 measured full travel, in counts
# --sw-mode 0 TMC SW_MODE: 0 = no switches (StallGuard homing)
#
# These take ONE value, applied to the first axis in --axes. A second axis gets a
# commented template: nothing here can be probed, and inventing a sense resistor
# is how a driver gets cooked.
#
# Other flags:
# --force overwrite an existing hardware.ini (backs it up first)
# --no-apt-update skip 'apt-get update'
@ -34,6 +50,10 @@ AXES="yaw,pitch"
IMU="none"
ENV_SENSOR="none"
CAMERAS="rgb"
# Empty means "not known at install time" -> written commented out, never as a
# sentinel: these are numeric keys, and a non-numeric value makes the program
# refuse to start rather than report the axis as undeclared.
R_SENSE="" RUN_CURRENT="" HOMING_CURRENT="" TRAVEL_DEG="" STEPS_PER_REV="" SW_MODE=""
DO_DEPS=0 DO_PRE=0 DO_HW=0 DO_VIMBA=0 PROBE_ONLY=0 TO_STDOUT=0 FORCE=0 APT_UPDATE=1
VIMBA_SRC=""
EXPLICIT=0
@ -49,6 +69,12 @@ while [[ $# -gt 0 ]]; do
--imu) IMU="$2"; shift ;;
--env) ENV_SENSOR="$2"; shift ;;
--cameras) CAMERAS="$2"; shift ;;
--r-sense) R_SENSE="$2"; shift ;;
--run-current) RUN_CURRENT="$2"; shift ;;
--homing-current) HOMING_CURRENT="$2"; shift ;;
--travel-deg) TRAVEL_DEG="$2"; shift ;;
--steps-per-rev) STEPS_PER_REV="$2"; shift ;;
--sw-mode) SW_MODE="$2"; shift ;;
--vimba) VIMBA_SRC="$2"; DO_VIMBA=1; EXPLICIT=1; shift ;;
--force) FORCE=1 ;;
--no-apt-update) APT_UPDATE=0 ;;
@ -267,6 +293,26 @@ do_vimba() {
}
# ------------------------------------------------------------ hardware.ini ---
# Emit "key = value", or a commented-out line naming what has to be measured.
#
# Commented rather than a placeholder string on purpose: these keys are numeric,
# and a non-numeric value makes the program refuse to start with a message about
# a config key. Absent, it starts and reports the axis as undeclared - naming the
# missing keys and which axis will not move, which is the useful failure.
kv_or_todo() {
local key="$1" value="$2" why="$3"
if [[ -n $value ]]; then
printf '%-18s = %s\n' "$key" "$value"
return
fi
# Wrapped, and not merely for looks: ini.h sets INI_MAX_LINE to 200, and a
# longer line is SPLIT rather than truncated, so its tail becomes a bogus
# line and the whole file fails to parse. A generated file that cannot be
# read is worse than one with a gap in it.
printf '; TODO %s\n' "$key"
printf '%s\n' "$why" | fold -s -w 72 | sed 's/^/; /' | sed 's/[[:space:]]*$//'
}
emit_hardware_ini() {
local board mcu cam fw_env
board="$(probe_board)"
@ -329,6 +375,60 @@ i2c_addr = 0x44
EOF
fi
# One [Motor.<axis>] block per axis. The firmware holds none of these itself
# any more - it asks, and refuses to move until answered - so an axis without
# a block here is an axis that will not turn.
#
# A value we were not given is written COMMENTED OUT, with what to measure.
# Not as a placeholder string: these keys are numeric, and a non-numeric value
# makes the program refuse to start, which is a worse failure than the axis
# reporting itself undeclared.
local ax first=1
for ax in ${AXES//,/ }; do
ax="$(echo "$ax" | tr -d '[:space:]')"
[[ -z $ax ]] && continue
echo
echo "[Motor.$ax]"
if [[ $first -eq 1 ]]; then
kv_or_todo "r_sense_ohm" "$R_SENSE" \
"the resistor fitted to THIS driver board - read it off the board, do not assume"
kv_or_todo "run_current_ma" "$RUN_CURRENT" \
"coil current for normal motion, mA RMS, from the motor's nameplate"
kv_or_todo "homing_current_ma" "$HOMING_CURRENT" \
"current while hunting an endstop. LOWER than run_current wherever the stop can be out-pushed: on this rig a screw-head stop was sheared straight past its mate at full torque"
kv_or_todo "travel_deg" "$TRAVEL_DEG" \
"mechanical angle between the hard stops. Rough is fine to start; every bearing inherits its error proportionally until 'gimbal heading setup' supersedes it"
kv_or_todo "steps_per_rev" "$STEPS_PER_REV" \
"measured full travel IN COUNTS - read it off a homing run. The only guard against a PHANTOM endstop"
kv_or_todo "sw_mode" "$SW_MODE" \
"TMC SW_MODE. 0 = no physical switches (StallGuard homing); set the bits if this axis has REFL/REFR wired"
else
echo "; Nothing here can be probed, and this is not the first axis, so the"
echo "; values below are a template. Measure them on THIS axis."
kv_or_todo "r_sense_ohm" "" "sense resistor on this axis's driver board"
kv_or_todo "run_current_ma" "" "coil current for normal motion, mA RMS"
kv_or_todo "homing_current_ma" "" "current while hunting an endstop"
kv_or_todo "travel_deg" "" "mechanical angle between the hard stops"
kv_or_todo "steps_per_rev" "" "measured full travel, in counts"
kv_or_todo "sw_mode" "" "TMC SW_MODE (0 = no physical switches)"
fi
cat <<'EOF'
; Speeds are DEGREES PER SECOND, not VMAX: a register value means different
; things on different gear ratios, so it is not portable between towers. The
; host converts through counts_per_deg and sends SPEED.
scan_speed_deg_s = 8.0
homing_speed_deg_s = 2.0
center_speed_deg_s = 6.0
; |XACTUAL - XENC| that means "position untrustworthy". 1500 is this mechanism's
; starting point; re-measure it whenever the homing current changes, because the
; free-running gap is coupled to it.
enc_deviation = 1500
; Is this axis's encoder aligned to XACTUAL and usable for hold and DIAG?
has_encoder = true
EOF
first=0
done
local c
for c in ${CAMERAS//,/ }; do
c="$(echo "$c" | tr -d '[:space:]')"
@ -370,7 +470,12 @@ do_hardware_ini() {
board="$(probe_board)"
if [[ $board != lattepanda-iota ]]; then
warn "this kit is for the LattePanda IOTA; this machine reports '$board'"
[[ $PROBE_ONLY -eq 1 ]] || die "refusing to apply IOTA preconditions to a $board"
# --probe and --stdout change nothing on this machine: one reports what it
# found, the other prints the file it WOULD write. Refusing them on a
# non-IOTA only means the generator can never be exercised off a tower,
# which is how a setup kit ends up untested.
[[ $PROBE_ONLY -eq 1 || $TO_STDOUT -eq 1 ]] ||
die "refusing to apply IOTA preconditions to a $board"
fi
if [[ $PROBE_ONLY -eq 1 ]]; then probe_report; exit 0; fi

View File

@ -1243,6 +1243,22 @@ struct Application::Impl {
continue;
}
const bool is_yaw = need->axis == 'Y';
// An axis the tower says it does not have. That is not a missing
// declaration - it is a firmware built for more axes than this rig,
// and answering would configure a driver that is not there. Same
// class of fault as presence reconciliation catches, so it is named
// that way rather than as a hole in hardware.ini.
const char* axis_name = is_yaw ? "yaw" : "pitch";
if (cfg.hardware.declared && !cfg.hardware.hasAxis(axis_name)) {
LOG_ERROR << "firmware asked for axis " << need->axis << " (" << axis_name
<< "), which this tower does not declare (axes "
<< cfg.hardware.axesLabel()
<< ") -- the firmware was built for more axes than the rig has; "
"rebuild it for this board rather than declaring an axis that is "
"not fitted";
continue;
}
const auto src = is_yaw
? axisConfigSource("yaw", cfg.geometry.yaw, cfg.drivetrain.yaw,
yaw_current_, cfg.yaw_soft_margin_counts)