James Harton James Harton

October 2026: Goats, Cats and Balance Bots

Back from Goatmire: a workshop rescued by hand-flashing, a talk with two robot arms and a HAL 9000 theme nobody noticed, and a balance bot that went home, learned to drive, grew a face and got a remote control. Plus kits, two new packages, and a disarm that told fibs.

This one's a little late. I've just got back from Goatmire in Varberg, Sweden, via the most punishing jet lag I have ever experienced — the sort where you wake up at 3am wide awake and fall asleep mid-sentence at 4pm. If anything below trails off, that's why.

Short version: the workshop happened, the talk happened, and then the robots went home with other people and immediately got better than the ones I'd built.

Achieving Balance (Eventually)

The workshop was a little more chaotic than I'd planned for.

The plan was for it to be seamless: plug the Nerves Starter Kit into your laptop, run a couple of mix tasks, and have it suddenly work. The reality was that two things stood between a lot of laptops and that plan. sunxi-tools — which talks to the Allwinner chip in FEL mode — didn't want to compile for a good fraction of the room. And the seamless route into FEL mode went via the board's built-in CH340 USB-serial adapter, the same one that gives you a serial console on the Linux system. Its macOS driver is a kernel extension, which on current macOS means a parade of increasingly alarming security dialogs. Not what you want to be talking people through with a room full of strangers' laptops and four hours on the clock.

The punchline: the serial driver was only ever needed to flip the board into FEL mode, and there's a button on the board that does exactly that. We always knew it was there — Gus designed the board, after all — but I was so focused on the plug-it-in-and-it-works experience that it didn't occur to either of us to reach for it as the escape hatch until after the workshop. I'm choosing to regard this as a fun fact rather than a personal failing.

Gus and I ended up flashing a pile of boards by hand so everyone could get to the fun part, which they did. We didn't get all the way through the slides, so they're in the repo for anyone who wants to finish the job at home: bb_nsk/slides, with a PDF if you'd rather not build them.

The fixes we made in the trenches are already released in bb_nsk: 0.1.3 added Gus's nsk flashing tooling to the installer and points it at our own forks of the flashing tools, and 0.1.4 adds rustler so the one NIF without a prebuilt artefact can actually be built. Next time, the button goes on the first slide anyway.

The Talk

James on stage at Goatmire 2026, pointing upwards, with two orange SO-101 robot arms on a table beside him Photo: Petter Boström, Five Keys.

The other half of the trip was the conference talk. I went on stage with two SO-101 arms and gave people the walk-through of what I've been banging on about for the last year: robots described declaratively, running on the BEAM, with supervision trees doing what they do best — keeping things running when the hardware has opinions.

It went over well — better than I'd expected, honestly. I came home with a lot of positive feedback and enthusiasm, and a recurring theme in it: people who'd heard of the project but didn't really understand what it was until they saw it on stage. Noted for future talks — fewer words, more servos.

Sadly, nobody picked up on the HAL 9000 theme running through the slides. I'm sorry Dave; I'm afraid nobody noticed that.

Then The Robots Went Home And Got Better

This is my favourite part of the month, and I didn't do any of it.

Last week, Gus Workman and Lars Wikman (of the Goatmire clan) took their balance bots home and had a proper play.

Gus rewrote the control law, and the robot now drives beautifully and shrugs off disturbances — including, as it turns out, the scrutiny of a cat.

A balance bot under feline inspection. Video by Gus Workman.

The clever bit is how it finds its balance point. A balance bot only stands still when it's leaning at exactly the angle that puts its centre of mass over the wheels, and that angle moves with the battery charge, the floor and whatever you've strapped to it. Get it slightly wrong and the robot doesn't fall over — it just crawls steadily off in one direction. Gus's velocity loop treats that crawl as the measurement: while nobody's driving and the robot is moving slowly, a slow trim integrates the crawl back out and learns the missing lean. A shove is fast enough to fall outside that window, so the robot doesn't march back to where it was pushed from. And the trim is kept across falls, because the balance point doesn't change just because the robot fell over.

Then Lars gave it a face. I'd hoped the display would be good for something like this, and Lars went and proved it:

Expressive. Video by Lars Wikman.

Then Gus added a remote control, using the AtomVM-based 2026 Goatmire badge. The badge joins the robot as a node over Erlang distribution and sends {:drive, self(), throttle, turn} messages to a registered process on the robot. There's no watchdog in the remote code at all, because there doesn't need to be: the drive controllers already drop a request after drive_timeout, so a badge that goes quiet stops the robot. That's the sort of thing I like seeing — reaching for the framework's guarantee instead of writing another one.

Driving a balance bot from a Goatmire badge running AtomVM. Video by Gus Workman.

Two very different BEAMs, one send/2. Lovely.

There's more speed on the way, too: Gus and Frank Hunleth are working on getting the ARM32 JIT into nerves_system_trellis, the Nerves system the starter kit runs. The BEAM on that board is currently interpreted, so the JIT should give us a major boost — welcome news on a board where, as last month's post showed, CPU headroom is the difference between balancing and not.

Gus's improvements will be rolled back into the official bb_nsk project. In the meantime, if you've got a balance bot and want to play, his firmware is at gworkman/goatbot.

You Can Have One Too

Balance bot kits are going up on the Protolux Electronics store: protolux.io/store/nsk-balance-bot. Pricing and details are still to come, but there'll be two versions:

  • an upgrade kit for people who already own a Nerves Starter Kit, and
  • a complete kit that includes the mainboard and battery, for starting from scratch.

Cat not included.

Two New Packages

Both of these were built for the workshop and are now on Hex:

  • bb_nsk — board support for the Nerves Starter Kit and its balance bot add-on. An Igniter installer builds the robot up a subsystem at a time (bb_nsk.add_wheels, add_imu, add_balance, add_display, add_web and friends), and it ships the drivers, sensors and balance loop, the panel, the LEDs, networking and a web interface with a drive pad. There's a doctor task you run on the board when something isn't working, and a cheat task that runs every add_* step in one go for when the workshop clock is against you. Five releases in under a fortnight, which is roughly what happens when the deadline is a room full of people.

    It's about to get a major rework. The goal is that a single mix igniter.install bb_nsk takes you all the way to a working balance bot, with Gus's new control law built in. Watch this space.

  • bb_parameter_store_cubdb — a BB.Parameter.Store backed by CubDB, so parameter changes survive a reboot. Rather than slotting a module into an existing DSL option, it ships a DSL extension of its own: opt in with use BB, extensions: [BB.Parameter.Store.CubDB.Dsl].

The Framework, Meanwhile

Conference prep doesn't leave much room for framework work, but a few things landed that are worth knowing about.

Cancel anything

bb 0.31.1 lets anyone cancel a running command, not just whoever started it (#260). Previously, if automation kicked off a command, an operator in the dashboard had no way to stop it. LiveView, Kino and MCP had all worked around this by keeping hold of the pids they'd started themselves. Commands now register under their execution id, with BB.Command.list/1, whereis/2 and cancel/2 on top, and a string encoding of the id so it survives JSON, URLs and DOM attributes.

Fixing that turned up that executing_commands/1 had been reporting the wrong id all along — the monitor reference rather than the execution id — so nothing could correlate a listed command with its PubSub topic. bb_liveview 0.3.4, bb_kino 0.4.4 and bb_mcp 0.3.3 all now cancel any running command.

One arm down shouldn't mean both arms down

bb 0.31.2 came straight from hardware (#263). With two SO-101s in one VM, one arm's servo bus lost power. Within a few seconds the other arm had gone too, along with the rest of the application.

The topology supervisor was :permanent, so once it exhausted its restart budget it was started again, against hardware that still wasn't there, spending the root's budget until that gave up too. And when the root went down, the safety controller deleted the robot from its table — taking with it the :error state that had been latched precisely so someone could see what went wrong. Not just unrecovered: forgotten.

It's :transient now. A robot whose hardware subtree gives up stays down, the robot stays registered, the fault stays readable, nothing propagates to its neighbours, and Supervisor.restart_child/2 brings it back once a human has acknowledged it. That is a behaviour change: a robot no longer quietly reattaches itself to hardware that vanished. For anything with motors on it, I'd argue that was never recovery.

A disarm that told fibs

The same power cut turned up something worse (bb_servo_feetech #126): BB.Safety.disarm/1 could return :ok while the servos were still holding torque. Safety state said :disarmed; the arm was still stiff.

The controller registered its disarm handler with a snapshot of its servo list — empty at startup — and re-registered on every servo it discovered. The safety controller keeps handlers in an ETS bag, so they piled up rather than replacing each other: seven registrations for a six-servo arm, each holding a different prefix of the list. A disarm during discovery ran all of them, including the one holding [], and writing torque-off to an empty list of servos succeeds admirably.

The handler is now registered once and asks the controller for its live servo list as it runs. An empty list or an unreachable controller is an error, and the robot stays in :error rather than being told the hardware is safe. A related fix (#118) stops an actuator when its controller dies, rather than carrying on accepting commands and dropping them into a table that no longer exists. Both are in bb_servo_feetech 0.7.2 and bb_servo_robotis 0.6.3, which had the same shape.

Smaller things

  • bb_so101 0.4.0 (breaking) drops the gripper's 45° transmission offset (#88). Since 0.3.0 calibration anchors the gripper's zero to its closed stop, so the offset was being applied twice. If your arm was calibrated on 0.3.0 or later, recalibrate the gripper and remove the offset together — the README has the --joint gripper invocation. The upgrader deliberately doesn't do it for you, because the right answer depends on when the arm was last calibrated, which it can't see.
  • bb_mcp 0.3.3 got an audit against the DSL (#57), prompted by a report that joint limits weren't coming through. They weren't — and a robot that declared a command category got no MCP session at all, the state resource crashed whenever a command was running, and error replies couldn't be encoded. Four of six resources had no tests; they do now. 0.3.4 hands back an execution id when a command outlives its tool call.
  • bb_pid_controller 0.3.3 accepts nx 1.0 alongside 0.12, as does bb_policy.
  • The shared CI workflow can now install apt packages before compiling, because bb_nsk pulls in nerves_runtime, which wants libmnl, which the runner doesn't have. And every repo swapped mix_audit for mix hex.audit, which reports advisories straight from hex.pm rather than from a git checkout that quietly goes stale.

Coming In bb 0.32

I'd hoped to get bb 0.32.0 out the door before this post, and didn't quite manage it. Everything below is merged on main and will be in it. Heads up: it includes breaking changes to how actuators are commanded, so if you drive actuators directly, read the first two items.

  • Arm epochs (#271). "Is the robot armed?" is not the same question as "has the robot been armed continuously since this command was created?" — and only the first was being asked. So a command from one arming session could still be applied in a later one, after a disarm that was supposed to make things safe. Each successful arm now allocates an epoch, the BB.Actuator send functions stamp it onto the outgoing message, and actuators refuse anything from an earlier session with BB.Error.State.StaleEpoch. If you build command messages by hand and publish them straight at an actuator, you'll need to stamp them with BB.Safety.epoch/1 or switch to the send functions.
  • One delivery API for every actuator command (#272). set_position/4 took a :delivery option; the other five commands each had a plain function, a ! that meant "cast" (the one variant that can't raise), and a _sync. They're all one function with delivery: :pubsub | :broadcast | :direct now, and the ! and _sync variants are gone. Options are validated, so a typo'd option raises instead of doing nothing, and reply_on_reject?: true means a :direct send can finally hear about a refusal.
  • Estimator intake freshness (#270) — max_input_age, per input or per estimator, and fixes for an estimator that could never recover from :lost without a latency_budget, an auxiliary input keeping a dead driver looking alive, and a latency measurement that mixed nanoseconds with native time units.
  • Validated command results (#277). A command whose result/1 returned a bare term took the runtime down, and the restart force-disarmed the robot. Now it just fails the command.
  • Two vocabulary pages (#280), reworked from the workshop material: Robotics Vocabulary for Elixir developers who've never had to describe a physical machine, and Elixir for Roboticists for people coming the other way. bb_pid_controller gained a matching explanation of PID from scratch, including how to tune one by watching what the robot does. If you've ever stared at a robot rocking back and forth and wondered which gain to blame, it's for you.