Check the firmware was built with the pins hardware.ini lists

This commit is contained in:
pgdalmeida 2026-09-23 18:26:14 +02:00
parent 7a7e7ab0a5
commit 4049a85b9b
Signed by: pedro.almeida
GPG Key ID: D4A6C394DF13F1D7
17 changed files with 330 additions and 8 deletions

View File

@ -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,

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
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 <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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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<std::string> reset_flags;
std::vector<DumpAxis> axes;
};

View File

@ -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

View File

@ -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_;

View File

@ -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);
}

View File

@ -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 +

View File

@ -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 ..".
@ -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<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.

View File

@ -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) {

View File

@ -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));
}

View File

@ -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"},

View File

@ -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);
}

View File

@ -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);
}