Compare commits

..

No commits in common. "041ad2a18f209d201d2ce757a2b466a8df05e1ed" and "842d95eecafa2d7a56c7946f913c461eba9baaa7" have entirely different histories.

15 changed files with 15 additions and 234 deletions

View File

@ -723,10 +723,6 @@ Three rules, each protecting against a different silent failure:
- **A partial answer does not unlock the axis.** Most keys landing is the common - **A partial answer does not unlock the axis.** Most keys landing is the common
case when a declaration is nearly complete, and it must not look like success. case when a declaration is nearly complete, and it must not look like success.
> An axis waiting on its config reports `NEEDCFG` in the dashboard and `C` in the
> `ST` line — deliberately not red, and deliberately not "no telemetry": the link
> is fine and the axis is talking, it just has not been told what it is driving.
>
> **Status: the host half only.** The host answers `NEED` and the responder is > **Status: the host half only.** The host answers `NEED` and the responder is
> verified end to end, but the firmware that *asks* is not written yet. Set > verified end to end, but the firmware that *asks* is not written yet. Set
> `[Features] mock_needcfg = 1` to make the simulated controller run the > `[Features] mock_needcfg = 1` to make the simulated controller run the

View File

@ -426,7 +426,7 @@ Phase 1 is deployable on its own. Phase 2 requires it.
## Progress ## Progress
Branch `worktree-hardware-config` (worktree at `.claude/worktrees/hardware-config`). Branch `worktree-hardware-config` (worktree at `.claude/worktrees/hardware-config`).
298 tests pass, up from a 204 baseline. 295 tests pass, up from a 204 baseline.
| § | Item | State | | § | Item | State |
|----|------|-------| |----|------|-------|
@ -435,14 +435,12 @@ Branch `worktree-hardware-config` (worktree at `.claude/worktrees/hardware-confi
| 3 | `hardware.ini`: `[Board]` / `[Hardware]` typed, layered over `config.ini`, shadowed keys warned | **done**, 5 tests | | 3 | `hardware.ini`: `[Board]` / `[Hardware]` typed, layered over `config.ini`, shadowed keys warned | **done**, 5 tests |
| 4 | Resolution chain `--hardware` / `$FGC_HARDWARE` / `/etc/...` / beside `config.ini` | **done**, 2 tests | | 4 | Resolution chain `--hardware` / `$FGC_HARDWARE` / `/etc/...` / beside `config.ini` | **done**, 2 tests |
| 8 | `config/hardware.example.ini` documenting the shape | **done**, and the `hardware/lattepanda-iota/` setup kit now exists too (`install.sh`, `packages.txt`, three precondition files) | | 8 | `config/hardware.example.ini` documenting the shape | **done**, and the `hardware/lattepanda-iota/` setup kit now exists too (`install.sh`, `packages.txt`, three precondition files) |
| 8 | `install.sh` writes the drivetrain block | **done.** One `[Motor.<axis>]` per declared axis, with `--r-sense` / `--run-current` / `--homing-current` / `--travel-deg` / `--steps-per-rev` / `--sw-mode` for what an installer knows and commented `; TODO` lines for what nothing can probe. Needed because the firmware now refuses to move undeclared |
| 10 | Presence reconciliation (declared vs found) | **done**, 13 tests. `HardwarePresence.h` compares `[Hardware]` against what came up; warn-only by default, refusing under `[Features] strict_presence` with `--degraded` as the field-triage escape (decision 4) | | 10 | Presence reconciliation (declared vs found) | **done**, 13 tests. `HardwarePresence.h` compares `[Hardware]` against what came up; warn-only by default, refusing under `[Features] strict_presence` with `--degraded` as the field-triage escape (decision 4) |
| 11 | `test hardware` | **done**, 11 tests. Five leaves (`axes` / `motor` / `imu` / `env` / `camera`) under a new `hardware` subsystem; included in a bare `test`, and deliberately not gated on a homed gimbal. Nothing declared reports SKIP, never PASS | | 11 | `test hardware` | **done**, 11 tests. Five leaves (`axes` / `motor` / `imu` / `env` / `camera`) under a new `hardware` subsystem; included in a bare `test`, and deliberately not gated on a homed gimbal. Nothing declared reports SKIP, never PASS |
| 5 | Delete the `Application.cpp` speed literals | **done**, 13 tests. Decision 2 answered: speed is declared as `[Motor.<axis>] scan_speed_deg_s` (default **8.0 deg/s**) and converted per tower through `counts_per_deg` (`AxisSpeed.h`). The literals sent 36.4 deg/s against an intended 21.8 | | 5 | Delete the `Application.cpp` speed literals | **done**, 13 tests. Decision 2 answered: speed is declared as `[Motor.<axis>] scan_speed_deg_s` (default **8.0 deg/s**) and converted per tower through `counts_per_deg` (`AxisSpeed.h`). The literals sent 36.4 deg/s against an intended 21.8 |
| 6 | Config handshake — **host half** | **done**, 13 tests. `FirmwareConfig.h` parses `NEED` and plans the `SET` lines; unanswered keys and unknown keys are reported separately. `[Features] mock_needcfg` makes the simulated controller run the handshake, so the host half is verified with no firmware. **The firmware half is not written** | | 6 | Config handshake — **host half** | **done**, 13 tests. `FirmwareConfig.h` parses `NEED` and plans the `SET` lines; unanswered keys and unknown keys are reported separately. `[Features] mock_needcfg` makes the simulated controller run the handshake, so the host half is verified with no firmware. **The firmware half is not written** |
| 6 | Config handshake — **firmware half** | **done and built** on both targets 2026-09-18 (iota_rp2040 and leonardo, `./deploy.sh --compile-only`); behaviour still unproven on a running rig. `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`. **Behaviour on a running rig is the one check still outstanding** | | 6 | Config handshake — **firmware half** | **written, NOT compile-verified.** `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. Parser verified locally (30 native tests, g++); the rest type-checks against stubs on both board branches but has never been built by PlatformIO or run on hardware |
| 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` |
| 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 |
Presence reconciliation (§10) changes nothing on a tower without a `hardware.ini`: absent is Presence reconciliation (§10) changes nothing on a tower without a `hardware.ini`: absent is
silent and `hardware.declared` stays false. Verified by running the binary both ways. silent and `hardware.declared` stays false. Verified by running the binary both ways.

View File

@ -299,25 +299,17 @@ commit `aab1925`.
| 2 | **Phase 2 gate: RP2040 + TMC5160-BOB at 3.3 V** | **PASSED** on tower 121, 2026-09-15: `IOIN Y=0x3000001E version=0x30`, encoder reading (`X_ENC`), one axis, `speed=3000` | | 2 | **Phase 2 gate: RP2040 + TMC5160-BOB at 3.3 V** | **PASSED** on tower 121, 2026-09-15: `IOIN Y=0x3000001E version=0x30`, encoder reading (`X_ENC`), one axis, `speed=3000` |
| 2 | `src/platform.h` — reset cause and free RAM behind one interface per MCU family | **done** | | 2 | `src/platform.h` — reset cause and free RAM behind one interface per MCU family | **done** |
| 2 | `Serial.begin()` moved ahead of all hardware init, so a wedged image stays flashable | **done** | | 2 | `Serial.begin()` moved ahead of all hardware init, so a wedged image stays flashable | **done** |
| 2 | `[env:iota_rp2040]` PlatformIO target (arduino-pico core, 8 MB flash) | **done and built.** Latest, 2026-09-18, with the config handshake: **iota_rp2040** 51716 B flash (0.6 % of 8 MB), 9108 B RAM (3.5 %); **leonardo** 22646 B flash (**79 % of 28 KB**), 939 B RAM (36.7 %). Both from `./deploy.sh --compile-only` against towers 121 and 111; 33 native tests pass (`pio test -e native`) | | 2 | `[env:iota_rp2040]` PlatformIO target (arduino-pico core, 8 MB flash) | **done and built** on tower 121, 2026-09-15: 49452 B flash (0.6 % of 8 MB), 9068 B RAM. `leonardo` still builds (19254 B) and the 22 native tests still pass |
| 2 | Per-board pin map + `NUM_AXES` in `platform_leonardo.h` / `platform_iota.h`, as-built wiring | **done** — compile-time, not yet generated from `hardware.ini` | | 2 | Per-board pin map + `NUM_AXES` in `platform_leonardo.h` / `platform_iota.h`, as-built wiring | **done** — compile-time, not yet generated from `hardware.ini` |
| 2 | Pins + `NUM_AXES` generated from `hardware.ini` | **not started** — needs the config plan's phase 2 first | | 2 | Pins + `NUM_AXES` generated from `hardware.ini` | **not started** — needs the config plan's phase 2 first |
| 3 | Yaw homing on the real drivetrain | **done** — the endstop was being sheared through at full torque; homing at 189 mA holds. See below | | 3 | Yaw homing on the real drivetrain | **done** — the endstop was being sheared through at full torque; homing at 189 mA holds. See below |
| 1 | Everything else | blocked on [hardware-config-plan.md](hardware-config-plan.md) | | 1 | Everything else | blocked on [hardware-config-plan.md](hardware-config-plan.md) |
> **Where the firmware gets built.** PlatformIO is not installed on the dev box; the firmware is built > **Not compile-verified.** PlatformIO is not installed on the dev box and the firmware is built
> *remotely on a tower* by `firmware/deploy.sh --compile-only`. Locally, the Arduino-free sources > *remotely on the tower* by `firmware/deploy.sh`. What was verified locally: both branches of
> (`command_parse.cpp` and its native tests) compile and run under plain g++, and the rest > `platform.h` syntax-check against stub headers — the AVR branch under `gnu++11` (what the Arduino AVR
> syntax-checks against stub headers on both branches — the AVR under `gnu++11` and the RP2040 under > core uses) and the RP2040 branch under `gnu++17` (what arduino-pico uses) — and an unrecognised
> `gnu++17`, with an unrecognised architecture failing on the `#error` rather than compiling silently. > architecture fails on the `#error` rather than compiling silently.
> That catches typos and signatures; it says nothing about size or behaviour, so a remote build is
> still the gate.
>
> **Flash headroom is now the constraint on the Delta fleet.** The ATmega32U4 build sits at **79 % of
> its 28 KB** (6026 B left), up from 19254 B at the phase 2 gate — that growth is everything since:
> `BOOTSEL`, `CURRENT`, the reduced homing current, `STOP` ending a homing, and the config handshake.
> RP2040 flash is a non-issue at 0.6 % of 8 MB. Deleting the drivetrain fields from `MOTOR_CONFIG`
> (config plan §14) now buys space as well as removing a duplicate definition.
> >
> To actually build it: `cd firmware && ./deploy.sh --compile-only` compiles on the device without > To actually build it: `cd firmware && ./deploy.sh --compile-only` compiles on the device without
> flashing. That rsyncs over the tower's firmware source tree, so it is a deliberate step, not one to > flashing. That rsyncs over the tower's firmware source tree, so it is a deliberate step, not one to

View File

@ -86,7 +86,7 @@ Per-file reference for the refactored tree, plus the shared data structures.
### `MotorTelemetry` / `AxisTelemetry` ([IMotorController.h](../include/fgc/IMotorController.h)) ### `MotorTelemetry` / `AxisTelemetry` ([IMotorController.h](../include/fgc/IMotorController.h))
Per-axis `yaw` and `pitch` segments (`pitch_present` flag), each an `AxisTelemetry`: `state` Per-axis `yaw` and `pitch` segments (`pitch_present` flag), each an `AxisTelemetry`: `state`
(`AxisState` B/R/H/A/E/C — the last being NEEDCFG, a firmware waiting to be told its drivetrain), `xactual`, `xenc` (encoder counts), `drv_status`, `sg`/`cs`/`pwm`, and flags (`AxisState` B/R/H/A/E), `xactual`, `xenc` (encoder counts), `drv_status`, `sg`/`cs`/`pwm`, and flags
`standstill`/`stall`/`deviation`/`overtemp`/`endstop_l`/`endstop_r`, with `moving()`/`ready()` helpers. `standstill`/`stall`/`deviation`/`overtemp`/`endstop_l`/`endstop_r`, with `moving()`/`ready()` helpers.
Parsed from the firmware `ST Y:...[ P:...]` line; degrees are derived via `Geometry`. Parsed from the firmware `ST Y:...[ P:...]` line; degrees are derived via `Geometry`.

View File

@ -230,26 +230,8 @@ Then prove the program finds it **with nothing in the environment** - no
### The drivetrain block ### The drivetrain block
**`install.sh` now writes this block**, one `[Motor.<axis>]` per declared axis — `install.sh` does not write this one - no probe can read a sense resistor off a
it has to, because the firmware refuses to move until it has been told these board - so add it by hand, one `[Motor.<axis>]` block per axis:
(see the config handshake in [configuration.md](configuration.md)). What it still
cannot do is *probe* them: no reading of a board tells you its sense resistor.
So pass what you know and it writes the values; omit anything and it writes a
commented `; TODO` line naming what to measure:
```bash
./install.sh --stdout --axes yaw \
--r-sense 0.075 --run-current 756 --homing-current 200 \
--travel-deg 340 --steps-per-rev 1210778 --sw-mode 0
```
A `TODO` line is **commented, not a placeholder value**, and deliberately: these
keys are numeric, and a non-numeric value makes the program refuse to start with
a message about a config key. Left absent, it starts and names exactly which keys
that axis is missing and that it will not move — the useful failure.
The block it writes looks like this:
```ini ```ini
[Motor.yaw] [Motor.yaw]

View File

@ -20,22 +20,6 @@
# --env none (default: none) ambient sensor model, or none # --env none (default: none) ambient sensor model, or none
# --cameras rgb (default: rgb) expected [Camera.<name>] blocks # --cameras rgb (default: rgb) expected [Camera.<name>] blocks
# #
# The drivetrain, per axis. The firmware REFUSES TO MOVE until it has been told
# these (docs/configuration.md, the config handshake), so an axis left undeclared
# is an axis that will sit in NEEDCFG. Pass what you know; what you omit is
# written as a commented line naming what has to be measured:
#
# --r-sense 0.075 sense resistor on that axis's TMC board (ohms)
# --run-current 756 coil current for normal motion (mA RMS)
# --homing-current 200 coil current while hunting an endstop (mA RMS)
# --travel-deg 340 mechanical angle between the hard stops
# --steps-per-rev 1210778 measured full travel, in counts
# --sw-mode 0 TMC SW_MODE: 0 = no switches (StallGuard homing)
#
# These take ONE value, applied to the first axis in --axes. A second axis gets a
# commented template: nothing here can be probed, and inventing a sense resistor
# is how a driver gets cooked.
#
# Other flags: # Other flags:
# --force overwrite an existing hardware.ini (backs it up first) # --force overwrite an existing hardware.ini (backs it up first)
# --no-apt-update skip 'apt-get update' # --no-apt-update skip 'apt-get update'
@ -50,10 +34,6 @@ AXES="yaw,pitch"
IMU="none" IMU="none"
ENV_SENSOR="none" ENV_SENSOR="none"
CAMERAS="rgb" CAMERAS="rgb"
# Empty means "not known at install time" -> written commented out, never as a
# sentinel: these are numeric keys, and a non-numeric value makes the program
# refuse to start rather than report the axis as undeclared.
R_SENSE="" RUN_CURRENT="" HOMING_CURRENT="" TRAVEL_DEG="" STEPS_PER_REV="" SW_MODE=""
DO_DEPS=0 DO_PRE=0 DO_HW=0 DO_VIMBA=0 PROBE_ONLY=0 TO_STDOUT=0 FORCE=0 APT_UPDATE=1 DO_DEPS=0 DO_PRE=0 DO_HW=0 DO_VIMBA=0 PROBE_ONLY=0 TO_STDOUT=0 FORCE=0 APT_UPDATE=1
VIMBA_SRC="" VIMBA_SRC=""
EXPLICIT=0 EXPLICIT=0
@ -69,12 +49,6 @@ while [[ $# -gt 0 ]]; do
--imu) IMU="$2"; shift ;; --imu) IMU="$2"; shift ;;
--env) ENV_SENSOR="$2"; shift ;; --env) ENV_SENSOR="$2"; shift ;;
--cameras) CAMERAS="$2"; shift ;; --cameras) CAMERAS="$2"; shift ;;
--r-sense) R_SENSE="$2"; shift ;;
--run-current) RUN_CURRENT="$2"; shift ;;
--homing-current) HOMING_CURRENT="$2"; shift ;;
--travel-deg) TRAVEL_DEG="$2"; shift ;;
--steps-per-rev) STEPS_PER_REV="$2"; shift ;;
--sw-mode) SW_MODE="$2"; shift ;;
--vimba) VIMBA_SRC="$2"; DO_VIMBA=1; EXPLICIT=1; shift ;; --vimba) VIMBA_SRC="$2"; DO_VIMBA=1; EXPLICIT=1; shift ;;
--force) FORCE=1 ;; --force) FORCE=1 ;;
--no-apt-update) APT_UPDATE=0 ;; --no-apt-update) APT_UPDATE=0 ;;
@ -293,26 +267,6 @@ do_vimba() {
} }
# ------------------------------------------------------------ hardware.ini --- # ------------------------------------------------------------ hardware.ini ---
# Emit "key = value", or a commented-out line naming what has to be measured.
#
# Commented rather than a placeholder string on purpose: these keys are numeric,
# and a non-numeric value makes the program refuse to start with a message about
# a config key. Absent, it starts and reports the axis as undeclared - naming the
# missing keys and which axis will not move, which is the useful failure.
kv_or_todo() {
local key="$1" value="$2" why="$3"
if [[ -n $value ]]; then
printf '%-18s = %s\n' "$key" "$value"
return
fi
# Wrapped, and not merely for looks: ini.h sets INI_MAX_LINE to 200, and a
# longer line is SPLIT rather than truncated, so its tail becomes a bogus
# line and the whole file fails to parse. A generated file that cannot be
# read is worse than one with a gap in it.
printf '; TODO %s\n' "$key"
printf '%s\n' "$why" | fold -s -w 72 | sed 's/^/; /' | sed 's/[[:space:]]*$//'
}
emit_hardware_ini() { emit_hardware_ini() {
local board mcu cam fw_env local board mcu cam fw_env
board="$(probe_board)" board="$(probe_board)"
@ -375,60 +329,6 @@ i2c_addr = 0x44
EOF EOF
fi fi
# One [Motor.<axis>] block per axis. The firmware holds none of these itself
# any more - it asks, and refuses to move until answered - so an axis without
# a block here is an axis that will not turn.
#
# A value we were not given is written COMMENTED OUT, with what to measure.
# Not as a placeholder string: these keys are numeric, and a non-numeric value
# makes the program refuse to start, which is a worse failure than the axis
# reporting itself undeclared.
local ax first=1
for ax in ${AXES//,/ }; do
ax="$(echo "$ax" | tr -d '[:space:]')"
[[ -z $ax ]] && continue
echo
echo "[Motor.$ax]"
if [[ $first -eq 1 ]]; then
kv_or_todo "r_sense_ohm" "$R_SENSE" \
"the resistor fitted to THIS driver board - read it off the board, do not assume"
kv_or_todo "run_current_ma" "$RUN_CURRENT" \
"coil current for normal motion, mA RMS, from the motor's nameplate"
kv_or_todo "homing_current_ma" "$HOMING_CURRENT" \
"current while hunting an endstop. LOWER than run_current wherever the stop can be out-pushed: on this rig a screw-head stop was sheared straight past its mate at full torque"
kv_or_todo "travel_deg" "$TRAVEL_DEG" \
"mechanical angle between the hard stops. Rough is fine to start; every bearing inherits its error proportionally until 'gimbal heading setup' supersedes it"
kv_or_todo "steps_per_rev" "$STEPS_PER_REV" \
"measured full travel IN COUNTS - read it off a homing run. The only guard against a PHANTOM endstop"
kv_or_todo "sw_mode" "$SW_MODE" \
"TMC SW_MODE. 0 = no physical switches (StallGuard homing); set the bits if this axis has REFL/REFR wired"
else
echo "; Nothing here can be probed, and this is not the first axis, so the"
echo "; values below are a template. Measure them on THIS axis."
kv_or_todo "r_sense_ohm" "" "sense resistor on this axis's driver board"
kv_or_todo "run_current_ma" "" "coil current for normal motion, mA RMS"
kv_or_todo "homing_current_ma" "" "current while hunting an endstop"
kv_or_todo "travel_deg" "" "mechanical angle between the hard stops"
kv_or_todo "steps_per_rev" "" "measured full travel, in counts"
kv_or_todo "sw_mode" "" "TMC SW_MODE (0 = no physical switches)"
fi
cat <<'EOF'
; Speeds are DEGREES PER SECOND, not VMAX: a register value means different
; things on different gear ratios, so it is not portable between towers. The
; host converts through counts_per_deg and sends SPEED.
scan_speed_deg_s = 8.0
homing_speed_deg_s = 2.0
center_speed_deg_s = 6.0
; |XACTUAL - XENC| that means "position untrustworthy". 1500 is this mechanism's
; starting point; re-measure it whenever the homing current changes, because the
; free-running gap is coupled to it.
enc_deviation = 1500
; Is this axis's encoder aligned to XACTUAL and usable for hold and DIAG?
has_encoder = true
EOF
first=0
done
local c local c
for c in ${CAMERAS//,/ }; do for c in ${CAMERAS//,/ }; do
c="$(echo "$c" | tr -d '[:space:]')" c="$(echo "$c" | tr -d '[:space:]')"
@ -470,12 +370,7 @@ do_hardware_ini() {
board="$(probe_board)" board="$(probe_board)"
if [[ $board != lattepanda-iota ]]; then if [[ $board != lattepanda-iota ]]; then
warn "this kit is for the LattePanda IOTA; this machine reports '$board'" warn "this kit is for the LattePanda IOTA; this machine reports '$board'"
# --probe and --stdout change nothing on this machine: one reports what it [[ $PROBE_ONLY -eq 1 ]] || die "refusing to apply IOTA preconditions to a $board"
# found, the other prints the file it WOULD write. Refusing them on a
# non-IOTA only means the generator can never be exercised off a tower,
# which is how a setup kit ends up untested.
[[ $PROBE_ONLY -eq 1 || $TO_STDOUT -eq 1 ]] ||
die "refusing to apply IOTA preconditions to a $board"
fi fi
if [[ $PROBE_ONLY -eq 1 ]]; then probe_report; exit 0; fi if [[ $PROBE_ONLY -eq 1 ]]; then probe_report; exit 0; fi

View File

@ -6,13 +6,7 @@
namespace fgc { namespace fgc {
// Per-axis lifecycle state, from the firmware ST line's state char (B/R/H/A/E). // Per-axis lifecycle state, from the firmware ST line's state char (B/R/H/A/E).
// Per-axis lifecycle, from the firmware ST line's state char. enum class AxisState { Boot, Reset, Homing, Ready, Error, Unknown };
// B Boot R Reset H Homing A reAdy E Error C NeedConfig
//
// NeedConfig means the firmware has been told nothing about the drivetrain it is
// driving and is refusing to move until the host answers its NEED lines. It is
// NOT a fault and NOT "no telemetry": the link is fine and the axis is talking.
enum class AxisState { Boot, Reset, Homing, Ready, Error, NeedConfig, Unknown };
// One axis segment of a firmware `ST` status line: // One axis segment of a firmware `ST` status line:
// ST Y:<state>,<xactual>,<xenc>,<drv_hex>,<sg>,<cs>,<pwm>,<flags> [P:...] // ST Y:<state>,<xactual>,<xenc>,<drv_hex>,<sg>,<cs>,<pwm>,<flags> [P:...]

View File

@ -109,9 +109,7 @@ public:
// and main.cpp) so the host-side DumpParser is exercised without // and main.cpp) so the host-side DumpParser is exercised without
// hardware. Register values are illustrative, not simulated; the // hardware. Register values are illustrative, not simulated; the
// encoder positions track the mock's live state. // encoder positions track the mock's live state.
// 5 = NEEDCFG, appended to the firmware enum so the older codes keep const int st = homed_ ? 3 : 1; // READY : RESET
// their meanings. See motor.h.
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\n";
auto axis = [&](char L, const AxisTelemetry& a) { auto axis = [&](char L, const AxisTelemetry& a) {
@ -242,10 +240,7 @@ private:
a.standstill = false; a.standstill = false;
} }
a.xactual = a.xenc; a.xactual = a.xenc;
// An axis waiting on its config says so, rather than looking like a a.state = homed_ ? AxisState::Ready : AxisState::Reset;
// healthy un-homed one - that distinction is the whole point of the state.
a.state = (needcfg_ && !configured_) ? AxisState::NeedConfig
: (homed_ ? AxisState::Ready : AxisState::Reset);
a.deviation = deviation_; a.deviation = deviation_;
} }

View File

@ -1243,22 +1243,6 @@ struct Application::Impl {
continue; continue;
} }
const bool is_yaw = need->axis == 'Y'; const bool is_yaw = need->axis == 'Y';
// An axis the tower says it does not have. That is not a missing
// declaration - it is a firmware built for more axes than this rig,
// and answering would configure a driver that is not there. Same
// class of fault as presence reconciliation catches, so it is named
// that way rather than as a hole in hardware.ini.
const char* axis_name = is_yaw ? "yaw" : "pitch";
if (cfg.hardware.declared && !cfg.hardware.hasAxis(axis_name)) {
LOG_ERROR << "firmware asked for axis " << need->axis << " (" << axis_name
<< "), which this tower does not declare (axes "
<< cfg.hardware.axesLabel()
<< ") -- the firmware was built for more axes than the rig has; "
"rebuild it for this board rather than declaring an axis that is "
"not fitted";
continue;
}
const auto src = is_yaw const auto src = is_yaw
? axisConfigSource("yaw", cfg.geometry.yaw, cfg.drivetrain.yaw, ? axisConfigSource("yaw", cfg.geometry.yaw, cfg.drivetrain.yaw,
yaw_current_, cfg.yaw_soft_margin_counts) yaw_current_, cfg.yaw_soft_margin_counts)

View File

@ -16,10 +16,6 @@ const char* stateName(int s) {
case 2: return "HOMING"; case 2: return "HOMING";
case 3: return "READY"; case 3: return "READY";
case 4: return "ERROR"; case 4: return "ERROR";
// Appended in the firmware enum rather than placed in lifecycle order,
// because these numbers are on the wire: inserting it would renumber
// every later state and a READY axis would read back as ERROR.
case 5: return "NEEDCFG";
default: return "?"; default: return "?";
} }
} }

View File

@ -29,7 +29,6 @@ AxisState stateFromChar(char c) {
case 'H': case 'h': return AxisState::Homing; case 'H': case 'h': return AxisState::Homing;
case 'A': case 'a': return AxisState::Ready; case 'A': case 'a': return AxisState::Ready;
case 'E': case 'e': return AxisState::Error; case 'E': case 'e': return AxisState::Error;
case 'C': case 'c': return AxisState::NeedConfig;
default: return AxisState::Unknown; default: return AxisState::Unknown;
} }
} }

View File

@ -867,9 +867,6 @@ void TestRunner::testHwAxes(TestReport& r) {
// its own segment. // its own segment.
MotorTelemetry t = motor_.telemetry(); MotorTelemetry t = motor_.telemetry();
FoundHardware f; FoundHardware f;
// NEEDCFG counts as telemetry arriving: the link is fine and the axis is
// talking, it just has not been told what it is driving. Treating it as "no
// telemetry" would blame the serial port for a configuration gap.
f.axes_known = t.yaw.state != AxisState::Unknown; f.axes_known = t.yaw.state != AxisState::Unknown;
if (f.axes_known) { if (f.axes_known) {
f.axes.push_back("yaw"); f.axes.push_back("yaw");

View File

@ -13,7 +13,6 @@ const char* axisStateLabel(AxisState s) {
case AxisState::Homing: return "HOMING"; case AxisState::Homing: return "HOMING";
case AxisState::Ready: return "READY"; case AxisState::Ready: return "READY";
case AxisState::Error: return "ERROR"; case AxisState::Error: return "ERROR";
case AxisState::NeedConfig: return "NEEDCFG";
case AxisState::Unknown: case AxisState::Unknown:
default: return "----"; default: return "----";
} }
@ -24,9 +23,6 @@ UiColor axisStateColor(AxisState s) {
case AxisState::Ready: case AxisState::Ready:
case AxisState::Homing: return UiColor::Cyan; case AxisState::Homing: return UiColor::Cyan;
case AxisState::Error: return UiColor::Red; case AxisState::Error: return UiColor::Red;
// Not red: nothing is broken, the firmware is waiting to be told what it
// is driving. Yellow so it reads as "needs you", not as a fault.
case AxisState::NeedConfig: return UiColor::Yellow;
case AxisState::Boot: case AxisState::Boot:
case AxisState::Reset: return UiColor::Dim; case AxisState::Reset: return UiColor::Dim;
case AxisState::Unknown: case AxisState::Unknown:

View File

@ -201,25 +201,3 @@ TEST_CASE("a firmware predating the handshake leaves those fields absent, not ze
CHECK(d.axes[0].sw_mode == -1); CHECK(d.axes[0].sw_mode == -1);
CHECK(d.axes[0].cfg_received == -1); CHECK(d.axes[0].cfg_received == -1);
} }
TEST_CASE("the numeric state codes did not shift when NEEDCFG was added") {
// NEEDCFG is appended (5), not inserted in lifecycle order, because these
// numbers are on the wire. Had it gone in at 1, a READY axis (which would
// have become 4) would read back as ERROR - a healthy tower reporting a
// fault in every dump, and an old firmware misread by a new host.
auto one = [](int n) {
const std::string text =
"DUMP BEGIN build=x uptime=1 mcusr=0x01 free_ram=1\n"
"DUMP Y state=" + std::to_string(n) +
" hsub=0 enabled=1 lim_neg=0 lim_pos=1 hold_target=0 speed=1 center=0 "
"soft_margin=0 hold_corr=0 hold_peak=0 hold_cumul=0 has_encoder=1\n"
"DUMP END\n";
return fgc::parseDump(text).axes.at(0).state_name;
};
CHECK(one(0) == "BOOT");
CHECK(one(1) == "RESET");
CHECK(one(2) == "HOMING");
CHECK(one(3) == "READY");
CHECK(one(4) == "ERROR");
CHECK(one(5) == "NEEDCFG");
}

View File

@ -82,24 +82,3 @@ TEST_CASE("parseAxisSegment reads the encoder-deviation flag 'd'") {
CHECK(e->stall == true); CHECK(e->stall == true);
CHECK(e->deviation == true); CHECK(e->deviation == true);
} }
TEST_CASE("the NEEDCFG state char is understood, not read as unknown") {
// The firmware refuses to move until the host answers its NEED lines, and
// says so with 'C'. A host that did not know the code would map it to
// Unknown, which downstream reads as "no telemetry arrived" - blaming the
// serial link for a configuration gap.
auto t = fgc::parseTelemetryLine("ST Y:C,0,0,00000000,0,0,0,S");
REQUIRE(t);
CHECK(t->yaw.state == fgc::AxisState::NeedConfig);
CHECK_FALSE(t->yaw.state == fgc::AxisState::Unknown);
auto lower = fgc::parseTelemetryLine("ST Y:c,0,0,00000000,0,0,0,S");
REQUIRE(lower);
CHECK(lower->yaw.state == fgc::AxisState::NeedConfig);
}
TEST_CASE("an axis in NEEDCFG is not ready") {
auto t = fgc::parseTelemetryLine("ST Y:C,0,0,00000000,0,0,0,S");
REQUIRE(t);
CHECK_FALSE(t->yaw.ready());
}