Add the handshake bring-up runbook (plus the mock/test changes)

This commit is contained in:
pgdalmeida 2026-09-18 17:36:19 +02:00
parent 07de495840
commit 816242725a
Signed by: pedro.almeida
GPG Key ID: D4A6C394DF13F1D7
3 changed files with 140 additions and 0 deletions

View File

@ -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 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.<axis>]` 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=<rev>
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 ## 11. Verification checklist
| Check | How | Tower 121 | | Check | How | Tower 121 |

View File

@ -87,6 +87,8 @@ public:
deviation_ = true; deviation_ = true;
} else if (verb == "RESET") { } else if (verb == "RESET") {
deviation_ = false; 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") { } else if (verb == "MOVE") {
std::string a; std::string a;
in >> a; in >> a;

View File

@ -247,3 +247,19 @@ TEST_CASE("a partial answer leaves the axis in NEEDCFG") {
m.sendCommand("HOME"); m.sendCommand("HOME");
CHECK_FALSE(m.telemetry().yaw.ready()); 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);
}