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
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.
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:
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.
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_weband 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 adoctortask you run on the board when something isn't working, and acheattask that runs everyadd_*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_nsktakes you all the way to a working balance bot, with Gus's new control law built in. Watch this space. -
bb_parameter_store_cubdb— aBB.Parameter.Storebacked 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 withuse 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_so1010.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 gripperinvocation. 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_mcp0.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 commandcategorygot 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_controller0.3.3 acceptsnx1.0 alongside 0.12, as doesbb_policy.- The shared CI workflow can now
install apt packages before compiling,
because
bb_nskpulls innerves_runtime, which wantslibmnl, which the runner doesn't have. And every repo swappedmix_auditformix 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.Actuatorsend functions stamp it onto the outgoing message, and actuators refuse anything from an earlier session withBB.Error.State.StaleEpoch. If you build command messages by hand and publish them straight at an actuator, you'll need to stamp them withBB.Safety.epoch/1or switch to the send functions. - One delivery API for every actuator command
(#272).
set_position/4took a:deliveryoption; 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 withdelivery: :pubsub | :broadcast | :directnow, and the!and_syncvariants are gone. Options are validated, so a typo'd option raises instead of doing nothing, andreply_on_reject?: truemeans a:directsend 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:lostwithout alatency_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/1returned 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_controllergained 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.
Links
- Goatmire · workshop slides · balance bot kits at Protolux
gworkman/goatbot— Gus's balance bot firmwarebb_nsk·bb_parameter_store_cubdbbb0.31.2 · 0.31.1bb_servo_feetech·bb_servo_robotis·bb_so101·bb_mcpbbon GitHub- Beam Bots on the Elixir Forum · Discord