Compare commits

..

No commits in common. "4049a85b9bdba14168fe521e306f5a7416f30e46" and "12e23a5fb648818db95a4400444758f2b2e2695d" have entirely different histories.

21 changed files with 22 additions and 482 deletions

View File

@ -69,26 +69,6 @@ cameras = rgb
device = /dev/serial/by-id/usb-Arduino_LLC_Arduino_Leonardo-if00 device = /dev/serial/by-id/usb-Arduino_LLC_Arduino_Leonardo-if00
baud = 115200 baud = 115200
[Pins]
; How the MCU is wired to each axis's TMC5160 driver board. The host never sends
; these: the FIRMWARE is built from them. firmware/tools/gen_wiring.py reads this
; section from the tower's own copy of this file when the firmware is built on
; the tower, and refuses pins the board cannot use. Change a wire, change this,
; rebuild and re-flash. `test hardware motor` compares it with what the running
; firmware reports, so a flash built from an older copy shows up there.
;
; The reference loom for each board, with the MCU-pin -> driver-pin table, is in
; hardware/<board>/pins.ini.
;
; Leonardo (Delta): SPI is fixed on the ICSP header, so only CS and EN are named.
; RP2040 (IOTA): also spi_sck / spi_mosi / spi_miso, from SPI0's pin groups -
; SCK {2,6,18,22} MOSI {3,7,19,23} MISO {0,4,16,20}
; Only axes listed in [Hardware] axes get pins.
yaw_cs = 8
yaw_en = 7
pitch_cs = 1
pitch_en = 0
[IMU] [IMU]
; Only read when [Hardware] imu is not 'none'. On the LattePanda 3 Delta the MTi ; Only read when [Hardware] imu is not 'none'. On the LattePanda 3 Delta the MTi
; is on the RS-232 header, which is a hardware UART: a stable /dev/ttyS* node, ; is on the RS-232 header, which is a hardware UART: a stable /dev/ttyS* node,

View File

@ -227,7 +227,7 @@ this whole design is removing. Make it an explicit act.
Only **pin assignments** and the **axis count**: pins because SPI must be up before anything can be 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 asked or answered, the axis count because it sizes the axis array. Both are still defined once in
`hardware.ini` (`[Pins]`, `[Hardware] axes`) and *generated* into `board_wiring.h` at build — one definition, realised at build `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 time instead of over the wire. Everything else becomes runtime, which shrinks the build-time set far
below what I proposed last turn. below what I proposed last turn.
@ -402,27 +402,6 @@ Deploy host-before-firmware (A.6). Fix the `Application.cpp:1040` literals in th
existing fleet that drops yaw 50000 → 30000, a real behaviour change that wants a deliberate rig check existing fleet that drops yaw 50000 → 30000, a real behaviour change that wants a deliberate rig check
rather than a silent correction. rather than a silent correction.
> ### ⚠️ The Delta fleet is NOT migrated, and flashing it first would strand it
>
> Checked 2026-09-23: tower **111 has no `/etc/fire_gimbal_control/hardware.ini`**.
> Only tower 121 has been migrated. This is a live hazard rather than a leftover
> chore, because the firmware half is now written: **an axis with no declared
> drivetrain sits in `NEEDCFG` and refuses to move.** Flashing 111 with current
> firmware today would leave it unable to home, with nothing wrong that an
> operator could see on the board.
>
> The ordering rule above is what prevents it, and it is not advisory:
>
> 1. Write 111 a `hardware.ini` — `hardware/lattepanda-iota/install.sh --stdout`
> generates the shape; the Delta's own values come from its `config.ini`.
> 2. Deploy the **host** and run `test hardware`. It must pass on the old
> firmware, which is the point of the `_current`/`_declared` metrics reporting
> an absent field as unconfirmable rather than as a mismatch.
> 3. Only then flash the firmware, and re-run `test hardware`.
>
> Until step 1 is done for every Delta tower, phase 1's gate — "`test hardware`
> green on every existing tower" — is **not met**, whatever 121 reports.
## 14. Firmware changes, collected ## 14. Firmware changes, collected
- **Per board** → `platform_leonardo.h` / `platform_iota.h`: pin map plus the three AVR-isms - **Per board** → `platform_leonardo.h` / `platform_iota.h`: pin map plus the three AVR-isms
@ -466,7 +445,6 @@ after the table.
| 6 | Config handshake — **firmware half** | **done, and validated on tower 121.** `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`. On the rig: the handshake was watched live, `HOME` refused with `ERR need config`, `RESET` did not escape the state, and `USE DEFAULTS` released it | | 6 | Config handshake — **firmware half** | **done, and validated on tower 121.** `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`. On the rig: the handshake was watched live, `HOME` refused with `ERR need config`, `RESET` did not escape the state, and `USE DEFAULTS` released it |
| 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` | | 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` |
| 14 | Drivetrain out of `MOTOR_CONFIG` | **done.** The struct keeps pins and `homing_kind` only; `platform_iota.h` keeps its measurement record but no longer defines this tower's values. `USE DEFAULTS` now loads a shared **limp-home** set (`LIMP_*` in `config.h`) that is deliberately nobody's real values — slow speeds, generous margins, the gentle search current — so a tower still running them fails `test hardware motor` instead of looking configured. `SW_MODE` is derived from `homing_kind` rather than shared, because disabling the hardware stops on an axis that homes on real switches would remove its last protection. Flash cost, not saved: see the porting plan. Found a real bug on the way — `DIAG` read `normal_speed` / `center_speed` from the compile-time table, so it ran at compiled speeds rather than the tower's declared ones | | 14 | Drivetrain out of `MOTOR_CONFIG` | **done.** The struct keeps pins and `homing_kind` only; `platform_iota.h` keeps its measurement record but no longer defines this tower's values. `USE DEFAULTS` now loads a shared **limp-home** set (`LIMP_*` in `config.h`) that is deliberately nobody's real values — slow speeds, generous margins, the gentle search current — so a tower still running them fails `test hardware motor` instead of looking configured. `SW_MODE` is derived from `homing_kind` rather than shared, because disabling the hardware stops on an axis that homes on real switches would remove its last protection. Flash cost, not saved: see the porting plan. Found a real bug on the way — `DIAG` read `normal_speed` / `center_speed` from the compile-time table, so it ran at compiled speeds rather than the tower's declared ones |
| 14 | Pins and `NUM_AXES` generated from `hardware.ini` | **done, validated on tower 121** (2026-09-23): built from its `/etc` file, flashed, `DUMP` echoed `spi=2,3,0 cs=1 en=4`, `IOIN version=0x30`, and `test hardware motor` passed `yaw_pins` / `spi_pins`; a build against a copy without `[Pins]` failed with the reason. Leonardo flash cost not yet measured. New `[Pins]` section; `firmware/tools/gen_wiring.py` generates `board_wiring.h` into the build directory from the tower's own file at build time, and refuses pins the board cannot use (RP2040 SPI0 groups, the Leonardo's fixed ICSP pins, duplicates, a board/env mismatch). No compiled-in fallback: no file or no `[Pins]` fails the build. `DUMP` echoes `cs=`/`en=` per axis and `spi=` on the RP2040; `test hardware motor` compares them with `[Pins]`. The loom tables moved to `hardware/<board>/pins.ini`. 18 generator tests (run without PlatformIO), 9 host tests |
| 6 | `NEEDCFG` understood host-side | **done**, 5 tests. `AxisState::NeedConfig` parsed from `ST` `C`, named in `DUMP` and the dashboard, and counted as telemetry arriving rather than as a dead link. The firmware enum APPENDS the state (5) rather than inserting it — inserting renumbered every later state and made a READY axis read back as ERROR | | 6 | `NEEDCFG` understood host-side | **done**, 5 tests. `AxisState::NeedConfig` parsed from `ST` `C`, named in `DUMP` and the dashboard, and counted as telemetry arriving rather than as a dead link. The firmware enum APPENDS the state (5) rather than inserting it — inserting renumbered every later state and made a READY axis read back as ERROR |
**Validated on tower 121.** The handshake was watched live on the rig, not inferred: **Validated on tower 121.** The handshake was watched live on the rig, not inferred:

View File

@ -133,13 +133,11 @@ size or override it, or the filesystem/OTA layout will be wrong.
> >
> So the **first** flash of a factory board needs the REPL trick or a physical > So the **first** flash of a factory board needs the REPL trick or a physical
> BOOTSEL press — one time per board, and worth knowing before someone drives to > BOOTSEL press — one time per board, and worth knowing before someone drives to
> a mast. **That one is still true.** > a mast. The **update** case is the one that still has no answer: our firmware
> honours the 1200-baud reset but returns to the application, so a tower cannot
> currently be re-flashed over the network at all.
> >
> The **update** case was the one that had no answer at the time of that table: > **Fixed by a `BOOTSEL` serial command** calling arduino-pico's
> our firmware honoured the 1200-baud reset but returned to the application, so a
> tower could not be re-flashed over the network at all.
>
> **Since fixed by a `BOOTSEL` serial command** calling arduino-pico's
> `rp2040.rebootToBootloader()` — implemented and proven on tower 121 the same > `rp2040.rebootToBootloader()` — implemented and proven on tower 121 the same
> day. The full loop now runs with nobody at the board: > day. The full loop now runs with nobody at the board:
> >
@ -297,7 +295,7 @@ commit `aab1925`.
| Phase | Item | State | | Phase | Item | State |
|---|---|---| |---|---|---|
| 0 | Remote-flash question researched — two viable paths, design rule derived (§2) | **done, and solved** (§2). Both researched paths turned out not to work on the board; the answer was a `BOOTSEL` serial command, proven on tower 121 and now sent automatically by `firmware/deploy.sh` before every RP2040 upload. The **first** flash of a factory board still needs the MicroPython REPL or a physical BOOTSEL press — once per board | | 0 | Remote-flash question researched — two viable paths, design rule derived (§2) | **done, and now measured on the board: neither works** (§2). First flash needs the MicroPython REPL or BOOTSEL; remote *update* is unsolved pending a `BOOTSEL` command |
| 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 | **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 | `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 | `Serial.begin()` moved ahead of all hardware init, so a wedged image stays flashable | **done** |
@ -305,7 +303,7 @@ commit `aab1925`.
| 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 | 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, no longer blocked** — the config plan it waited on is complete. The last open phase-2 item | | 2 | Pins + `NUM_AXES` generated from `hardware.ini` | **not started, no longer blocked** — the config plan it waited on is complete. The last open phase-2 item |
| 3 | Yaw homing on the real drivetrain | **done** — the endstop was being sheared through at full torque; homing at 189 mA holds. See below | | 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 | Phase 1 in full — `hardware.ini`, the config handshake, presence reconciliation, `test hardware` | **built and validated on 121, NOT rolled out.** [hardware-config-plan.md](hardware-config-plan.md) is complete, but tower **111 has no `hardware.ini` at all** (checked 2026-09-23), so the gate ("`test hardware` green on every existing tower") is not met. Flashing a Delta before writing it one would strand the axis in `NEEDCFG` — see the migration warning in [the config plan §13](hardware-config-plan.md#13-migration) | | 1 | Phase 1 in full — `hardware.ini`, the config handshake, presence reconciliation, `test hardware` | **done.** [hardware-config-plan.md](hardware-config-plan.md) is complete and validated on tower 121; the gate ("`test hardware` green on every existing tower") is met on 121, and still wants running against the Delta fleet |
> **Where the firmware gets built.** PlatformIO is not installed on the dev box; the firmware is built > **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 > *remotely on a tower* by `firmware/deploy.sh --compile-only`. Locally, the Arduino-free sources

View File

@ -31,7 +31,7 @@ doctest unit-test suite (`ctest`).
| 14 | Capture sweep on hardware | **Mechanically verified on the rig (2026-08-07).** A full `--init --start` run homed both axes and completed `MOVE → settle → trigger` for 15/15 waypoints with 0 dropped frames, 0 stalls and 0 acquisition failures. `kSettleTolCounts` (600) and the per-interval timing in [CaptureScheduler.cpp](../src/core/CaptureScheduler.cpp) have **not** been tuned against observed `ST` behaviour yet, and the settle tolerance has not been checked against a sharp image (see #16). | | 14 | Capture sweep on hardware | **Mechanically verified on the rig (2026-08-07).** A full `--init --start` run homed both axes and completed `MOVE → settle → trigger` for 15/15 waypoints with 0 dropped frames, 0 stalls and 0 acquisition failures. `kSettleTolCounts` (600) and the per-interval timing in [CaptureScheduler.cpp](../src/core/CaptureScheduler.cpp) have **not** been tuned against observed `ST` behaviour yet, and the settle tolerance has not been checked against a sharp image (see #16). |
| 15 | **Camera profiles unverified on a lit scene** | The `standard`/`night` profiles must be exported from the VimbaX Viewer (see `config/camera_profiles/rgb/README.md`) and have not been measured against real light. Confirm on the rig that `LoadSettings` succeeds, that `standard` sustains 10 fps with `frames_dropped` at 0, and that the auto-exposure lands on a sensible `ExposureTime` outdoors. The only previous rig test ran with the lens covered. | | 15 | **Camera profiles unverified on a lit scene** | The `standard`/`night` profiles must be exported from the VimbaX Viewer (see `config/camera_profiles/rgb/README.md`) and have not been measured against real light. Confirm on the rig that `LoadSettings` succeeds, that `standard` sustains 10 fps with `frames_dropped` at 0, and that the auto-exposure lands on a sensible `ExposureTime` outdoors. The only previous rig test ran with the lens covered. |
| 16 | **`settle_wait_ms` untuned** | `standard` waits 1 s and `night` 2.5 s after standstill before keeping a frame. Both are reasoned (≫ one frame period; long enough for the auto-exposure to see the new scene) but not measured. Check on a lit scene that the kept frame shows no slew blur and that its exposure has actually converged for that heading, and shorten the wait if it has. | | 16 | **`settle_wait_ms` untuned** | `standard` waits 1 s and `night` 2.5 s after standstill before keeping a frame. Both are reasoned (≫ one frame period; long enough for the auto-exposure to see the new scene) but not measured. Check on a lit scene that the kept frame shows no slew blur and that its exposure has actually converged for that heading, and shorten the wait if it has. |
| 19 | **USB preconditions do not survive a reboot** | `usbcore.usbfs_memory_mb` reverts to 16 (needs 1000) and USB3 LPM re-enables on the camera's port. **Both were wrong on the LattePanda when tested on 2026-08-07**, and frame delivery stalls without them regardless of application code. **Solved on tower 121 by the setup kit** (`hardware/lattepanda-iota/preconditions/`): a systemd-tmpfiles drop-in for usbfs and two udev rules, all installed by `install.sh --preconditions`. Verified 2026-09-23 — `usbfs_memory_mb` reads 1000 after **6 days of uptime**, so it survived the boot, and both rules are in `/etc/udev/rules.d/`. Note it is **tmpfiles, not the kernel cmdline** the original entry proposed. **Still open on the Delta fleet**, which has not had the kit applied. | | 19 | **USB preconditions do not survive a reboot** | `usbcore.usbfs_memory_mb` reverts to 16 (needs 1000) and USB3 LPM re-enables on the camera's port. **Both were wrong on the LattePanda when tested on 2026-08-07**, and frame delivery stalls without them regardless of application code. Needs the udev rule + kernel cmdline persistence described under *Camera acquisition* below. |
## Camera acquisition (USB3) — root cause + approach ## Camera acquisition (USB3) — root cause + approach
@ -100,18 +100,6 @@ scripts for this are worth keeping; note that wrapping them in `timeout` SIGTERM
and real motor captured 15/15 waypoints, one software-triggered frame each, 0 dropped and 0 stalls. What and real motor captured 15/15 waypoints, one software-triggered frame each, 0 dropped and 0 stalls. What
this does **not** cover is image *content* — the lens was covered, so exposure/blur behaviour on a real this does **not** cover is image *content* — the lens was covered, so exposure/blur behaviour on a real
scene remains open (#15–#17). scene remains open (#15–#17).
- **Log rotation verified on the rig (2026-09-23); pruning is not.** With
`[Logging] file_max_mb = 1` on tower 121, a segment closed at 1048513 bytes against
the 1048576 cap — rotating *before* the write that would cross it, on a whole line —
and the next segment re-emitted the header. Retention (`file_keep`) needs a fourth
segment to observe a deletion, roughly three hours at that cap, and was not run.
`tests/test_filelogsink.cpp` covers it directly.
- **The positive-travel guard has never fired in anger.** `checkTravelFitsPositive()`
catches a `center_count` smaller than half the homed travel, which puts the negative
end below zero. It was tested against real numbers and shipped *after* 121's centre
was raised to 1000000, so no tower has ever tripped it. Whether the TMC5160 actually
misbehaves below zero is still not established — see the note in
`include/fgc/HardwareChecks.h`.
- **Makefile parity** is moot: the Makefile was removed in favour of CMake, and the full - **Makefile parity** is moot: the Makefile was removed in favour of CMake, and the full
`WITH_VIMBA=ON WITH_MQTT=ON` CMake build now runs on the device. `WITH_VIMBA=ON WITH_MQTT=ON` CMake build now runs on the device.
- **Demo mode** copies `bin/x64/Release/test_smoke.jxl`, resolved relative to the working directory. Run from a - **Demo mode** copies `bin/x64/Release/test_smoke.jxl`, resolved relative to the working directory. Run from a

View File

@ -18,15 +18,6 @@ Keep entries dated; move shipped items into the reference docs.
(peak/mean CS, SG, PWM during a move) so the friction/balance/homing-time (peak/mean CS, SG, PWM during a move) so the friction/balance/homing-time
metrics come off the firmware clock instead of host `ST` sampling; plus a metrics come off the firmware clock instead of host `ST` sampling; plus a
`PING`/`PONG` link-health command for `test system link`. `PING`/`PONG` link-health command for `test system link`.
- ⚠️ **This collides with AVR flash headroom.** Three new commands are the
kind of growth the ATmega32U4 no longer has room for — it sits at **79.8 %
of its 28 KB** (5828 B free, measured 2026-09-23), up from 19254 B at the
phase 2 gate. The RP2040 is a non-issue at 0.6 % of 8 MB. So either these
land RP2040-only behind the `platform_*.h` split, or the Delta fleet gets a
deliberate answer on flash first — `PROGMEM` for the string tables, dropping
a command, or accepting that the Delta is feature-frozen. Note §14 of the
config plan *cost* 222 B rather than freeing any, so the obvious saving has
already been spent. See [hardware-porting-plan.md](hardware-porting-plan.md).
## Planned components & integrations ## Planned components & integrations

View File

@ -54,7 +54,7 @@ While a test runs, interfering commands (moves, homing, a second test, capture,
| `host` | `memory` | available RAM / swap (`/proc/meminfo`) | | `host` | `memory` | available RAM / swap (`/proc/meminfo`) |
| `host` | `load` | load average vs CPU count | | `host` | `load` | load average vs CPU count |
| `hardware` | `axes` | the axes `[Hardware] axes` declares appear in telemetry, **and no others** | | `hardware` | `axes` | the axes `[Hardware] axes` declares appear in telemetry, **and no others** |
| `hardware` | `motor` | `DUMP` echoes **everything** `hardware.ini` declares — current, `steps_per_rev`, `enc_deviation`, `sw_mode` and all three speeds — catching a stale flash, a lost `SET`, or an axis that never left `NEEDCFG`. Also checks the axis is not on the firmware's **limp-home** fallback, which `USE DEFAULTS` leaves looking fully configured | | `hardware` | `motor` | `DUMP` echoes **everything** `hardware.ini` declares — current, `steps_per_rev`, `enc_deviation`, `sw_mode` and all three speeds — catching a stale flash, a lost `SET`, or an axis that never left `NEEDCFG` |
| `hardware` | `imu` | a declared IMU is connected and a sample arrives | | `hardware` | `imu` | a declared IMU is connected and a sample arrives |
| `hardware` | `env` | a declared ambient sensor sits on a **real I2C** adapter (not SMBus-only) and returns a CRC-valid reading | | `hardware` | `env` | a declared ambient sensor sits on a **real I2C** adapter (not SMBus-only) and returns a CRC-valid reading |
| `hardware` | `camera` | every configured camera opened; `usbfs_memory_mb` and USB3 LPM hold | | `hardware` | `camera` | every configured camera opened; `usbfs_memory_mb` and USB3 LPM hold |
@ -99,21 +99,12 @@ Three rules hold across every leaf:
look identical from the sensor's side, and the distinction is what look identical from the sensor's side, and the distinction is what
known-issues #2 cost to learn. known-issues #2 cost to learn.
The `motor` leaf reports three metrics per axis. `<axis>_not_limp` asks where the The `motor` leaf reports two metrics per axis. `<axis>_current` covers
values came from; `<axis>_current` covers `GLOBALSCALER`/`IRUN`/`homing IRUN`; and `GLOBALSCALER`/`IRUN`/`homing IRUN`; `<axis>_declared` covers everything else the
`<axis>_declared` covers everything else the host owns. All three treat an host owns. Both treat an **absent** field as unconfirmable rather than as a
**absent** field as unconfirmable rather than as a mismatch — comparing against a mismatch — comparing against a missing value would report "firmware has -1", which
missing value would report "firmware has -1", which reads as a wrong setting and reads as a wrong setting and sends someone to re-send a command that was never the
sends someone to re-send a command that was never the problem. problem.
`<axis>_not_limp` runs first because it explains the other two. `USE DEFAULTS` is
the deliberate bench escape from `NEEDCFG`, and it fills the firmware's `cfg_rx`
mask completely — so an axis running limp-home values leaves `NEEDCFG`, reports
`READY`, and passes every state check while running numbers that describe no real
drivetrain. Without this metric the only symptom is `<axis>_declared` listing six
mismatches, which reads as "host and firmware disagree" and sends someone to
re-send values that were never sent at all. The fix is a host restart, which
re-sends the whole declared block.
The `motor` leaf is the one worth running after every flash: `GLOBALSCALER` and The `motor` leaf is the one worth running after every flash: `GLOBALSCALER` and
`IRUN` are **write-only** on the TMC5160, so the `DUMP` echo is the only evidence `IRUN` are **write-only** on the TMC5160, so the `DUMP` echo is the only evidence

View File

@ -360,17 +360,10 @@ SERIAL_PORT=/dev/serial/by-id/usb-...
./deploy.sh --monitor # watch it boot ./deploy.sh --monitor # watch it boot
``` ```
**Confirm the pin map before flashing a board that is wired to motors.** A **Confirm the pin map before flashing a board that is wired to motors.** The map
chip-select on the wrong pin writes into a driver that was never meant to hear lives in `src/platform_iota.h` and a chip-select on the wrong pin writes into a
it. The pins are the `[Pins]` section of this tower's `hardware.ini`: the build driver that was never meant to hear it. The numbers there are the loom as built
reads them from `/etc/fire_gimbal_control/hardware.ini` (`firmware/tools/gen_wiring.py`) on tower 121, which is **not** the map proposed in the porting plan.
and **fails without them**, so add the section before the first build. The
reference loom, with the table saying which wire goes where, is
[`hardware/lattepanda-iota/pins.ini`](../hardware/lattepanda-iota/pins.ini) -
the loom as built on tower 121, which is **not** the map proposed in the porting
plan. Copy it in if the tower is wired the same way. The generator refuses pins
SPI0 cannot reach, and `test hardware motor` compares the declared pins with the
ones the running firmware reports it was built with.
### Getting a factory board to accept its first flash ### Getting a factory board to accept its first flash

View File

@ -1,17 +0,0 @@
; The reference loom for a LattePanda 3 Delta tower: ATmega32U4 (Leonardo) ->
; two TMC5160 driver boards, yaw and pitch.
;
; NOT TRANSCRIBED FROM A TOWER. These are the numbers the firmware carried in
; src/platform_leonardo.h before pins moved into hardware.ini, which is what
; the Delta fleet has been running on. Nobody has yet traced the loom on a Delta
; the way tower 121's was traced, so there is no pin -> driver-pin table here.
; Check it against tower 111 and replace this paragraph with the table.
;
; SPI is on the ICSP header (MISO 14, SCK 15, MOSI 16), fixed by the hardware,
; so there are no spi_* keys; firmware/tools/gen_wiring.py refuses them, and
; refuses 14-17 as a chip-select or enable.
[Pins]
yaw_cs = 8
yaw_en = 7
pitch_cs = 1
pitch_en = 0

View File

@ -1,27 +0,0 @@
; The reference loom for a LattePanda IOTA tower: RP2040 -> one TMC5160-BOB.
; AS BUILT on tower 10.11.12.121, transcribed 2026-09-15. Not the map proposed
; in docs/hardware-porting-plan.md section 2 - that one was never wired.
;
; IOTA Function TMC5160-BOB
; 3.3V logic supply pin 1 VCC_IO (3.3 V: the RP2040 is NOT 5 V tolerant)
; GND signal ground pin 2 GND
; GP1 SPI chip select pin 3 CSN
; GP2 SPI clock pin 4 SCK
; GP3 SPI MOSI pin 5 SDI
; GP0 SPI MISO pin 6 SDO
; GP4 driver enable pin 9 DRV_ENN
;
; The SPI pins are not a free choice. The firmware drives SPI0, which reaches
; SCK {2,6,18,22}, MOSI {3,7,19,23}, MISO {0,4,16,20} and nothing else;
; firmware/tools/gen_wiring.py refuses anything outside those at build time.
; GP1 is SPI0's hardware CSn, though the driver toggles it in software anyway.
;
; The installer copies this block into hardware.ini when the tower is wired to
; this loom. A tower wired differently gets its own numbers - and the comments
; beside them should then say what each wire really goes to.
[Pins]
spi_sck = 2 ; -> BOB pin 4 SCK
spi_mosi = 3 ; -> BOB pin 5 SDI
spi_miso = 0 ; -> BOB pin 6 SDO
yaw_cs = 1 ; -> BOB pin 3 CSN
yaw_en = 4 ; -> BOB pin 9 DRV_ENN

View File

@ -48,18 +48,6 @@ struct HardwareConfig {
// Where it came from, for logging and for `test hardware` to cite. // Where it came from, for logging and for `test hardware` to cite.
std::string source_path; std::string source_path;
// [Pins] - how the MCU is wired to each driver board. The host never sends
// these: the firmware is BUILT from them (firmware/tools/gen_wiring.py reads
// the same file on the tower). Held here only so `test hardware motor` can
// compare them with the pins the running firmware reports it was built with.
// -1 = not declared.
struct Pins {
long yaw_cs = -1, yaw_en = -1, pitch_cs = -1, pitch_en = -1;
long spi_sck = -1, spi_mosi = -1, spi_miso = -1; // RP2040 only
bool declared() const { return yaw_cs >= 0 || yaw_en >= 0 || pitch_cs >= 0 ||
pitch_en >= 0 || spi_sck >= 0; }
} pins;
// True when `name` is listed in axes. An UNDECLARED rig answers true for // True when `name` is listed in axes. An UNDECLARED rig answers true for
// everything: with nothing stated there is nothing to contradict, and callers // everything: with nothing stated there is nothing to contradict, and callers
// must keep behaving exactly as they did before. // must keep behaving exactly as they did before.

View File

@ -42,15 +42,6 @@ struct DumpAxis {
// Bitmask over the firmware's CfgKey: which required keys have arrived. // Bitmask over the firmware's CfgKey: which required keys have arrived.
// -1 when absent. // -1 when absent.
long cfg_received = -1; long cfg_received = -1;
// Is the axis running the firmware's limp-home fallback rather than anything
// this host declared? USE DEFAULTS fills cfg_received with the whole required
// mask, so that field alone cannot tell the two apart - a bench board reads as
// fully configured. -1 when absent, i.e. a firmware predating the flag.
long limp_home = -1;
// The chip-select and enable pins the running image was BUILT with. -1 when
// absent, i.e. a firmware that predates the pin echo.
long cs_pin = -1;
long en_pin = -1;
// Encoder-tracking health accumulated by the firmware hold corrector. // Encoder-tracking health accumulated by the firmware hold corrector.
long hold_corrections = 0; // # of hold resyncs since firmware boot/RESET long hold_corrections = 0; // # of hold resyncs since firmware boot/RESET
long hold_peak_dev = 0; // largest deviation seen while holding (counts) long hold_peak_dev = 0; // largest deviation seen while holding (counts)
@ -89,9 +80,6 @@ struct DumpData {
unsigned mcusr = 0; unsigned mcusr = 0;
std::string reset_text; // empty => firmware did not name it std::string reset_text; // empty => firmware did not name it
long free_ram = 0; long free_ram = 0;
// SPI pins the image was built with - RP2040 only, where they are a choice.
// -1 when absent: a Leonardo, or firmware predating the echo.
long spi_sck = -1, spi_mosi = -1, spi_miso = -1;
std::vector<std::string> reset_flags; std::vector<std::string> reset_flags;
std::vector<DumpAxis> axes; std::vector<DumpAxis> axes;
}; };

View File

@ -88,36 +88,6 @@ struct DrivetrainEcho {
CheckResult checkDrivetrainAdopted(const std::string& axis, const DrivetrainExpectation& want, CheckResult checkDrivetrainAdopted(const std::string& axis, const DrivetrainExpectation& want,
const DrivetrainEcho& got); const DrivetrainEcho& got);
// Is the axis running the host's configuration, or the firmware's limp-home
// fallback?
//
// `USE DEFAULTS` is the deliberate bench escape from NEEDCFG, and it fills the
// firmware's cfg_rx mask completely - so an axis on limp-home values reports as
// fully configured and passes every state check. What gives it away is the DUMP
// `limp` flag, which says where the values came from rather than whether they
// arrived.
//
// checkDeclaredValues() would also catch this, but only as a list of mismatched
// numbers, which reads as "the host and firmware disagree" and sends someone to
// re-send them. The truthful diagnosis is that nobody ever sent any.
//
// `limp` < 0 means the firmware predates the flag: reported as not-run, because
// an old firmware is a version skew rather than a misconfigured tower.
CheckResult checkNotLimpHome(const std::string& axis, long limp);
// Is the running firmware built from the wiring hardware.ini declares?
//
// Pins are not sent over the wire: firmware/tools/gen_wiring.py compiles them in
// from the tower's hardware.ini [Pins]. So a mismatch never means a lost SET -
// it means the image was built from a different copy of that file, and the fix
// is to rebuild and re-flash. The firmware drives whatever pins it was built
// with, which on a tower wired differently means a chip-select into a driver
// that was never meant to hear it.
//
// Same shape as checkDeclaredValues: `want` false = not declared, `got` -1 =
// not echoed, i.e. a firmware predating the pin echo.
CheckResult checkPinsBuilt(const std::string& label, const DeclaredValue* values, int count);
// Does the homed travel fit entirely in positive counts? // Does the homed travel fit entirely in positive counts?
// //
// `center_count` defines where the middle of the found travel sits, so the range // `center_count` defines where the middle of the found travel sits, so the range

View File

@ -1,6 +1,5 @@
#pragma once #pragma once
#include "fgc/Config.h"
#include "fgc/IMotorController.h" #include "fgc/IMotorController.h"
#include "fgc/Logger.h" #include "fgc/Logger.h"
@ -29,14 +28,6 @@ public:
// way the handshake is exercised on a dev box with no rig. // way the handshake is exercised on a dev box with no rig.
explicit MockMotorController(bool needcfg = false) : needcfg_(needcfg) {} explicit MockMotorController(bool needcfg = false) : needcfg_(needcfg) {}
// The pins a real image would have been built with, from hardware.ini
// [Pins]. Echoed in DUMP in NEEDCFG mode only, which is the mock's stand-in
// for current firmware; the plain mode is a firmware predating the echo.
void setBuiltPins(const HardwareConfig::Pins& p) {
std::lock_guard<std::mutex> lock(mutex_);
pins_ = p;
}
void start() override { void start() override {
LOG_INFO << "[mock] motor controller started"; LOG_INFO << "[mock] motor controller started";
if (!needcfg_) return; if (!needcfg_) return;
@ -124,10 +115,7 @@ public:
// their meanings. See motor.h. // their meanings. See motor.h.
const int st = (needcfg_ && !configured_) ? 5 : (homed_ ? 3 : 1); const int st = (needcfg_ && !configured_) ? 5 : (homed_ ? 3 : 1);
std::ostringstream d; std::ostringstream d;
d << "DUMP BEGIN build=mock uptime=0 mcusr=0x01 free_ram=4096"; d << "DUMP BEGIN build=mock uptime=0 mcusr=0x01 free_ram=4096\n";
if (needcfg_ && pins_.spi_sck >= 0)
d << " spi=" << pins_.spi_sck << "," << pins_.spi_mosi << "," << pins_.spi_miso;
d << "\n";
auto axis = [&](char L, const AxisTelemetry& a) { auto axis = [&](char L, const AxisTelemetry& a) {
// In NEEDCFG mode, echo back what the host actually SET, so the // In NEEDCFG mode, echo back what the host actually SET, so the
// whole round trip - NEED, SET, DUMP, `test hardware motor` - // whole round trip - NEED, SET, DUMP, `test hardware motor` -
@ -157,11 +145,7 @@ public:
<< " hsub=0 enabled=" << (homed_ ? 1 : 0) << " hsub=0 enabled=" << (homed_ ? 1 : 0)
<< " lim_neg=0 lim_pos=1000000 hold_target=" << a.xenc << " lim_neg=0 lim_pos=1000000 hold_target=" << a.xenc
<< owned << owned
<< " hold_corr=0 hold_peak=0 hold_cumul=0 has_encoder=1"; << " hold_corr=0 hold_peak=0 hold_cumul=0 has_encoder=1\n";
const long cs = L == 'Y' ? pins_.yaw_cs : pins_.pitch_cs;
const long en = L == 'Y' ? pins_.yaw_en : pins_.pitch_en;
if (needcfg_ && cs >= 0) d << " cs=" << cs << " en=" << en;
d << "\n";
d << "DUMP " << L << " TMC GCONF=0x00000004 GSTAT=0x00000000" d << "DUMP " << L << " TMC GCONF=0x00000004 GSTAT=0x00000000"
<< " IOIN=0x30000008 TSTEP=0x000fffff RAMPMODE=0x00000000" << " IOIN=0x30000008 TSTEP=0x000fffff RAMPMODE=0x00000000"
<< " XACTUAL=0x" << std::hex << (a.xactual & 0xffffffff) << " XACTUAL=0x" << std::hex << (a.xactual & 0xffffffff)
@ -243,7 +227,6 @@ public:
private: private:
bool needcfg_ = false; bool needcfg_ = false;
HardwareConfig::Pins pins_;
bool configured_ = false; bool configured_ = false;
std::vector<std::string> need_lines_; std::vector<std::string> need_lines_;
std::map<std::string, std::string> settings_; std::map<std::string, std::string> settings_;

View File

@ -306,11 +306,7 @@ struct Application::Impl {
std::unique_ptr<IMotorController> makeMotor() { std::unique_ptr<IMotorController> makeMotor() {
bool mock = opts.mock_serial.value_or(cfg.features.mock_serial); bool mock = opts.mock_serial.value_or(cfg.features.mock_serial);
if (mock) { if (mock) return std::make_unique<MockMotorController>(cfg.features.mock_needcfg);
auto m = std::make_unique<MockMotorController>(cfg.features.mock_needcfg);
m->setBuiltPins(cfg.hardware.pins);
return m;
}
return std::make_unique<SerialMotorController>(cfg.serial.device, cfg.serial.baud); return std::make_unique<SerialMotorController>(cfg.serial.device, cfg.serial.baud);
} }

View File

@ -246,16 +246,6 @@ AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
cfg.hardware.cameras = splitList(get(kv, "Hardware.cameras")); cfg.hardware.cameras = splitList(get(kv, "Hardware.cameras"));
cfg.hardware.imu = lowered(get(kv, "Hardware.imu")); cfg.hardware.imu = lowered(get(kv, "Hardware.imu"));
cfg.hardware.env = lowered(get(kv, "Hardware.env")); cfg.hardware.env = lowered(get(kv, "Hardware.env"));
{
auto& p = cfg.hardware.pins;
p.yaw_cs = getInt(kv, "Pins.yaw_cs", -1);
p.yaw_en = getInt(kv, "Pins.yaw_en", -1);
p.pitch_cs = getInt(kv, "Pins.pitch_cs", -1);
p.pitch_en = getInt(kv, "Pins.pitch_en", -1);
p.spi_sck = getInt(kv, "Pins.spi_sck", -1);
p.spi_mosi = getInt(kv, "Pins.spi_mosi", -1);
p.spi_miso = getInt(kv, "Pins.spi_miso", -1);
}
for (const auto& a : cfg.hardware.axes) for (const auto& a : cfg.hardware.axes)
if (a != "yaw" && a != "pitch") if (a != "yaw" && a != "pitch")
throw std::runtime_error("Hardware.axes: unknown axis '" + a + throw std::runtime_error("Hardware.axes: unknown axis '" + a +

View File

@ -1,7 +1,6 @@
#include "fgc/DumpParser.h" #include "fgc/DumpParser.h"
#include <array> #include <array>
#include <cstdio>
#include <sstream> #include <sstream>
#include <utility> #include <utility>
@ -121,15 +120,6 @@ DumpData parseDump(const std::string& block) {
d.mcusr = static_cast<unsigned>(asInt(get(hdr, "mcusr", "0"))); d.mcusr = static_cast<unsigned>(asInt(get(hdr, "mcusr", "0")));
d.free_ram = asInt(get(hdr, "free_ram", "0")); d.free_ram = asInt(get(hdr, "free_ram", "0"));
d.reset_text = get(hdr, "reset"); d.reset_text = get(hdr, "reset");
{
// "spi=<sck>,<mosi>,<miso>". All three or none: a partial triple is a
// garbled line, and reporting two real pins beside a -1 would mislead.
const std::string spi = get(hdr, "spi");
long v[3];
if (std::sscanf(spi.c_str(), "%ld,%ld,%ld", &v[0], &v[1], &v[2]) == 3) {
d.spi_sck = v[0]; d.spi_mosi = v[1]; d.spi_miso = v[2];
}
}
d.reset_flags = bitsSet(d.mcusr, kMcusr); d.reset_flags = bitsSet(d.mcusr, kMcusr);
// Per-axis lines: "DUMP <Y|P> state=.." and "DUMP <Y|P> TMC ..". // Per-axis lines: "DUMP <Y|P> state=.." and "DUMP <Y|P> TMC ..".
@ -186,9 +176,6 @@ DumpData parseDump(const std::string& block) {
ax.center_speed = asInt(get(m, "center_speed", "-1")); ax.center_speed = asInt(get(m, "center_speed", "-1"));
ax.sw_mode = asInt(get(m, "sw_mode", "-1")); ax.sw_mode = asInt(get(m, "sw_mode", "-1"));
ax.cfg_received = asInt(get(m, "cfg_rx", "-1"), -1); ax.cfg_received = asInt(get(m, "cfg_rx", "-1"), -1);
ax.limp_home = asInt(get(m, "limp", "-1"), -1);
ax.cs_pin = asInt(get(m, "cs", "-1"), -1);
ax.en_pin = asInt(get(m, "en", "-1"), -1);
ax.hold_corrections = asInt(get(m, "hold_corr", "0")); ax.hold_corrections = asInt(get(m, "hold_corr", "0"));
ax.hold_peak_dev = asInt(get(m, "hold_peak", "0")); ax.hold_peak_dev = asInt(get(m, "hold_peak", "0"));
ax.hold_cumulative_dev = asInt(get(m, "hold_cumul", "0")); ax.hold_cumulative_dev = asInt(get(m, "hold_cumul", "0"));
@ -235,11 +222,6 @@ std::vector<std::string> formatDump(const DumpData& d) {
<< " limits=[" << ax.lim_neg << ".." << ax.lim_pos << "]" << " limits=[" << ax.lim_neg << ".." << ax.lim_pos << "]"
<< " hold_target=" << ax.hold_target << " hold_target=" << ax.hold_target
<< " has_encoder=" << (ax.has_encoder ? 1 : 0); << " has_encoder=" << (ax.has_encoder ? 1 : 0);
if (ax.cs_pin >= 0) os << " cs=" << ax.cs_pin << " en=" << ax.en_pin;
// Shown only when set. It is a fault on a tower - the axis is on the
// firmware's fallback values, not hardware.ini's - and printing
// "limp=0" on every healthy dump would bury it.
if (ax.limp_home > 0) os << " LIMP-HOME (not hardware.ini's values)";
out.push_back(os.str()); out.push_back(os.str());
} }
for (const char* k : {"GCONF", "CHOPCONF", "XACTUAL", "X_ENC", "XTARGET", "VACTUAL"}) { for (const char* k : {"GCONF", "CHOPCONF", "XACTUAL", "X_ENC", "XTARGET", "VACTUAL"}) {

View File

@ -79,28 +79,6 @@ CheckResult checkI2cFunctionality(unsigned long funcs) {
return r; return r;
} }
CheckResult checkNotLimpHome(const std::string& axis, long limp) {
CheckResult r;
if (limp < 0) {
r.ran = false;
r.detail = axis +
": firmware does not report the limp flag, so whether it is running "
"hardware.ini's values or its own fallback cannot be told apart here";
return r;
}
r.pass = (limp == 0);
r.detail = r.pass
? axis + ": running the host's declared values"
: axis +
": running LIMP-HOME values, which describe no real drivetrain -- slow "
"speeds, a reduced search current and a placeholder travel. Something "
"sent USE DEFAULTS, so hardware.ini is NOT in force however healthy the "
"axis looks. Restart the host to re-send it";
return r;
}
CheckResult checkDrivetrainAdopted(const std::string& axis, const DrivetrainExpectation& want, CheckResult checkDrivetrainAdopted(const std::string& axis, const DrivetrainExpectation& want,
const DrivetrainEcho& got) { const DrivetrainEcho& got) {
CheckResult r; CheckResult r;
@ -206,43 +184,6 @@ CheckResult checkDeclaredValues(const std::string& axis, const DeclaredValue* va
return r; return r;
} }
CheckResult checkPinsBuilt(const std::string& label, const DeclaredValue* values, int count) {
CheckResult r;
int checked = 0, absent = 0;
std::string diff;
for (int i = 0; i < count; ++i) {
const DeclaredValue& v = values[i];
if (!v.want) continue;
if (v.got < 0) { ++absent; continue; }
++checked;
if (v.got != v.value) {
if (!diff.empty()) diff += ", ";
diff += std::string(v.name) + " declared " + std::to_string(v.value) +
", built with " + std::to_string(v.got);
}
}
if (checked == 0 && absent == 0) {
r.ran = false;
r.detail = label + ": no [Pins] declared";
return r;
}
if (!diff.empty()) {
r.pass = false;
r.detail = label + ": " + diff +
" -- the firmware was built from different wiring than hardware.ini "
"declares. Rebuild and re-flash it";
return r;
}
if (absent > 0) {
r.pass = false;
r.detail = label + ": the firmware does not report the pins it was built with, so its "
"wiring cannot be confirmed -- it predates the pin echo. Re-flash it";
return r;
}
r.detail = label + ": built with the declared pins";
return r;
}
CheckResult checkTravelFitsPositive(const std::string& axis, long center, long half_travel) { CheckResult checkTravelFitsPositive(const std::string& axis, long center, long half_travel) {
CheckResult r; CheckResult r;
if (half_travel <= 0) { if (half_travel <= 0) {

View File

@ -828,7 +828,6 @@ long echoSwMode(const DumpData& d, char c) { return axisField(d, c, &DumpA
long echoHomingSpeed(const DumpData& d, char c) { return axisField(d, c, &DumpAxis::homing_speed); } long echoHomingSpeed(const DumpData& d, char c) { return axisField(d, c, &DumpAxis::homing_speed); }
long echoCenterSpeed(const DumpData& d, char c) { return axisField(d, c, &DumpAxis::center_speed); } long echoCenterSpeed(const DumpData& d, char c) { return axisField(d, c, &DumpAxis::center_speed); }
long echoSpeed(const DumpData& d, char c) { return axisField(d, c, &DumpAxis::speed); } long echoSpeed(const DumpData& d, char c) { return axisField(d, c, &DumpAxis::speed); }
long echoLimp(const DumpData& d, char c) { return axisField(d, c, &DumpAxis::limp_home); }
// The VMAX a declared deg/s becomes on this axis, or 0 when it cannot be formed. // The VMAX a declared deg/s becomes on this axis, or 0 when it cannot be formed.
// Mirrors Application's fallback: the live calibration when it is trustworthy, // Mirrors Application's fallback: the live calibration when it is trustworthy,
@ -926,12 +925,6 @@ void TestRunner::testHwMotor(TestReport& r) {
} }
auto check_axis = [&](const char* name, char code, const AxisDrivetrainConfig& cfg) { auto check_axis = [&](const char* name, char code, const AxisDrivetrainConfig& cfg) {
// First, because it explains the rest. An axis on limp-home values will
// fail every comparison below, and that list of mismatched numbers reads
// as "host and firmware disagree" - sending someone to re-send values
// that were never sent at all.
verdict(s, std::string(name) + "_not_limp", checkNotLimpHome(name, echoLimp(d, code)));
DrivetrainExpectation want; DrivetrainExpectation want;
if (cfg.run_current_ma > 0.0 && cfg.r_sense_ohm > 0.0) { if (cfg.run_current_ma > 0.0 && cfg.r_sense_ohm > 0.0) {
const auto plan = planTmcCurrent(cfg.r_sense_ohm, cfg.run_current_ma, const auto plan = planTmcCurrent(cfg.r_sense_ohm, cfg.run_current_ma,
@ -969,35 +962,10 @@ void TestRunner::testHwMotor(TestReport& r) {
}; };
verdict(s, std::string(name) + "_declared", verdict(s, std::string(name) + "_declared",
checkDeclaredValues(name, vals, (int)(sizeof(vals) / sizeof(vals[0])))); checkDeclaredValues(name, vals, (int)(sizeof(vals) / sizeof(vals[0]))));
// The wiring the image was built from, which no SET can change.
if (hw_.declared) {
const auto& p = hw_.declared->pins;
const bool yaw = code == 'Y';
long cs = -1, en = -1;
for (const auto& ax : d.axes)
if (ax.axis == code) { cs = ax.cs_pin; en = ax.en_pin; }
const DeclaredValue pins[] = {
{"cs", (yaw ? p.yaw_cs : p.pitch_cs) >= 0, yaw ? p.yaw_cs : p.pitch_cs, cs},
{"en", (yaw ? p.yaw_en : p.pitch_en) >= 0, yaw ? p.yaw_en : p.pitch_en, en},
};
verdict(s, std::string(name) + "_pins", checkPinsBuilt(name, pins, 2));
}
}; };
check_axis("yaw", 'Y', hw_.drive->yaw); check_axis("yaw", 'Y', hw_.drive->yaw);
if (!hw_.declared || hw_.declared->hasAxis("pitch")) check_axis("pitch", 'P', hw_.drive->pitch); if (!hw_.declared || hw_.declared->hasAxis("pitch")) check_axis("pitch", 'P', hw_.drive->pitch);
// SPI is a choice only on the RP2040, so only there is it declared and echoed.
if (hw_.declared && hw_.declared->pins.spi_sck >= 0) {
const auto& p = hw_.declared->pins;
const DeclaredValue spi[] = {
{"spi_sck", true, p.spi_sck, d.spi_sck},
{"spi_mosi", p.spi_mosi >= 0, p.spi_mosi, d.spi_mosi},
{"spi_miso", p.spi_miso >= 0, p.spi_miso, d.spi_miso},
};
verdict(s, "spi_pins", checkPinsBuilt("spi", spi, 3));
}
rollup(s); rollup(s);
r.sections.push_back(std::move(s)); r.sections.push_back(std::move(s));
} }

View File

@ -582,25 +582,6 @@ TEST_CASE("Hardware.axes parsing and membership") {
} }
} }
TEST_CASE("[Pins] is read for comparison, and absent reads as undeclared") {
auto c = ConfigLoader::fromMap({{"Camera.rgb.id", "D"},
{"Camera.rgb.profile_dir", "/tmp/p"},
{"Pins.yaw_cs", "1"},
{"Pins.yaw_en", "4"},
{"Pins.spi_sck", "2"},
{"Pins.spi_miso", "0"}});
CHECK(c.hardware.pins.declared());
CHECK(c.hardware.pins.yaw_cs == 1);
CHECK(c.hardware.pins.yaw_en == 4);
CHECK(c.hardware.pins.spi_sck == 2);
CHECK(c.hardware.pins.spi_miso == 0); // GP0 is a pin, not "unset"
CHECK(c.hardware.pins.pitch_cs == -1);
auto none = ConfigLoader::fromMap({{"Camera.rgb.id", "D"},
{"Camera.rgb.profile_dir", "/tmp/p"}});
CHECK_FALSE(none.hardware.pins.declared());
}
TEST_CASE("A mount angle that cannot be a rotation is rejected") { TEST_CASE("A mount angle that cannot be a rotation is rejected") {
auto with = [](const std::string& k, const std::string& v) { auto with = [](const std::string& k, const std::string& v) {
return ConfigLoader::fromMap({{"Camera.rgb.id", "D"}, return ConfigLoader::fromMap({{"Camera.rgb.id", "D"},

View File

@ -1,7 +1,6 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "fgc/DumpParser.h" #include "fgc/DumpParser.h"
#include "fgc/mock/MockMotorController.h"
#include <algorithm> #include <algorithm>
#include <string> #include <string>
@ -261,61 +260,3 @@ TEST_CASE("without that field the AVR decode is labelled as an assumption") {
CHECK(out.find("PORF") != std::string::npos); // still decoded CHECK(out.find("PORF") != std::string::npos); // still decoded
CHECK(out.find("assumed AVR") != std::string::npos); // but not stated as fact CHECK(out.find("assumed AVR") != std::string::npos); // but not stated as fact
} }
TEST_CASE("the pins the firmware was built with are read back from DUMP") {
const std::string text =
"DUMP BEGIN build=abc uptime=1 mcusr=0x3 reset=soft free_ram=4096 spi=2,3,0\n"
"DUMP Y state=2 hsub=0 enabled=1 lim_neg=0 lim_pos=1000000 hold_target=0 speed=1 "
"center=500000 soft_margin=5000 hold_corr=0 hold_peak=0 hold_cumul=0 has_encoder=1 "
"cs=1 en=4\n"
"DUMP END\n";
auto d = fgc::parseDump(text);
REQUIRE(d.valid);
CHECK(d.spi_sck == 2);
CHECK(d.spi_mosi == 3);
CHECK(d.spi_miso == 0); // GP0 is a real pin, not "absent"
REQUIRE(d.axes.size() == 1);
CHECK(d.axes[0].cs_pin == 1);
CHECK(d.axes[0].en_pin == 4);
CHECK(d.axes[0].has_encoder); // no longer the last field, still parsed
}
TEST_CASE("a firmware predating the pin echo reports -1, and a Leonardo has no spi") {
auto d = fgc::parseDump(kDump);
REQUIRE(d.valid);
CHECK(d.spi_sck == -1);
CHECK(d.spi_miso == -1);
for (const auto& a : d.axes) {
CHECK(a.cs_pin == -1);
CHECK(a.en_pin == -1);
}
}
TEST_CASE("a garbled spi triple is absent, not half-read") {
auto d = fgc::parseDump("DUMP BEGIN build=x uptime=1 mcusr=0x0 free_ram=1 spi=2,3\nDUMP END\n");
REQUIRE(d.valid);
CHECK(d.spi_sck == -1);
}
TEST_CASE("the mock echoes declared pins only when standing in for current firmware") {
fgc::HardwareConfig::Pins p;
p.yaw_cs = 1; p.yaw_en = 4; p.spi_sck = 2; p.spi_mosi = 3; p.spi_miso = 0;
fgc::MockMotorController current(/*needcfg=*/true);
current.setBuiltPins(p);
current.sendCommand("DUMP");
auto d = fgc::parseDump(current.lastDump());
REQUIRE(d.valid);
CHECK(d.spi_sck == 2);
CHECK(d.axes.at(0).cs_pin == 1);
CHECK(d.axes.at(0).en_pin == 4);
CHECK(d.axes.at(1).cs_pin == -1); // no pitch pins declared
fgc::MockMotorController old; // predates the echo, as real old firmware does
old.setBuiltPins(p);
old.sendCommand("DUMP");
auto o = fgc::parseDump(old.lastDump());
REQUIRE(o.valid);
CHECK(o.spi_sck == -1);
CHECK(o.axes.at(0).cs_pin == -1);
}

View File

@ -41,35 +41,6 @@ TEST_CASE("an SMBus-only adapter is caught before it looks like a dead sensor")
CHECK(smbus.detail.find("i2cdetect -F") != std::string::npos); CHECK(smbus.detail.find("i2cdetect -F") != std::string::npos);
} }
TEST_CASE("a tower on limp-home values is a fault, however healthy it looks") {
// The case this exists for: USE DEFAULTS fills the firmware's cfg_rx mask
// completely, so the axis leaves NEEDCFG, reports READY and passes every
// state check - while running values that describe no real drivetrain.
auto r = checkNotLimpHome("yaw", 1);
CHECK(r.ran);
CHECK_FALSE(r.pass);
CHECK(r.detail.find("LIMP-HOME") != std::string::npos);
// It must send the reader to the cause, not to the symptom. "Re-send the
// values" is the wrong instruction when nobody ever sent any.
CHECK(r.detail.find("USE DEFAULTS") != std::string::npos);
CHECK(r.detail.find("Restart the host") != std::string::npos);
}
TEST_CASE("an axis on the host's own values passes") {
auto r = checkNotLimpHome("yaw", 0);
CHECK(r.ran);
CHECK(r.pass);
}
TEST_CASE("a firmware with no limp flag is a version skew, not a passing tower") {
// Absent must not read as limp=0. A firmware predating the flag genuinely
// cannot answer the question, and claiming PASS here would assert something
// the wire never said - the same -1-is-not-zero rule the current echo uses.
auto r = checkNotLimpHome("yaw", -1);
CHECK_FALSE(r.ran);
CHECK(r.detail.find("cannot be told apart") != std::string::npos);
}
TEST_CASE("an undeclared axis has no drivetrain skew to find") { TEST_CASE("an undeclared axis has no drivetrain skew to find") {
DrivetrainExpectation want; // declared == false DrivetrainExpectation want; // declared == false
DrivetrainEcho got; // all -1 DrivetrainEcho got; // all -1
@ -250,37 +221,3 @@ TEST_CASE("an un-homed axis is not checked") {
CHECK_FALSE(r.ran); CHECK_FALSE(r.ran);
CHECK(r.pass); CHECK(r.pass);
} }
// --- the wiring the image was built from ------------------------------------
TEST_CASE("pins built as declared pass") {
const DeclaredValue v[] = {{"cs", true, 1, 1}, {"en", true, 4, 4}};
auto r = checkPinsBuilt("yaw", v, 2);
CHECK(r.ran);
CHECK(r.pass);
}
TEST_CASE("a pin mismatch blames the build, not a lost SET") {
// Pins never travel over the wire, so "a SET was lost" would send someone
// looking in the wrong place. The only fix is a rebuild from this file.
const DeclaredValue v[] = {{"cs", true, 1, 5}, {"en", true, 4, 4}};
auto r = checkPinsBuilt("yaw", v, 2);
CHECK_FALSE(r.pass);
CHECK(r.detail.find("cs declared 1, built with 5") != std::string::npos);
CHECK(r.detail.find("Rebuild and re-flash") != std::string::npos);
CHECK(r.detail.find("SET") == std::string::npos);
}
TEST_CASE("a firmware that does not echo its pins cannot pass") {
const DeclaredValue v[] = {{"cs", true, 1, -1}, {"en", true, 4, -1}};
auto r = checkPinsBuilt("yaw", v, 2);
CHECK(r.ran);
CHECK_FALSE(r.pass);
CHECK(r.detail.find("predates the pin echo") != std::string::npos);
}
TEST_CASE("no [Pins] declared is not checked") {
const DeclaredValue v[] = {{"cs", false, 0, 1}};
auto r = checkPinsBuilt("yaw", v, 1);
CHECK_FALSE(r.ran);
}