From 4049a85b9bdba14168fe521e306f5a7416f30e46 Mon Sep 17 00:00:00 2001 From: pgdalmeida Date: Wed, 23 Sep 2026 18:26:14 +0200 Subject: [PATCH] Check the firmware was built with the pins hardware.ini lists --- config/hardware.example.ini | 20 +++++++++ docs/hardware-config-plan.md | 3 +- docs/tower-installation.md | 15 +++++-- hardware/lattepanda-3-delta/pins.ini | 17 ++++++++ hardware/lattepanda-iota/pins.ini | 27 ++++++++++++ include/fgc/Config.h | 12 ++++++ include/fgc/DumpParser.h | 7 +++ include/fgc/HardwareChecks.h | 13 ++++++ include/fgc/mock/MockMotorController.h | 21 ++++++++- src/core/Application.cpp | 6 ++- src/core/Config.cpp | 10 +++++ src/core/DumpParser.cpp | 13 ++++++ src/core/HardwareChecks.cpp | 37 ++++++++++++++++ src/core/TestRunner.cpp | 25 +++++++++++ tests/test_config.cpp | 19 +++++++++ tests/test_dumpparser.cpp | 59 ++++++++++++++++++++++++++ tests/test_hardwarechecks.cpp | 34 +++++++++++++++ 17 files changed, 330 insertions(+), 8 deletions(-) create mode 100644 hardware/lattepanda-3-delta/pins.ini create mode 100644 hardware/lattepanda-iota/pins.ini diff --git a/config/hardware.example.ini b/config/hardware.example.ini index 42d72f1..2fac607 100644 --- a/config/hardware.example.ini +++ b/config/hardware.example.ini @@ -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//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, diff --git a/docs/hardware-config-plan.md b/docs/hardware-config-plan.md index 7a0e78e..6a4afa6 100644 --- a/docs/hardware-config-plan.md +++ b/docs/hardware-config-plan.md @@ -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. @@ -466,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 `, `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//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: diff --git a/docs/tower-installation.md b/docs/tower-installation.md index 56d3d7e..d922f6c 100644 --- a/docs/tower-installation.md +++ b/docs/tower-installation.md @@ -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 diff --git a/hardware/lattepanda-3-delta/pins.ini b/hardware/lattepanda-3-delta/pins.ini new file mode 100644 index 0000000..5d3cb47 --- /dev/null +++ b/hardware/lattepanda-3-delta/pins.ini @@ -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 diff --git a/hardware/lattepanda-iota/pins.ini b/hardware/lattepanda-iota/pins.ini new file mode 100644 index 0000000..ed8fa14 --- /dev/null +++ b/hardware/lattepanda-iota/pins.ini @@ -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 diff --git a/include/fgc/Config.h b/include/fgc/Config.h index 2b1ce30..0e6d60c 100644 --- a/include/fgc/Config.h +++ b/include/fgc/Config.h @@ -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. diff --git a/include/fgc/DumpParser.h b/include/fgc/DumpParser.h index 1aa2874..95cdb0b 100644 --- a/include/fgc/DumpParser.h +++ b/include/fgc/DumpParser.h @@ -47,6 +47,10 @@ struct DumpAxis { // 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) @@ -85,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 reset_flags; std::vector axes; }; diff --git a/include/fgc/HardwareChecks.h b/include/fgc/HardwareChecks.h index a779de6..963b374 100644 --- a/include/fgc/HardwareChecks.h +++ b/include/fgc/HardwareChecks.h @@ -105,6 +105,19 @@ CheckResult checkDrivetrainAdopted(const std::string& axis, const DrivetrainExpe // 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 diff --git a/include/fgc/mock/MockMotorController.h b/include/fgc/mock/MockMotorController.h index 3f42756..ad6fa9d 100644 --- a/include/fgc/mock/MockMotorController.h +++ b/include/fgc/mock/MockMotorController.h @@ -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 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 need_lines_; std::map settings_; diff --git a/src/core/Application.cpp b/src/core/Application.cpp index 0dbc71c..d0bb0b1 100644 --- a/src/core/Application.cpp +++ b/src/core/Application.cpp @@ -306,7 +306,11 @@ struct Application::Impl { std::unique_ptr makeMotor() { bool mock = opts.mock_serial.value_or(cfg.features.mock_serial); - if (mock) return std::make_unique(cfg.features.mock_needcfg); + if (mock) { + auto m = std::make_unique(cfg.features.mock_needcfg); + m->setBuiltPins(cfg.hardware.pins); + return m; + } return std::make_unique(cfg.serial.device, cfg.serial.baud); } diff --git a/src/core/Config.cpp b/src/core/Config.cpp index ecf3d06..c93a2de 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -246,6 +246,16 @@ AppConfig ConfigLoader::fromMap(const std::map& 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 + diff --git a/src/core/DumpParser.cpp b/src/core/DumpParser.cpp index 5854bcf..34555a6 100644 --- a/src/core/DumpParser.cpp +++ b/src/core/DumpParser.cpp @@ -1,6 +1,7 @@ #include "fgc/DumpParser.h" #include +#include #include #include @@ -120,6 +121,15 @@ DumpData parseDump(const std::string& block) { d.mcusr = static_cast(asInt(get(hdr, "mcusr", "0"))); d.free_ram = asInt(get(hdr, "free_ram", "0")); d.reset_text = get(hdr, "reset"); + { + // "spi=,,". 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 state=.." and "DUMP TMC ..". @@ -177,6 +187,8 @@ DumpData parseDump(const std::string& block) { 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")); @@ -223,6 +235,7 @@ std::vector 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. diff --git a/src/core/HardwareChecks.cpp b/src/core/HardwareChecks.cpp index a99cc6a..a71f1ba 100644 --- a/src/core/HardwareChecks.cpp +++ b/src/core/HardwareChecks.cpp @@ -206,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) { diff --git a/src/core/TestRunner.cpp b/src/core/TestRunner.cpp index 2e930ec..bbd5303 100644 --- a/src/core/TestRunner.cpp +++ b/src/core/TestRunner.cpp @@ -969,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)); } diff --git a/tests/test_config.cpp b/tests/test_config.cpp index 3a5272a..7f817a9 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -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"}, diff --git a/tests/test_dumpparser.cpp b/tests/test_dumpparser.cpp index afa39c5..cc68b35 100644 --- a/tests/test_dumpparser.cpp +++ b/tests/test_dumpparser.cpp @@ -1,6 +1,7 @@ #include #include "fgc/DumpParser.h" +#include "fgc/mock/MockMotorController.h" #include #include @@ -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); +} diff --git a/tests/test_hardwarechecks.cpp b/tests/test_hardwarechecks.cpp index c8a58f1..3fbd19b 100644 --- a/tests/test_hardwarechecks.cpp +++ b/tests/test_hardwarechecks.cpp @@ -250,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); +}