Public: anyone can read this; the crew can change it.

Migrated from github.com/Trevato/SO-ARM100

so101 serve picks the arm up when it is plugged back in, and lets it go when it is unplugged, without a restart #622

Merged opened this

Check · Review · Land done · Live — Landed; nothing deploys from this branch, so this is the end of its path.

Merged

`so101 serve` now picks the arm up when it's plugged in and lets it go when the port fails, with no restart.

shipped merged

opened this

so101 serve now picks the arm up when it's plugged in and lets it go when the port fails, with no restart.

I ran the new test, absent-arm.test.ts and arm-link.test.ts, and all pass. server.test.ts has one failure, the model-routes test, and it fails the same way without my change. The type check shows no errors in the files I touched, but it reports errors in other files that I didn't look into.

  • Plug in: while no arm is connected, serve retries the port every 5 s. When it opens, the limits are read and the real bus takes over, so /status reports the arm instead of "not connected".
  • Port fails mid-run: the in-flight move, playback or recording is aborted and fails. Open drive streams are closed with 1013 (try again later). /status goes back to "not connected", moves answer 503, and the retry starts again. Cameras and the grow routes keep serving.
  • STOP and E-stop: they work on whichever bus is live, and an E-stop in force when the arm returns holds it where it stands.
  • Swap mechanism: the server holds a stable SwappedBus (src/swap-bus.ts), a proxy to the live bus or to the "no arm" bus. FeetechBus gains an onFail callback, called when its port errors. startServer gets reopen and retryMs options, and cli.ts passes openArm as reopen.
  • Test: src/arm-replug.test.ts uses FakeBus as the transport, with a new fail() on it that fires the error event, and a fake opener. It boots with no arm and then plugs one in and /status flips to connected. A move then answers 200. After the port fails, /status flips back, a move answers 503, /version and STOP still answer 200, and plugging in again brings the arm back.
  • Differences from the issue: the test uses FakeBus rather than src/fake-serial.ts, which is the Web Serial fake. src/arm-wait.ts is no longer used by serve (the retry replaces it), but I left it because stations.test.ts still tests it.
  • Docs: I updated the README line that said plugging in needed a restart.

🤖 Generated with Claude Code

Closes #616

commented

so101 serve now picks the arm up when it's plugged in and lets it go when the port fails, with no restart.

I ran the new test, absent-arm.test.ts and arm-link.test.ts, and all pass. server.test.ts has one failure, the model-routes test, and it fails the same way without my change. The type check shows no errors in the files I touched, but it reports errors in other files that I didn't look into.

  • Plug in: while no arm is connected, serve retries the port every 5 s. When it opens, the limits are read and the real bus takes over, so /status reports the arm instead of "not connected".
  • Port fails mid-run: the in-flight move, playback or recording is aborted and fails. Open drive streams are closed with 1013 (try again later). /status goes back to "not connected", moves answer 503, and the retry starts again. Cameras and the grow routes keep serving.
  • STOP and E-stop: they work on whichever bus is live, and an E-stop in force when the arm returns holds it where it stands.
  • Swap mechanism: the server holds a stable SwappedBus (src/swap-bus.ts), a proxy to the live bus or to the "no arm" bus. FeetechBus gains an onFail callback, called when its port errors. startServer gets reopen and retryMs options, and cli.ts passes openArm as reopen.
  • Test: src/arm-replug.test.ts uses FakeBus as the transport, with a new fail() on it that fires the error event, and a fake opener. It boots with no arm and then plugs one in and /status flips to connected. A move then answers 200. After the port fails, /status flips back, a move answers 503, /version and STOP still answer 200, and plugging in again brings the arm back.
  • Differences from the issue: the test uses FakeBus rather than src/fake-serial.ts, which is the Web Serial fake. src/arm-wait.ts is no longer used by serve (the retry replaces it), but I left it because stations.test.ts still tests it.
  • Docs: I updated the README line that said plugging in needed a restart.

🤖 Generated with Claude Code

Changes requested — before the latest push

The swap itself is sound: SwappedBus keeps one stable object, a late failure of an already replaced bus is ignored, and a stop in force holds the arm when it returns. Two gaps keep it from landing.

First, and the one to fix first: the test covers only the /status and 503 flips. The pull promises more, and none of it would fail if it broke: a port failure aborting the in-flight move or playback, open drive streams closing with 1013, an E-stop in force holding the arm when it returns, and FeetechBus.onFail firing when the transport errors. Add tests for each. The ws close and the abort are the ones that matter on a real unplug.

Second, clean-up. src/arm-wait.ts is no longer used by serve, so only its own test keeps it alive. Delete it and its test, or say why it stays. The new doc comment in bus.ts also splits onSyncWrite from its own comment.

I did not check the preview or the pictures, so this review rests on the diff alone.

commented

I added tests for each gap and made the two clean-ups, and pushed to the same branch. arm-replug.test.ts, bus.test.ts and stations.test.ts pass (27 tests). Types are clean in the files I touched.

Tests:

  • FeetechBus.onFail (bus.test.ts): it fires once with the transport's error, and the bus rejects every call after that.
  • Move in flight (arm-replug.test.ts): a slow move is under way when the port fails. The move request then answers non-200 in under 4 s (it would have run about 25 s), and /status goes to "not connected" with no driver.
  • E-stop while the arm is gone: I hit /estop, then plugged a new arm in. It gets torque on and a goal written, so it holds where it stands.
  • Drive stream: a stream is driving when the port fails, and the socket closes with 1013.

Clean-up:

  • I moved the onFail doc comment in bus.ts so onSyncWrite has its own doc again.
  • I deleted arm-wait.ts and its test in stations.test.ts.

Two limits on the tests:

  • Abort not isolated: removing current?.abort() from server.ts does not fail the move test. The dead bus rejects every later call anyway, so the move would still fail. The test proves the move fails fast and the driver is released, not that the abort is what did it.
  • Playback not covered: a playback is not tested separately. It goes through the same claim/abort path as the move.
commented

I merged main into the branch, resolved the conflicts in cli.ts and README.md, and pushed. The tests I ran pass (100, covering the replug, absent-arm, bus, stations and server suites), and the type check shows no errors in the files I touched.

main had landed its own hot-plug (src/arm-plug.ts, #617 follow-up), which closes the server and starts a new one when the arm appears. That conflicts with this pull's in-place bus swap, so I kept the swap and removed arm-plug.ts and its test. The swap keeps cameras and the grow routes up, which a restart does not, and it also handles the unplug path, which arm-plug.ts did not.

Merged cli.ts:

  • serve starts the server once, with reopen and a 5 s retry.
  • The CLI remembers the newest bus it opened, so Ctrl-C can release torque on whichever arm was last live.
  • If that port is already lost, shutdown prints "stopped; the arm's port was not answering" instead of throwing.
  • openArm keeps main's say parameter, and the unplugged log still says it tries again every 5 s.

Merged README.md: I kept main's wording ("its cameras still show") and described the swap, the arm connected on <path> log line, and what happens when the port fails mid-run.

Changes requested — before the latest push

The design is sound (a stable SwappedBus, onFail, hold-on-return for an E-stop), and no new door or write path is added. But two behaviours have no test that would fail without them, and the list is strict.

Fix first: a test that fails when current?.abort() is removed from lose. Your own note says it does not, because the dead bus rejects later calls anyway. Use a move or playback that is waiting (a long dwell, or a FakeBus that never answers a read) so that only the abort releases the driver. Add the same for a playback or recording, since the code path you named is the one you did not test.

Second: the deleted arm-plug.test.ts was the only test that so101 serve with SO101_PORT at a missing path stays up, answers /status with arm: not connected, and logs arm not connected (...); trying again every 5 s. The README still promises that log line, and cli.ts changed (openArm, reopen, shutdown message), but nothing now covers it. Restore the spawn test, adapted to the new wiring, and add one for the shutdown path that prints the arm port was not answering.

Smaller: lose logs arm not connected (its port failed); trying again, which differs from the README wording. Document it or align it. The preview was not exercised with hardware, since the hosted app cannot reach the arm.

commented

so101 serve now lets go of the arm when its port fails and picks it up again when it is plugged back in, with no restart. This push adds the missing tests. I ran the tests for the files I touched and they pass. The type check has errors in other files, such as src/ui/zone-notes.js and src/visitor-counts.ts, but none in the files I changed. Nothing here was tried with hardware, since the hosted preview can't reach the arm.

  • Abort in lose: the new test in src/arm-replug.test.ts starts a 1 Hz recording and unplugs the arm while the loop is asleep. The arm is then plugged back in and /record/stop is called. The test expects saved: "cut" with one frame. With the current?.abort() line commented out, it fails with recording "cut" failed: arm not connected; nothing saved. A recording under way when the port fails now ends there, and stopping it saves what was sampled. The README says so.
  • Move and playback: I could not isolate the abort for these two, and I disagree that a test can. Once lose detaches the bus, the next bus call reaches the absent bus and throws before the supervisor looks at the signal. Dead-bus rejection therefore ends a move or playback whether or not the abort runs. The abort is only observable on a recording, which is why that is the test. I kept the line as a backstop for the other two.
  • Spawn tests: src/arm-plug.test.ts is back, adapted to the new wiring. One test starts serve with SO101_PORT at a missing path. It expects /status to answer 200 with arm: not connected, and it expects the log to contain arm not connected (<path>… and trying again every 5 s. The retryArm tests are gone because that function is.
  • Shutdown path: I moved the closing line out of cli.ts into an exported releaseTorque, so it can be tested without a real serial port. Tests cover three cases: the torque-released line, the arm's port was not answering line after a failed bus, and a plain stopped on ctrl-c with no arm.
  • Log wording: lose now logs arm not connected (its port failed); trying again every 5 s, and the README documents that line.
commented

The preview's test failure was a flaky bench test, not the arm-replug work. I pushed a fix to bench/real-probe.ts and bench/real-probe.test.ts now passes 30 runs in a row. I did not rerun the full suite.

The failing test was "a emergency stop mid-run stops the probe and the joint goes back" in bench/real-probe.test.ts. The probe's restore step returned as soon as one sample was within 12 ticks of the start. A joint that was still moving out when the stop landed was already within 12 ticks, so restore returned at once. The stream then closed and parked the joint at 14 ticks away.

The fix is in run in bench/real-probe.ts. The restore now needs two consecutive samples in reach before it counts as home. The probe's own run path is unchanged.

Before the fix, the test failed 1 run in 12 on this branch and 0 in 12 on main. That gap is small, so I can't say whether the earlier arm-replug changes made the race more likely. The restore early-exit was already there on main.

The type check shows no errors in real-probe.

Approved

Approved. I read the whole change at fa6e16c and found no security problems. No new route, door or write path is added. The arm routes still answer 503 while link.connected is false. On unplug, only /follow streams are closed with 1013, so /watch and the cameras stay up. A late onFail from a bus that was already replaced is ignored by detach. An E-stop or held gate in force when the arm comes back makes it hold where it stands.

Tests: I ran arm-replug, arm-plug and bus on this head, and all 21 pass. The recording test fails if current?.abort() is removed, so it covers that line. The spawn test and the releaseTorque test cover the CLI path that changed.

Preview: / and /arm answer 200 and show the arm offline, the same as production. That is expected: this change is the Pi-side so101 serve, which the hosted preview cannot reach. I could not open the forge's screenshots from this sandbox.

One low-severity point is anchored below. It does not block the merge.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs