Check the firmware was built with the pins hardware.ini lists
This commit is contained in:
parent
7a7e7ab0a5
commit
4049a85b9b
|
|
@ -69,6 +69,26 @@ cameras = rgb
|
||||||
device = /dev/serial/by-id/usb-Arduino_LLC_Arduino_Leonardo-if00
|
device = /dev/serial/by-id/usb-Arduino_LLC_Arduino_Leonardo-if00
|
||||||
baud = 115200
|
baud = 115200
|
||||||
|
|
||||||
|
[Pins]
|
||||||
|
; How the MCU is wired to each axis's TMC5160 driver board. The host never sends
|
||||||
|
; these: the FIRMWARE is built from them. firmware/tools/gen_wiring.py reads this
|
||||||
|
; section from the tower's own copy of this file when the firmware is built on
|
||||||
|
; the tower, and refuses pins the board cannot use. Change a wire, change this,
|
||||||
|
; rebuild and re-flash. `test hardware motor` compares it with what the running
|
||||||
|
; firmware reports, so a flash built from an older copy shows up there.
|
||||||
|
;
|
||||||
|
; The reference loom for each board, with the MCU-pin -> driver-pin table, is in
|
||||||
|
; hardware/<board>/pins.ini.
|
||||||
|
;
|
||||||
|
; Leonardo (Delta): SPI is fixed on the ICSP header, so only CS and EN are named.
|
||||||
|
; RP2040 (IOTA): also spi_sck / spi_mosi / spi_miso, from SPI0's pin groups -
|
||||||
|
; SCK {2,6,18,22} MOSI {3,7,19,23} MISO {0,4,16,20}
|
||||||
|
; Only axes listed in [Hardware] axes get pins.
|
||||||
|
yaw_cs = 8
|
||||||
|
yaw_en = 7
|
||||||
|
pitch_cs = 1
|
||||||
|
pitch_en = 0
|
||||||
|
|
||||||
[IMU]
|
[IMU]
|
||||||
; Only read when [Hardware] imu is not 'none'. On the LattePanda 3 Delta the MTi
|
; Only read when [Hardware] imu is not 'none'. On the LattePanda 3 Delta the MTi
|
||||||
; is on the RS-232 header, which is a hardware UART: a stable /dev/ttyS* node,
|
; is on the RS-232 header, which is a hardware UART: a stable /dev/ttyS* node,
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ this whole design is removing. Make it an explicit act.
|
||||||
|
|
||||||
Only **pin assignments** and the **axis count**: pins because SPI must be up before anything can be
|
Only **pin assignments** and the **axis count**: pins because SPI must be up before anything can be
|
||||||
asked or answered, the axis count because it sizes the axis array. Both are still defined once in
|
asked or answered, the axis count because it sizes the axis array. Both are still defined once in
|
||||||
`hardware.ini` 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
|
time instead of over the wire. Everything else becomes runtime, which shrinks the build-time set far
|
||||||
below what I proposed last turn.
|
below what I proposed last turn.
|
||||||
|
|
||||||
|
|
@ -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 | Config handshake — **firmware half** | **done, and validated on tower 121.** `NEEDCFG` state (`ST` code `C`), `NEED` emitted with the banner, `SET <axis> <key> <value>`, `USE DEFAULTS`, and the init block split so the residual-motion stop still runs at boot. 33 native tests pass under `pio test -e native`. On the rig: the handshake was watched live, `HOME` refused with `ERR need config`, `RESET` did not escape the state, and `USE DEFAULTS` released it |
|
||||||
| 6 | Adoption check generalised | **done**, 9 tests. `DUMP` echoes every host-owned value plus a `cfg_rx` bitmask; `test hardware motor` checks all of them, and reports an absent field as unconfirmable rather than as a mismatch against -1. Round trip NEED -> SET -> DUMP -> PASS verified against `mock_needcfg` |
|
| 6 | Adoption check generalised | **done**, 9 tests. `DUMP` echoes every host-owned value plus a `cfg_rx` bitmask; `test hardware motor` checks all of them, and reports an absent field as unconfirmable rather than as a mismatch against -1. Round trip NEED -> SET -> DUMP -> PASS verified against `mock_needcfg` |
|
||||||
| 14 | Drivetrain out of `MOTOR_CONFIG` | **done.** The struct keeps pins and `homing_kind` only; `platform_iota.h` keeps its measurement record but no longer defines this tower's values. `USE DEFAULTS` now loads a shared **limp-home** set (`LIMP_*` in `config.h`) that is deliberately nobody's real values — slow speeds, generous margins, the gentle search current — so a tower still running them fails `test hardware motor` instead of looking configured. `SW_MODE` is derived from `homing_kind` rather than shared, because disabling the hardware stops on an axis that homes on real switches would remove its last protection. Flash cost, not saved: see the porting plan. Found a real bug on the way — `DIAG` read `normal_speed` / `center_speed` from the compile-time table, so it ran at compiled speeds rather than the tower's declared ones |
|
| 14 | Drivetrain out of `MOTOR_CONFIG` | **done.** The struct keeps pins and `homing_kind` only; `platform_iota.h` keeps its measurement record but no longer defines this tower's values. `USE DEFAULTS` now loads a shared **limp-home** set (`LIMP_*` in `config.h`) that is deliberately nobody's real values — slow speeds, generous margins, the gentle search current — so a tower still running them fails `test hardware motor` instead of looking configured. `SW_MODE` is derived from `homing_kind` rather than shared, because disabling the hardware stops on an axis that homes on real switches would remove its last protection. Flash cost, not saved: see the porting plan. Found a real bug on the way — `DIAG` read `normal_speed` / `center_speed` from the compile-time table, so it ran at compiled speeds rather than the tower's declared ones |
|
||||||
|
| 14 | Pins and `NUM_AXES` generated from `hardware.ini` | **done, validated on tower 121** (2026-09-23): built from its `/etc` file, flashed, `DUMP` echoed `spi=2,3,0 cs=1 en=4`, `IOIN version=0x30`, and `test hardware motor` passed `yaw_pins` / `spi_pins`; a build against a copy without `[Pins]` failed with the reason. Leonardo flash cost not yet measured. New `[Pins]` section; `firmware/tools/gen_wiring.py` generates `board_wiring.h` into the build directory from the tower's own file at build time, and refuses pins the board cannot use (RP2040 SPI0 groups, the Leonardo's fixed ICSP pins, duplicates, a board/env mismatch). No compiled-in fallback: no file or no `[Pins]` fails the build. `DUMP` echoes `cs=`/`en=` per axis and `spi=` on the RP2040; `test hardware motor` compares them with `[Pins]`. The loom tables moved to `hardware/<board>/pins.ini`. 18 generator tests (run without PlatformIO), 9 host tests |
|
||||||
| 6 | `NEEDCFG` understood host-side | **done**, 5 tests. `AxisState::NeedConfig` parsed from `ST` `C`, named in `DUMP` and the dashboard, and counted as telemetry arriving rather than as a dead link. The firmware enum APPENDS the state (5) rather than inserting it — inserting renumbered every later state and made a READY axis read back as ERROR |
|
| 6 | `NEEDCFG` understood host-side | **done**, 5 tests. `AxisState::NeedConfig` parsed from `ST` `C`, named in `DUMP` and the dashboard, and counted as telemetry arriving rather than as a dead link. The firmware enum APPENDS the state (5) rather than inserting it — inserting renumbered every later state and made a READY axis read back as ERROR |
|
||||||
|
|
||||||
**Validated on tower 121.** The handshake was watched live on the rig, not inferred:
|
**Validated on tower 121.** The handshake was watched live on the rig, not inferred:
|
||||||
|
|
|
||||||
|
|
@ -360,10 +360,17 @@ SERIAL_PORT=/dev/serial/by-id/usb-...
|
||||||
./deploy.sh --monitor # watch it boot
|
./deploy.sh --monitor # watch it boot
|
||||||
```
|
```
|
||||||
|
|
||||||
**Confirm the pin map before flashing a board that is wired to motors.** The map
|
**Confirm the pin map before flashing a board that is wired to motors.** A
|
||||||
lives in `src/platform_iota.h` and a chip-select on the wrong pin writes into a
|
chip-select on the wrong pin writes into a driver that was never meant to hear
|
||||||
driver that was never meant to hear it. The numbers there are the loom as built
|
it. The pins are the `[Pins]` section of this tower's `hardware.ini`: the build
|
||||||
on tower 121, which is **not** the map proposed in the porting plan.
|
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
|
### 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.
|
// Where it came from, for logging and for `test hardware` to cite.
|
||||||
std::string source_path;
|
std::string source_path;
|
||||||
|
|
||||||
|
// [Pins] - how the MCU is wired to each driver board. The host never sends
|
||||||
|
// these: the firmware is BUILT from them (firmware/tools/gen_wiring.py reads
|
||||||
|
// the same file on the tower). Held here only so `test hardware motor` can
|
||||||
|
// compare them with the pins the running firmware reports it was built with.
|
||||||
|
// -1 = not declared.
|
||||||
|
struct Pins {
|
||||||
|
long yaw_cs = -1, yaw_en = -1, pitch_cs = -1, pitch_en = -1;
|
||||||
|
long spi_sck = -1, spi_mosi = -1, spi_miso = -1; // RP2040 only
|
||||||
|
bool declared() const { return yaw_cs >= 0 || yaw_en >= 0 || pitch_cs >= 0 ||
|
||||||
|
pitch_en >= 0 || spi_sck >= 0; }
|
||||||
|
} pins;
|
||||||
|
|
||||||
// True when `name` is listed in axes. An UNDECLARED rig answers true for
|
// True when `name` is listed in axes. An UNDECLARED rig answers true for
|
||||||
// everything: with nothing stated there is nothing to contradict, and callers
|
// everything: with nothing stated there is nothing to contradict, and callers
|
||||||
// must keep behaving exactly as they did before.
|
// must keep behaving exactly as they did before.
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@ struct DumpAxis {
|
||||||
// mask, so that field alone cannot tell the two apart - a bench board reads as
|
// 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.
|
// fully configured. -1 when absent, i.e. a firmware predating the flag.
|
||||||
long limp_home = -1;
|
long limp_home = -1;
|
||||||
|
// The chip-select and enable pins the running image was BUILT with. -1 when
|
||||||
|
// absent, i.e. a firmware that predates the pin echo.
|
||||||
|
long cs_pin = -1;
|
||||||
|
long en_pin = -1;
|
||||||
// Encoder-tracking health accumulated by the firmware hold corrector.
|
// Encoder-tracking health accumulated by the firmware hold corrector.
|
||||||
long hold_corrections = 0; // # of hold resyncs since firmware boot/RESET
|
long hold_corrections = 0; // # of hold resyncs since firmware boot/RESET
|
||||||
long hold_peak_dev = 0; // largest deviation seen while holding (counts)
|
long hold_peak_dev = 0; // largest deviation seen while holding (counts)
|
||||||
|
|
@ -85,6 +89,9 @@ struct DumpData {
|
||||||
unsigned mcusr = 0;
|
unsigned mcusr = 0;
|
||||||
std::string reset_text; // empty => firmware did not name it
|
std::string reset_text; // empty => firmware did not name it
|
||||||
long free_ram = 0;
|
long free_ram = 0;
|
||||||
|
// SPI pins the image was built with - RP2040 only, where they are a choice.
|
||||||
|
// -1 when absent: a Leonardo, or firmware predating the echo.
|
||||||
|
long spi_sck = -1, spi_mosi = -1, spi_miso = -1;
|
||||||
std::vector<std::string> reset_flags;
|
std::vector<std::string> reset_flags;
|
||||||
std::vector<DumpAxis> axes;
|
std::vector<DumpAxis> axes;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,19 @@ CheckResult checkDrivetrainAdopted(const std::string& axis, const DrivetrainExpe
|
||||||
// an old firmware is a version skew rather than a misconfigured tower.
|
// an old firmware is a version skew rather than a misconfigured tower.
|
||||||
CheckResult checkNotLimpHome(const std::string& axis, long limp);
|
CheckResult checkNotLimpHome(const std::string& axis, long limp);
|
||||||
|
|
||||||
|
// Is the running firmware built from the wiring hardware.ini declares?
|
||||||
|
//
|
||||||
|
// Pins are not sent over the wire: firmware/tools/gen_wiring.py compiles them in
|
||||||
|
// from the tower's hardware.ini [Pins]. So a mismatch never means a lost SET -
|
||||||
|
// it means the image was built from a different copy of that file, and the fix
|
||||||
|
// is to rebuild and re-flash. The firmware drives whatever pins it was built
|
||||||
|
// with, which on a tower wired differently means a chip-select into a driver
|
||||||
|
// that was never meant to hear it.
|
||||||
|
//
|
||||||
|
// Same shape as checkDeclaredValues: `want` false = not declared, `got` -1 =
|
||||||
|
// not echoed, i.e. a firmware predating the pin echo.
|
||||||
|
CheckResult checkPinsBuilt(const std::string& label, const DeclaredValue* values, int count);
|
||||||
|
|
||||||
// Does the homed travel fit entirely in positive counts?
|
// Does the homed travel fit entirely in positive counts?
|
||||||
//
|
//
|
||||||
// `center_count` defines where the middle of the found travel sits, so the range
|
// `center_count` defines where the middle of the found travel sits, so the range
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "fgc/Config.h"
|
||||||
#include "fgc/IMotorController.h"
|
#include "fgc/IMotorController.h"
|
||||||
#include "fgc/Logger.h"
|
#include "fgc/Logger.h"
|
||||||
|
|
||||||
|
|
@ -28,6 +29,14 @@ public:
|
||||||
// way the handshake is exercised on a dev box with no rig.
|
// way the handshake is exercised on a dev box with no rig.
|
||||||
explicit MockMotorController(bool needcfg = false) : needcfg_(needcfg) {}
|
explicit MockMotorController(bool needcfg = false) : needcfg_(needcfg) {}
|
||||||
|
|
||||||
|
// The pins a real image would have been built with, from hardware.ini
|
||||||
|
// [Pins]. Echoed in DUMP in NEEDCFG mode only, which is the mock's stand-in
|
||||||
|
// for current firmware; the plain mode is a firmware predating the echo.
|
||||||
|
void setBuiltPins(const HardwareConfig::Pins& p) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
pins_ = p;
|
||||||
|
}
|
||||||
|
|
||||||
void start() override {
|
void start() override {
|
||||||
LOG_INFO << "[mock] motor controller started";
|
LOG_INFO << "[mock] motor controller started";
|
||||||
if (!needcfg_) return;
|
if (!needcfg_) return;
|
||||||
|
|
@ -115,7 +124,10 @@ public:
|
||||||
// their meanings. See motor.h.
|
// their meanings. See motor.h.
|
||||||
const int st = (needcfg_ && !configured_) ? 5 : (homed_ ? 3 : 1);
|
const int st = (needcfg_ && !configured_) ? 5 : (homed_ ? 3 : 1);
|
||||||
std::ostringstream d;
|
std::ostringstream d;
|
||||||
d << "DUMP BEGIN build=mock uptime=0 mcusr=0x01 free_ram=4096\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) {
|
auto axis = [&](char L, const AxisTelemetry& a) {
|
||||||
// In NEEDCFG mode, echo back what the host actually SET, so the
|
// In NEEDCFG mode, echo back what the host actually SET, so the
|
||||||
// whole round trip - NEED, SET, DUMP, `test hardware motor` -
|
// whole round trip - NEED, SET, DUMP, `test hardware motor` -
|
||||||
|
|
@ -145,7 +157,11 @@ public:
|
||||||
<< " hsub=0 enabled=" << (homed_ ? 1 : 0)
|
<< " hsub=0 enabled=" << (homed_ ? 1 : 0)
|
||||||
<< " lim_neg=0 lim_pos=1000000 hold_target=" << a.xenc
|
<< " lim_neg=0 lim_pos=1000000 hold_target=" << a.xenc
|
||||||
<< owned
|
<< owned
|
||||||
<< " hold_corr=0 hold_peak=0 hold_cumul=0 has_encoder=1\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"
|
d << "DUMP " << L << " TMC GCONF=0x00000004 GSTAT=0x00000000"
|
||||||
<< " IOIN=0x30000008 TSTEP=0x000fffff RAMPMODE=0x00000000"
|
<< " IOIN=0x30000008 TSTEP=0x000fffff RAMPMODE=0x00000000"
|
||||||
<< " XACTUAL=0x" << std::hex << (a.xactual & 0xffffffff)
|
<< " XACTUAL=0x" << std::hex << (a.xactual & 0xffffffff)
|
||||||
|
|
@ -227,6 +243,7 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool needcfg_ = false;
|
bool needcfg_ = false;
|
||||||
|
HardwareConfig::Pins pins_;
|
||||||
bool configured_ = false;
|
bool configured_ = false;
|
||||||
std::vector<std::string> need_lines_;
|
std::vector<std::string> need_lines_;
|
||||||
std::map<std::string, std::string> settings_;
|
std::map<std::string, std::string> settings_;
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,11 @@ struct Application::Impl {
|
||||||
|
|
||||||
std::unique_ptr<IMotorController> makeMotor() {
|
std::unique_ptr<IMotorController> makeMotor() {
|
||||||
bool mock = opts.mock_serial.value_or(cfg.features.mock_serial);
|
bool mock = opts.mock_serial.value_or(cfg.features.mock_serial);
|
||||||
if (mock) 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);
|
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.cameras = splitList(get(kv, "Hardware.cameras"));
|
||||||
cfg.hardware.imu = lowered(get(kv, "Hardware.imu"));
|
cfg.hardware.imu = lowered(get(kv, "Hardware.imu"));
|
||||||
cfg.hardware.env = lowered(get(kv, "Hardware.env"));
|
cfg.hardware.env = lowered(get(kv, "Hardware.env"));
|
||||||
|
{
|
||||||
|
auto& p = cfg.hardware.pins;
|
||||||
|
p.yaw_cs = getInt(kv, "Pins.yaw_cs", -1);
|
||||||
|
p.yaw_en = getInt(kv, "Pins.yaw_en", -1);
|
||||||
|
p.pitch_cs = getInt(kv, "Pins.pitch_cs", -1);
|
||||||
|
p.pitch_en = getInt(kv, "Pins.pitch_en", -1);
|
||||||
|
p.spi_sck = getInt(kv, "Pins.spi_sck", -1);
|
||||||
|
p.spi_mosi = getInt(kv, "Pins.spi_mosi", -1);
|
||||||
|
p.spi_miso = getInt(kv, "Pins.spi_miso", -1);
|
||||||
|
}
|
||||||
for (const auto& a : cfg.hardware.axes)
|
for (const auto& a : cfg.hardware.axes)
|
||||||
if (a != "yaw" && a != "pitch")
|
if (a != "yaw" && a != "pitch")
|
||||||
throw std::runtime_error("Hardware.axes: unknown axis '" + a +
|
throw std::runtime_error("Hardware.axes: unknown axis '" + a +
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "fgc/DumpParser.h"
|
#include "fgc/DumpParser.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstdio>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
|
@ -120,6 +121,15 @@ DumpData parseDump(const std::string& block) {
|
||||||
d.mcusr = static_cast<unsigned>(asInt(get(hdr, "mcusr", "0")));
|
d.mcusr = static_cast<unsigned>(asInt(get(hdr, "mcusr", "0")));
|
||||||
d.free_ram = asInt(get(hdr, "free_ram", "0"));
|
d.free_ram = asInt(get(hdr, "free_ram", "0"));
|
||||||
d.reset_text = get(hdr, "reset");
|
d.reset_text = get(hdr, "reset");
|
||||||
|
{
|
||||||
|
// "spi=<sck>,<mosi>,<miso>". All three or none: a partial triple is a
|
||||||
|
// garbled line, and reporting two real pins beside a -1 would mislead.
|
||||||
|
const std::string spi = get(hdr, "spi");
|
||||||
|
long v[3];
|
||||||
|
if (std::sscanf(spi.c_str(), "%ld,%ld,%ld", &v[0], &v[1], &v[2]) == 3) {
|
||||||
|
d.spi_sck = v[0]; d.spi_mosi = v[1]; d.spi_miso = v[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
d.reset_flags = bitsSet(d.mcusr, kMcusr);
|
d.reset_flags = bitsSet(d.mcusr, kMcusr);
|
||||||
|
|
||||||
// Per-axis lines: "DUMP <Y|P> state=.." and "DUMP <Y|P> TMC ..".
|
// Per-axis lines: "DUMP <Y|P> state=.." and "DUMP <Y|P> TMC ..".
|
||||||
|
|
@ -177,6 +187,8 @@ DumpData parseDump(const std::string& block) {
|
||||||
ax.sw_mode = asInt(get(m, "sw_mode", "-1"));
|
ax.sw_mode = asInt(get(m, "sw_mode", "-1"));
|
||||||
ax.cfg_received = asInt(get(m, "cfg_rx", "-1"), -1);
|
ax.cfg_received = asInt(get(m, "cfg_rx", "-1"), -1);
|
||||||
ax.limp_home = asInt(get(m, "limp", "-1"), -1);
|
ax.limp_home = asInt(get(m, "limp", "-1"), -1);
|
||||||
|
ax.cs_pin = asInt(get(m, "cs", "-1"), -1);
|
||||||
|
ax.en_pin = asInt(get(m, "en", "-1"), -1);
|
||||||
ax.hold_corrections = asInt(get(m, "hold_corr", "0"));
|
ax.hold_corrections = asInt(get(m, "hold_corr", "0"));
|
||||||
ax.hold_peak_dev = asInt(get(m, "hold_peak", "0"));
|
ax.hold_peak_dev = asInt(get(m, "hold_peak", "0"));
|
||||||
ax.hold_cumulative_dev = asInt(get(m, "hold_cumul", "0"));
|
ax.hold_cumulative_dev = asInt(get(m, "hold_cumul", "0"));
|
||||||
|
|
@ -223,6 +235,7 @@ std::vector<std::string> formatDump(const DumpData& d) {
|
||||||
<< " limits=[" << ax.lim_neg << ".." << ax.lim_pos << "]"
|
<< " limits=[" << ax.lim_neg << ".." << ax.lim_pos << "]"
|
||||||
<< " hold_target=" << ax.hold_target
|
<< " hold_target=" << ax.hold_target
|
||||||
<< " has_encoder=" << (ax.has_encoder ? 1 : 0);
|
<< " has_encoder=" << (ax.has_encoder ? 1 : 0);
|
||||||
|
if (ax.cs_pin >= 0) os << " cs=" << ax.cs_pin << " en=" << ax.en_pin;
|
||||||
// Shown only when set. It is a fault on a tower - the axis is on the
|
// 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
|
// firmware's fallback values, not hardware.ini's - and printing
|
||||||
// "limp=0" on every healthy dump would bury it.
|
// "limp=0" on every healthy dump would bury it.
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,43 @@ CheckResult checkDeclaredValues(const std::string& axis, const DeclaredValue* va
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckResult checkPinsBuilt(const std::string& label, const DeclaredValue* values, int count) {
|
||||||
|
CheckResult r;
|
||||||
|
int checked = 0, absent = 0;
|
||||||
|
std::string diff;
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
const DeclaredValue& v = values[i];
|
||||||
|
if (!v.want) continue;
|
||||||
|
if (v.got < 0) { ++absent; continue; }
|
||||||
|
++checked;
|
||||||
|
if (v.got != v.value) {
|
||||||
|
if (!diff.empty()) diff += ", ";
|
||||||
|
diff += std::string(v.name) + " declared " + std::to_string(v.value) +
|
||||||
|
", built with " + std::to_string(v.got);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (checked == 0 && absent == 0) {
|
||||||
|
r.ran = false;
|
||||||
|
r.detail = label + ": no [Pins] declared";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
if (!diff.empty()) {
|
||||||
|
r.pass = false;
|
||||||
|
r.detail = label + ": " + diff +
|
||||||
|
" -- the firmware was built from different wiring than hardware.ini "
|
||||||
|
"declares. Rebuild and re-flash it";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
if (absent > 0) {
|
||||||
|
r.pass = false;
|
||||||
|
r.detail = label + ": the firmware does not report the pins it was built with, so its "
|
||||||
|
"wiring cannot be confirmed -- it predates the pin echo. Re-flash it";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
r.detail = label + ": built with the declared pins";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
CheckResult checkTravelFitsPositive(const std::string& axis, long center, long half_travel) {
|
CheckResult checkTravelFitsPositive(const std::string& axis, long center, long half_travel) {
|
||||||
CheckResult r;
|
CheckResult r;
|
||||||
if (half_travel <= 0) {
|
if (half_travel <= 0) {
|
||||||
|
|
|
||||||
|
|
@ -969,10 +969,35 @@ void TestRunner::testHwMotor(TestReport& r) {
|
||||||
};
|
};
|
||||||
verdict(s, std::string(name) + "_declared",
|
verdict(s, std::string(name) + "_declared",
|
||||||
checkDeclaredValues(name, vals, (int)(sizeof(vals) / sizeof(vals[0]))));
|
checkDeclaredValues(name, vals, (int)(sizeof(vals) / sizeof(vals[0]))));
|
||||||
|
|
||||||
|
// The wiring the image was built from, which no SET can change.
|
||||||
|
if (hw_.declared) {
|
||||||
|
const auto& p = hw_.declared->pins;
|
||||||
|
const bool yaw = code == 'Y';
|
||||||
|
long cs = -1, en = -1;
|
||||||
|
for (const auto& ax : d.axes)
|
||||||
|
if (ax.axis == code) { cs = ax.cs_pin; en = ax.en_pin; }
|
||||||
|
const DeclaredValue pins[] = {
|
||||||
|
{"cs", (yaw ? p.yaw_cs : p.pitch_cs) >= 0, yaw ? p.yaw_cs : p.pitch_cs, cs},
|
||||||
|
{"en", (yaw ? p.yaw_en : p.pitch_en) >= 0, yaw ? p.yaw_en : p.pitch_en, en},
|
||||||
|
};
|
||||||
|
verdict(s, std::string(name) + "_pins", checkPinsBuilt(name, pins, 2));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
check_axis("yaw", 'Y', hw_.drive->yaw);
|
check_axis("yaw", 'Y', hw_.drive->yaw);
|
||||||
if (!hw_.declared || hw_.declared->hasAxis("pitch")) check_axis("pitch", 'P', hw_.drive->pitch);
|
if (!hw_.declared || hw_.declared->hasAxis("pitch")) check_axis("pitch", 'P', hw_.drive->pitch);
|
||||||
|
|
||||||
|
// SPI is a choice only on the RP2040, so only there is it declared and echoed.
|
||||||
|
if (hw_.declared && hw_.declared->pins.spi_sck >= 0) {
|
||||||
|
const auto& p = hw_.declared->pins;
|
||||||
|
const DeclaredValue spi[] = {
|
||||||
|
{"spi_sck", true, p.spi_sck, d.spi_sck},
|
||||||
|
{"spi_mosi", p.spi_mosi >= 0, p.spi_mosi, d.spi_mosi},
|
||||||
|
{"spi_miso", p.spi_miso >= 0, p.spi_miso, d.spi_miso},
|
||||||
|
};
|
||||||
|
verdict(s, "spi_pins", checkPinsBuilt("spi", spi, 3));
|
||||||
|
}
|
||||||
|
|
||||||
rollup(s);
|
rollup(s);
|
||||||
r.sections.push_back(std::move(s));
|
r.sections.push_back(std::move(s));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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") {
|
TEST_CASE("A mount angle that cannot be a rotation is rejected") {
|
||||||
auto with = [](const std::string& k, const std::string& v) {
|
auto with = [](const std::string& k, const std::string& v) {
|
||||||
return ConfigLoader::fromMap({{"Camera.rgb.id", "D"},
|
return ConfigLoader::fromMap({{"Camera.rgb.id", "D"},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
#include "fgc/DumpParser.h"
|
#include "fgc/DumpParser.h"
|
||||||
|
#include "fgc/mock/MockMotorController.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -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("PORF") != std::string::npos); // still decoded
|
||||||
CHECK(out.find("assumed AVR") != std::string::npos); // but not stated as fact
|
CHECK(out.find("assumed AVR") != std::string::npos); // but not stated as fact
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("the pins the firmware was built with are read back from DUMP") {
|
||||||
|
const std::string text =
|
||||||
|
"DUMP BEGIN build=abc uptime=1 mcusr=0x3 reset=soft free_ram=4096 spi=2,3,0\n"
|
||||||
|
"DUMP Y state=2 hsub=0 enabled=1 lim_neg=0 lim_pos=1000000 hold_target=0 speed=1 "
|
||||||
|
"center=500000 soft_margin=5000 hold_corr=0 hold_peak=0 hold_cumul=0 has_encoder=1 "
|
||||||
|
"cs=1 en=4\n"
|
||||||
|
"DUMP END\n";
|
||||||
|
auto d = fgc::parseDump(text);
|
||||||
|
REQUIRE(d.valid);
|
||||||
|
CHECK(d.spi_sck == 2);
|
||||||
|
CHECK(d.spi_mosi == 3);
|
||||||
|
CHECK(d.spi_miso == 0); // GP0 is a real pin, not "absent"
|
||||||
|
REQUIRE(d.axes.size() == 1);
|
||||||
|
CHECK(d.axes[0].cs_pin == 1);
|
||||||
|
CHECK(d.axes[0].en_pin == 4);
|
||||||
|
CHECK(d.axes[0].has_encoder); // no longer the last field, still parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a firmware predating the pin echo reports -1, and a Leonardo has no spi") {
|
||||||
|
auto d = fgc::parseDump(kDump);
|
||||||
|
REQUIRE(d.valid);
|
||||||
|
CHECK(d.spi_sck == -1);
|
||||||
|
CHECK(d.spi_miso == -1);
|
||||||
|
for (const auto& a : d.axes) {
|
||||||
|
CHECK(a.cs_pin == -1);
|
||||||
|
CHECK(a.en_pin == -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a garbled spi triple is absent, not half-read") {
|
||||||
|
auto d = fgc::parseDump("DUMP BEGIN build=x uptime=1 mcusr=0x0 free_ram=1 spi=2,3\nDUMP END\n");
|
||||||
|
REQUIRE(d.valid);
|
||||||
|
CHECK(d.spi_sck == -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("the mock echoes declared pins only when standing in for current firmware") {
|
||||||
|
fgc::HardwareConfig::Pins p;
|
||||||
|
p.yaw_cs = 1; p.yaw_en = 4; p.spi_sck = 2; p.spi_mosi = 3; p.spi_miso = 0;
|
||||||
|
|
||||||
|
fgc::MockMotorController current(/*needcfg=*/true);
|
||||||
|
current.setBuiltPins(p);
|
||||||
|
current.sendCommand("DUMP");
|
||||||
|
auto d = fgc::parseDump(current.lastDump());
|
||||||
|
REQUIRE(d.valid);
|
||||||
|
CHECK(d.spi_sck == 2);
|
||||||
|
CHECK(d.axes.at(0).cs_pin == 1);
|
||||||
|
CHECK(d.axes.at(0).en_pin == 4);
|
||||||
|
CHECK(d.axes.at(1).cs_pin == -1); // no pitch pins declared
|
||||||
|
|
||||||
|
fgc::MockMotorController old; // predates the echo, as real old firmware does
|
||||||
|
old.setBuiltPins(p);
|
||||||
|
old.sendCommand("DUMP");
|
||||||
|
auto o = fgc::parseDump(old.lastDump());
|
||||||
|
REQUIRE(o.valid);
|
||||||
|
CHECK(o.spi_sck == -1);
|
||||||
|
CHECK(o.axes.at(0).cs_pin == -1);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -250,3 +250,37 @@ TEST_CASE("an un-homed axis is not checked") {
|
||||||
CHECK_FALSE(r.ran);
|
CHECK_FALSE(r.ran);
|
||||||
CHECK(r.pass);
|
CHECK(r.pass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- the wiring the image was built from ------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("pins built as declared pass") {
|
||||||
|
const DeclaredValue v[] = {{"cs", true, 1, 1}, {"en", true, 4, 4}};
|
||||||
|
auto r = checkPinsBuilt("yaw", v, 2);
|
||||||
|
CHECK(r.ran);
|
||||||
|
CHECK(r.pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a pin mismatch blames the build, not a lost SET") {
|
||||||
|
// Pins never travel over the wire, so "a SET was lost" would send someone
|
||||||
|
// looking in the wrong place. The only fix is a rebuild from this file.
|
||||||
|
const DeclaredValue v[] = {{"cs", true, 1, 5}, {"en", true, 4, 4}};
|
||||||
|
auto r = checkPinsBuilt("yaw", v, 2);
|
||||||
|
CHECK_FALSE(r.pass);
|
||||||
|
CHECK(r.detail.find("cs declared 1, built with 5") != std::string::npos);
|
||||||
|
CHECK(r.detail.find("Rebuild and re-flash") != std::string::npos);
|
||||||
|
CHECK(r.detail.find("SET") == std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a firmware that does not echo its pins cannot pass") {
|
||||||
|
const DeclaredValue v[] = {{"cs", true, 1, -1}, {"en", true, 4, -1}};
|
||||||
|
auto r = checkPinsBuilt("yaw", v, 2);
|
||||||
|
CHECK(r.ran);
|
||||||
|
CHECK_FALSE(r.pass);
|
||||||
|
CHECK(r.detail.find("predates the pin echo") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("no [Pins] declared is not checked") {
|
||||||
|
const DeclaredValue v[] = {{"cs", false, 0, 1}};
|
||||||
|
auto r = checkPinsBuilt("yaw", v, 1);
|
||||||
|
CHECK_FALSE(r.ran);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue