A month ago I started working on four little StackChan desk robots. Then I wiped the firmware on the spare, forgot most of what I had learned, and came back to a device speaking Chinese and asking to be activated.
It was not bricked. It was waiting for the wrong brain.
The factory path expected a XiaoZhi-backed cloud identity. I did not want to create another assistant, another profile, or another memory plane. Hermes already has those jobs. StackChan should be a face and a body attached to the same brain, not a separate cloud character sitting beside it.
We chose the fourth unit as the sacrificial spare. Units #1 through #3 stayed sealed on factory firmware. Before writing anything, we identified the USB device, saved an official factory rollback image, pinned a matched gateway/firmware pair, and documented how to recover the board.
The path is deliberately small. Hermes talks to one pinned MCP gateway. The gateway authenticates one spare robot over the LAN. Hermes sees only six read-only tools. The cloud activation path is absent.
stackchan-mcp 0.17.0, pinned and launched through a per-unit wrapper.A robot combines several different risk classes in one cheerful package: firmware writes, microphones, cameras, servos, LEDs, persistent credentials, and a network service. A generic “go” is too broad. We split the work into checkpoints and required a fresh approval whenever the next checkpoint changed the risk.
| Gate | Result |
|---|---|
| R0 | Researched hardware, recovery paths, firmware, and MCP options. |
| R1 | Prepared pinned software and collision-free local ports without connecting a robot. |
| R2 | Identified Unit #4 over USB and proved a factory rollback artifact existed before any write. |
| R3 | Flashed the tagged firmware, provisioned the LAN, and stored the local gateway configuration. |
| R3b | Repaired a startup-order problem with a narrow local-only build and an app-only flash. |
| R4 | Proved authenticated local WebSocket access and six read-only Hermes MCP calls. |
| R5a | Later qualified the custom avatar, brightness, and one static base-LED value, then restored idle state. |
The first R4 test looked promising. Hermes could start the MCP server, and the gateway could listen for the robot. But the robot never established its WebSocket connection.
The easy story was “Wi-Fi is broken.” It was wrong. Serial logs showed the spare joining the LAN correctly. Source inspection showed what happened next: tagged firmware 1.16.0 called CheckNewVersion() before InitializeProtocol(). The device entered a Tenclass activation loop and never reached the code that starts its local protocol. The saved gateway URL and token were fine. Firmware control flow made them irrelevant.
R3b added an explicit STACKCHAN_LOCAL_ONLY build option for the StackChan board. In that mode, the firmware applies its local assets, skips the cloud OTA/activation gate, then starts the local protocol. Other boards keep the tagged behavior unless they opt in.
We built the image with the upstream board-aware release script in ESP-IDF 5.5.2. The repair was flashed only to the application partition at 0x20000. That preserved NVS, so the spare kept its Wi-Fi credentials, local gateway URL, and device token.
After reboot, the serial log showed the local-only marker, the saved local gateway, a successful authenticated WebSocket handshake, and no Tenclass request. The gateway announced 40 firmware capabilities, but Hermes was allowed to see only the six reads we had approved.
| Allowlisted tool | What it proves |
|---|---|
| get_status | The device session is alive and returns structured status. |
| get_device_info | The gateway is talking to the intended hardware and firmware. |
| get_head_angles | Current pose can be read without commanding movement. |
| get_touch_state | Touch state can be observed without changing configuration. |
| get_touch_sensor_enabled | The persistent touch-enable state can be inspected. |
| gateway_config_get | The local gateway configuration can be checked without returning the token value. |
One caveat surfaced during the R4 proof. Gateway 0.17.0 automatically rendered its built-in idle avatar after device discovery. We did not call a display tool, but the screen changed because that behavior is hard-coded in the gateway. Removing it would require a separate gateway patch and review.
R5 is not one switch. R5a display and base LEDs have now passed; touch configuration, controlled head movement, one intentional device-camera frame, and audio remain separate approvals with their own evidence, cleanup, and rollback state.
Only after those tests would we consider voice integration, a seven-day soak on the spare, and a proposal for Unit #1. A successful spare test will never become a fleet cutover by accident.