diff --git a/docs/tower-installation.md b/docs/tower-installation.md index 6f8afae..0ada254 100644 --- a/docs/tower-installation.md +++ b/docs/tower-installation.md @@ -738,6 +738,128 @@ gimbal heading setup # ties yaw to true north by sighting landmarks; no IMU gimbal pitch setup # needs an IMU; skip on a rig without one ``` +## 10b. Bringing up the config handshake on an existing tower + +This is the one upgrade that changes what an **unconfigured** tower does: the new +firmware boots into `NEEDCFG` and refuses `HOME` / `MOVE` / `ENABLE` until the +host has answered its `NEED` lines. Flashing it onto a tower whose `hardware.ini` +predates the drivetrain keys leaves the gimbal inert — working, talking, and not +turning. + +So the order matters. **Host first, firmware second.** Each step below either +changes nothing or is reversible, and the step that could strand the tower comes +after the one that prevents it. + +### 0. Record what you are rolling back to + +```bash +gimbal dump # the running firmware's build id and register state +cat /etc/fire_gimbal_control/hardware.ini +git -C ~/projects/fwt_2a/firmware rev-parse --short HEAD +``` + +Keep the dump. It is the only record of what the old firmware was holding, and +the comparison that tells you whether the new one adopted the same values. + +### 1. Update `hardware.ini` — nothing else, and no restart yet + +Add the `[Motor.]` block (section 5). The five keys the new firmware will +not run without are `steps_per_rev`, `enc_deviation`, `sw_mode`, plus the two +speeds; `install.sh --stdout` generates the block if you would rather not type it. + +Doing this **before** flashing is what keeps the window at zero: the host is ready +to answer the moment the firmware starts asking. + +### 2. Deploy the new host against the OLD firmware + +A real test, and it risks nothing. The old firmware never sends `NEED`, so the +host must behave exactly as it did before — which is also the guarantee the whole +Delta fleet depends on. + +```bash +scripts/fgc start --start +scripts/fgc peek +test hardware motor +``` + +Expect: no handshake lines at all, and the motor leaf reporting a **version skew** +rather than a mismatch — + +``` +; yaw_current: yaw: firmware echoes only part of the current (2 of 3 fields + absent), so it cannot be confirmed -- it predates the full CURRENT echo. Re-flash it +``` + +If instead you see wrong *values*, stop: something in `hardware.ini` disagrees +with what the tower is really running, and flashing will not fix it. + +### 3. Flash + +```bash +cd firmware && ./deploy.sh # uploads; --compile-only if you want a dry run first +``` + +Watch the banner. A `NEED` line per axis should follow it: + +``` +BOOT build= +NEED Y steps_per_rev global_scaler run_irun homing_irun center_count soft_margin ... +``` + +**No `NEED` line means the axis is not in `NEEDCFG`** — either the flash did not +take, or the image is older than you think. Check the build id before going on. + +### 4. Confirm the handshake completed + +The host answers within a control-loop tick. Expect: + +``` +firmware config: answered 11 keys for axis Y +``` + +Then prove it landed rather than trusting the log: + +``` +gimbal dump # state should be 1 (RESET), NOT 5 (NEEDCFG) + # cfg_rx should be the full required mask +test hardware motor # both metrics PASS +``` + +`test hardware motor` is the real gate: it compares every declared value against +what the firmware echoes, so a `SET` that was lost on the wire shows up here and +nowhere else. + +### 5. Prove the refusal actually refuses + +Worth doing once per firmware version, because a guard that silently stopped +working looks exactly like a guard that is working. + +On a **bench** board, not a mast: power-cycle it without the host running, then +send `HOME` over a serial monitor. Expect `ERR need config`. Send `RESET` and try +again — still `ERR need config`, because a RESET is deliberately not a way out. +`USE DEFAULTS` is, and should then let `HOME` through. + +### 6. Only then, move the axis + +``` +gimbal home # at the reduced homing current +``` + +Check against the recorded figures: travel ~1,210,778 counts, repeatable to +~0.3 %, and the derived `counts_per_deg` near 3552.87. A travel that disagrees by +more than a few percent means the axis homed against something that was not the +endstop. + +### If it goes wrong + +The board is recoverable over the network as long as USB CDC comes up: +`BOOTSEL` over the serial port, then `./deploy.sh` with the previous firmware +checked out. Only an image that wedges *before* `Serial.begin()` needs a physical +button press — which is why `Serial.begin()` is the first thing `setup()` does. + +`USE DEFAULTS` is the in-place escape: it puts every axis on the compile-time +table and lets the tower move while you work out what `hardware.ini` is missing. + ## 11. Verification checklist | Check | How | Tower 121 | diff --git a/include/fgc/mock/MockMotorController.h b/include/fgc/mock/MockMotorController.h index 63264b9..3f42756 100644 --- a/include/fgc/mock/MockMotorController.h +++ b/include/fgc/mock/MockMotorController.h @@ -87,6 +87,8 @@ public: deviation_ = true; } else if (verb == "RESET") { deviation_ = false; + // A RESET is not a way out of NEEDCFG (firmware motor.cpp says why). + if (needcfg_ && !configured_) homed_ = false; } else if (verb == "MOVE") { std::string a; in >> a; diff --git a/tests/test_firmwareconfig.cpp b/tests/test_firmwareconfig.cpp index 3ae4972..af6e890 100644 --- a/tests/test_firmwareconfig.cpp +++ b/tests/test_firmwareconfig.cpp @@ -247,3 +247,19 @@ TEST_CASE("a partial answer leaves the axis in NEEDCFG") { m.sendCommand("HOME"); CHECK_FALSE(m.telemetry().yaw.ready()); } + +TEST_CASE("RESET is not a way out of NEEDCFG") { + // Found by reading the firmware before flashing it: Motor::reset() set + // ST_RESET unconditionally, so an axis that was never told its drivetrain + // would come back accepting HOME and MOVE on the compile-time table - + // reached by a command typed for unrelated reasons. + MockMotorController m(/*needcfg=*/true); + m.start(); + m.takeNeedLines(); + + m.sendCommand("RESET"); + CHECK_FALSE(m.configured()); + m.sendCommand("HOME"); + CHECK_FALSE(m.telemetry().yaw.ready()); + CHECK(m.telemetry().yaw.state == AxisState::NeedConfig); +}