Compare commits
3 Commits
12e23a5fb6
...
4049a85b9b
| Author | SHA1 | Date |
|---|---|---|
|
|
4049a85b9b | |
|
|
7a7e7ab0a5 | |
|
|
b6308c1eda |
|
|
@ -69,6 +69,26 @@ cameras = rgb
|
|||
device = /dev/serial/by-id/usb-Arduino_LLC_Arduino_Leonardo-if00
|
||||
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]
|
||||
; 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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
`hardware.ini` (`[Pins]`, `[Hardware] axes`) and *generated* into `board_wiring.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.
|
||||
|
||||
|
|
@ -402,6 +402,27 @@ 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
|
||||
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
|
||||
|
||||
- **Per board** → `platform_leonardo.h` / `platform_iota.h`: pin map plus the three AVR-isms
|
||||
|
|
@ -445,6 +466,7 @@ 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 | 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 | 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 |
|
||||
|
||||
**Validated on tower 121.** The handshake was watched live on the rig, not inferred:
|
||||
|
|
|
|||
|
|
@ -133,11 +133,13 @@ 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
|
||||
> BOOTSEL press — one time per board, and worth knowing before someone drives to
|
||||
> 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.
|
||||
> a mast. **That one is still true.**
|
||||
>
|
||||
> **Fixed by a `BOOTSEL` serial command** calling arduino-pico's
|
||||
> The **update** case was the one that had no answer at the time of that table:
|
||||
> 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
|
||||
> day. The full loop now runs with nobody at the board:
|
||||
>
|
||||
|
|
@ -295,7 +297,7 @@ commit `aab1925`.
|
|||
|
||||
| Phase | Item | State |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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** |
|
||||
|
|
@ -303,7 +305,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 | 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 |
|
||||
| 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 |
|
||||
| 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) |
|
||||
|
||||
> **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
|
||||
|
|
|
|||
|
|
@ -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). |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
## Camera acquisition (USB3) — root cause + approach
|
||||
|
||||
|
|
@ -100,6 +100,18 @@ 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
|
||||
this does **not** cover is image *content* — the lens was covered, so exposure/blur behaviour on a real
|
||||
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
|
||||
`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
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ Keep entries dated; move shipped items into the reference docs.
|
|||
(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
|
||||
`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
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ While a test runs, interfering commands (moves, homing, a second test, capture,
|
|||
| `host` | `memory` | available RAM / swap (`/proc/meminfo`) |
|
||||
| `host` | `load` | load average vs CPU count |
|
||||
| `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` |
|
||||
| `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` | `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` | `camera` | every configured camera opened; `usbfs_memory_mb` and USB3 LPM hold |
|
||||
|
|
@ -99,12 +99,21 @@ Three rules hold across every leaf:
|
|||
look identical from the sensor's side, and the distinction is what
|
||||
known-issues #2 cost to learn.
|
||||
|
||||
The `motor` leaf reports two metrics per axis. `<axis>_current` covers
|
||||
`GLOBALSCALER`/`IRUN`/`homing IRUN`; `<axis>_declared` covers everything else the
|
||||
host owns. Both treat an **absent** field as unconfirmable rather than as a
|
||||
mismatch — comparing against a missing value would report "firmware has -1", which
|
||||
reads as a wrong setting and sends someone to re-send a command that was never the
|
||||
problem.
|
||||
The `motor` leaf reports three metrics per axis. `<axis>_not_limp` asks where the
|
||||
values came from; `<axis>_current` covers `GLOBALSCALER`/`IRUN`/`homing IRUN`; and
|
||||
`<axis>_declared` covers everything else the host owns. All three treat an
|
||||
**absent** field as unconfirmable rather than as a mismatch — comparing against a
|
||||
missing value would report "firmware has -1", which reads as a wrong setting and
|
||||
sends someone to re-send a command that was never the 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
|
||||
`IRUN` are **write-only** on the TMC5160, so the `DUMP` echo is the only evidence
|
||||
|
|
|
|||
|
|
@ -360,10 +360,17 @@ SERIAL_PORT=/dev/serial/by-id/usb-...
|
|||
./deploy.sh --monitor # watch it boot
|
||||
```
|
||||
|
||||
**Confirm the pin map before flashing a board that is wired to motors.** The map
|
||||
lives in `src/platform_iota.h` and a chip-select on the wrong pin writes into a
|
||||
driver that was never meant to hear it. The numbers there are the loom as built
|
||||
on tower 121, which is **not** the map proposed in the porting plan.
|
||||
**Confirm the pin map before flashing a board that is wired to motors.** A
|
||||
chip-select on the wrong pin writes into a driver that was never meant to hear
|
||||
it. The pins are the `[Pins]` section of this tower's `hardware.ini`: the build
|
||||
reads them from `/etc/fire_gimbal_control/hardware.ini` (`firmware/tools/gen_wiring.py`)
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
; 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
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
; 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
|
||||
|
|
@ -48,6 +48,18 @@ struct HardwareConfig {
|
|||
// Where it came from, for logging and for `test hardware` to cite.
|
||||
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
|
||||
// everything: with nothing stated there is nothing to contradict, and callers
|
||||
// must keep behaving exactly as they did before.
|
||||
|
|
|
|||
|
|
@ -42,6 +42,15 @@ struct DumpAxis {
|
|||
// Bitmask over the firmware's CfgKey: which required keys have arrived.
|
||||
// -1 when absent.
|
||||
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.
|
||||
long hold_corrections = 0; // # of hold resyncs since firmware boot/RESET
|
||||
long hold_peak_dev = 0; // largest deviation seen while holding (counts)
|
||||
|
|
@ -80,6 +89,9 @@ struct DumpData {
|
|||
unsigned mcusr = 0;
|
||||
std::string reset_text; // empty => firmware did not name it
|
||||
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<DumpAxis> axes;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -88,6 +88,36 @@ struct DrivetrainEcho {
|
|||
CheckResult checkDrivetrainAdopted(const std::string& axis, const DrivetrainExpectation& want,
|
||||
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?
|
||||
//
|
||||
// `center_count` defines where the middle of the found travel sits, so the range
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include "fgc/Config.h"
|
||||
#include "fgc/IMotorController.h"
|
||||
#include "fgc/Logger.h"
|
||||
|
||||
|
|
@ -28,6 +29,14 @@ public:
|
|||
// way the handshake is exercised on a dev box with no rig.
|
||||
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 {
|
||||
LOG_INFO << "[mock] motor controller started";
|
||||
if (!needcfg_) return;
|
||||
|
|
@ -115,7 +124,10 @@ public:
|
|||
// their meanings. See motor.h.
|
||||
const int st = (needcfg_ && !configured_) ? 5 : (homed_ ? 3 : 1);
|
||||
std::ostringstream d;
|
||||
d << "DUMP BEGIN build=mock uptime=0 mcusr=0x01 free_ram=4096\n";
|
||||
d << "DUMP BEGIN build=mock uptime=0 mcusr=0x01 free_ram=4096";
|
||||
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) {
|
||||
// In NEEDCFG mode, echo back what the host actually SET, so the
|
||||
// whole round trip - NEED, SET, DUMP, `test hardware motor` -
|
||||
|
|
@ -145,7 +157,11 @@ public:
|
|||
<< " hsub=0 enabled=" << (homed_ ? 1 : 0)
|
||||
<< " lim_neg=0 lim_pos=1000000 hold_target=" << a.xenc
|
||||
<< owned
|
||||
<< " hold_corr=0 hold_peak=0 hold_cumul=0 has_encoder=1\n";
|
||||
<< " hold_corr=0 hold_peak=0 hold_cumul=0 has_encoder=1";
|
||||
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"
|
||||
<< " IOIN=0x30000008 TSTEP=0x000fffff RAMPMODE=0x00000000"
|
||||
<< " XACTUAL=0x" << std::hex << (a.xactual & 0xffffffff)
|
||||
|
|
@ -227,6 +243,7 @@ public:
|
|||
|
||||
private:
|
||||
bool needcfg_ = false;
|
||||
HardwareConfig::Pins pins_;
|
||||
bool configured_ = false;
|
||||
std::vector<std::string> need_lines_;
|
||||
std::map<std::string, std::string> settings_;
|
||||
|
|
|
|||
|
|
@ -306,7 +306,11 @@ struct Application::Impl {
|
|||
|
||||
std::unique_ptr<IMotorController> makeMotor() {
|
||||
bool mock = opts.mock_serial.value_or(cfg.features.mock_serial);
|
||||
if (mock) return std::make_unique<MockMotorController>(cfg.features.mock_needcfg);
|
||||
if (mock) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -246,6 +246,16 @@ AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
|
|||
cfg.hardware.cameras = splitList(get(kv, "Hardware.cameras"));
|
||||
cfg.hardware.imu = lowered(get(kv, "Hardware.imu"));
|
||||
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)
|
||||
if (a != "yaw" && a != "pitch")
|
||||
throw std::runtime_error("Hardware.axes: unknown axis '" + a +
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "fgc/DumpParser.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
|
|
@ -120,6 +121,15 @@ DumpData parseDump(const std::string& block) {
|
|||
d.mcusr = static_cast<unsigned>(asInt(get(hdr, "mcusr", "0")));
|
||||
d.free_ram = asInt(get(hdr, "free_ram", "0"));
|
||||
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);
|
||||
|
||||
// Per-axis lines: "DUMP <Y|P> state=.." and "DUMP <Y|P> TMC ..".
|
||||
|
|
@ -176,6 +186,9 @@ DumpData parseDump(const std::string& block) {
|
|||
ax.center_speed = asInt(get(m, "center_speed", "-1"));
|
||||
ax.sw_mode = asInt(get(m, "sw_mode", "-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_peak_dev = asInt(get(m, "hold_peak", "0"));
|
||||
ax.hold_cumulative_dev = asInt(get(m, "hold_cumul", "0"));
|
||||
|
|
@ -222,6 +235,11 @@ std::vector<std::string> formatDump(const DumpData& d) {
|
|||
<< " limits=[" << ax.lim_neg << ".." << ax.lim_pos << "]"
|
||||
<< " hold_target=" << ax.hold_target
|
||||
<< " 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());
|
||||
}
|
||||
for (const char* k : {"GCONF", "CHOPCONF", "XACTUAL", "X_ENC", "XTARGET", "VACTUAL"}) {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,28 @@ CheckResult checkI2cFunctionality(unsigned long funcs) {
|
|||
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,
|
||||
const DrivetrainEcho& got) {
|
||||
CheckResult r;
|
||||
|
|
@ -184,6 +206,43 @@ CheckResult checkDeclaredValues(const std::string& axis, const DeclaredValue* va
|
|||
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 r;
|
||||
if (half_travel <= 0) {
|
||||
|
|
|
|||
|
|
@ -828,6 +828,7 @@ 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 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 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.
|
||||
// Mirrors Application's fallback: the live calibration when it is trustworthy,
|
||||
|
|
@ -925,6 +926,12 @@ void TestRunner::testHwMotor(TestReport& r) {
|
|||
}
|
||||
|
||||
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;
|
||||
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,
|
||||
|
|
@ -962,10 +969,35 @@ void TestRunner::testHwMotor(TestReport& r) {
|
|||
};
|
||||
verdict(s, std::string(name) + "_declared",
|
||||
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);
|
||||
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);
|
||||
r.sections.push_back(std::move(s));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -582,6 +582,25 @@ 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") {
|
||||
auto with = [](const std::string& k, const std::string& v) {
|
||||
return ConfigLoader::fromMap({{"Camera.rgb.id", "D"},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include <doctest/doctest.h>
|
||||
|
||||
#include "fgc/DumpParser.h"
|
||||
#include "fgc/mock/MockMotorController.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
|
@ -260,3 +261,61 @@ 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("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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,35 @@ TEST_CASE("an SMBus-only adapter is caught before it looks like a dead sensor")
|
|||
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") {
|
||||
DrivetrainExpectation want; // declared == false
|
||||
DrivetrainEcho got; // all -1
|
||||
|
|
@ -221,3 +250,37 @@ TEST_CASE("an un-homed axis is not checked") {
|
|||
CHECK_FALSE(r.ran);
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue