Isometric wireframe of a workbench with one lit board, its build profile, its waveform, and a fleet where exactly one node is known. Generated with Google Nano Banana.
← Home

The Workbench I Wanted

There is a file at the root of every Bancada project called sketch.yaml. It pins the core platform and every library version the build uses, and it has a section the Arduino IDE will not write for you: dir: entries, pointing at libraries that live on your own disk.

That file is the entire reason the project exists.

I have used the Arduino IDE for years and I like Arduino a great deal. Those are two separate sentences. The IDE is the fastest possible start and the worst possible reproducibility — fine for the first hour, thin by the second week. Nearly everything I wanted from it was already sitting inside arduino-cli, unexposed.

This is not a product announcement. It is what two days produced, in the order the decisions arrived, including the ones I got wrong first.


The File the IDE Cannot Write

sketch.yaml is Arduino’s own format, not mine. A profile in it names an FQBN, pins the core platform to a version, and lists the libraries the sketch needs. Build against a profile and you get the same binary in six months that you got today. Build without one and you get whatever the registry felt like resolving this morning.

The IDE has no interface for any of it. It will install a library globally, into a sketchbook that profile builds deliberately exclude, and tell you nothing about which version ended up in your firmware.

So the founding rule went into the crate documentation before most of the code existed: we do not reimplement toolchain logic. Bancada shells out to arduino-cli and esptool, parses their output into typed structs, and manages sketch.yaml. It is a face on the same engines the official IDE drives.

That constraint is not modesty. It is what keeps the exit door working — everything the UI does can be reproduced by hand, and the sketch.yaml Bancada writes is plain Arduino tooling with no lock-in.

An app that owns your project format owns your project. This one does not.


Bancada Means Workbench

Bancada is Portuguese for workbench, which is what I wanted and is not what I would have called an IDE. An IDE is somewhere you live. A workbench is somewhere you put a thing down, work on it, and pick it up again.

The architecture is three rings. core/ is a plain Rust crate with no Tauri code and no UI code at all, so it unit tests headlessly and could be driven from a CLI tomorrow. src-tauri/ holds the command surface, the event streaming and the process ownership. React and CodeMirror sit on top.

That split was not an aesthetic decision. It is the reason 91% of the engine is covered by tests that need neither a window nor a board.

There was a predecessor. Eight days earlier I had a plan to grow an existing egui application into an ESP32-focused IDE — milestones, a donor project, the whole apparatus. I never wrote it. The risk list in my own plan had already called the outcome: no squiggles, no minimap, a full re-layout every frame. The ideas survived. The code did not.

I should say plainly that the pace here was agent-assisted. Twelve commits and forty thousand lines of diff across two evenings is not a number I would claim unaided. The decisions below are mine; the typing was shared.


The Oscilloscope Came First

The build order surprises people, so: the second commit is a software oscilloscope.

Not because a workbench needs one before it needs a New Project button, but because the scope was the piece I was least sure I could do, and the piece whose contract had to be right before anything sat on top of it. I wrote docs/scope-architecture.md first. The implementation commit cites it section by section.

The wire format is deliberately boring. Two sync bytes, 0xA5 0x5A. A type, flags, a u16 sequence number, a payload length capped at 2048, a u32 first-sample index, then samples packed as twelve bits of ADC count with the channel index in bits 12 to 15 — CRC-16/CCITT-FALSE over everything after the sync pair, little-endian throughout. The frame scanner holds a trailing lone 0xA5 between reads in case it is half a sync pair, and a bad length or a bad CRC rewinds one byte and rescans rather than throwing the buffer away.

Then the bug. The firmware reports sps per channel, and the engine was dividing it by the pin count. Every dual-channel capture came out with its time base halved — which looks entirely plausible on screen and is completely wrong. The spec now says so in bold, and a regression test holds it there.

A specification that does not name its units has not specified anything.


Libraries Are Where the IDE Gives Up

This is the part I am most quietly proud of, and the part that most justifies the project existing.

Three things shipped. Local libraries pin into the active profile as dir: entries, which is the workflow the IDE simply has no surface for:

yaml
profiles:
  esp32s3:
    fqbn: esp32:esp32:esp32s3:CDCOnBoot=cdc
    platforms:
      - platform: esp32:esp32 (3.3.11)
    libraries:
      - dir: ../libs/HomeNode

A scaffolder writes a new library that compiles as-is — library.properties with every key the specification requires, a stub class, tab-separated keywords, and an example that builds. And a library can come from a git repository by alias: @kayaman/Arduino/libraries/HomeNode, resolved to a version and vendored in.

That last one had the most interesting design. arduino-cli can install from a git URL, but only after you flip a global library.enable_unsafe_install flag, and then it installs into the sketchbook that profile builds exclude — unusable on both counts. The GitHub REST API would have meant tokens and rate limits. git ls-remote --tags needs neither, works against any git host, and removed authentication from the design entirely.

Annotated tags list twice — once as the tag object, once as a ^{} row for the commit it dereferences to. Only the commit is a valid pin. So bancada.yaml records both the ref you chose and the SHA it resolved to, and restoring refuses when the two have drifted, reporting both.

A tag can move. A commit cannot. Recording only the first one is not a pin; it is a wish.


The Things I Refused to Do

A project like this accumulates a list of things it could do and does not, and that list describes it better than the feature list does.

Bancada will not flip library.enable_unsafe_install to make git installs work. It will not write to board_manager.additional_urls to add a third-party board index — it reads whatever your arduino-cli already knows, and a platform with no configured index simply does not appear, behind an empty state that names the command you would run yourself. It never passes --overwrite. When New Project needed a form, the form replaced the editor area rather than introducing the app’s first modal.

The first two are the same refusal wearing different clothes. An application that improves your workflow by quietly weakening your tooling’s security posture has not improved your workflow. It has moved the cost somewhere you will not look.

Some of these cost real work. Pinning a platform version has to be written by hand, because arduino-cli offers nothing that edits an existing profile. profile create advertises “Create or update” and then does this:

console
$ arduino-cli profile create -m uno -b arduino:avr:uno ./demo
Error initializing the project file: Profile 'uno' already exists

Hand-writing YAML where a tool should have done it is the price of not making someone’s machine worse. I paid it.


Being Wrong Is Worse Than Being Silent

Ports are ephemeral. /dev/ttyACM0 is whichever board enumerated first, which makes it a terrible name for a thing you own. The Fleet panel keys boards by MAC address instead, in a fleet.json that accumulates nicknames and history.

On the hardware that matters, this is nearly free. A native-USB ESP32 burns its MAC into the USB descriptor, so board list already reports it as properties.serialNumber. The board identifies itself by being plugged in — no esptool, no port takeover, no bootloader reset. Three real shapes exist and all three are covered: an ESP32 whose serial is a MAC, an UNO Q whose serial is a genuine per-unit id but not a MAC, and a CP210x bridge with no serial at all, whose hardware_id belongs to the bridge and therefore cannot tell two boards apart.

Then a plain dev board on my desk got labelled Ozobot DRVKit, with total confidence.

arduino-cli had matched a family-wide USB vid and pid rather than a specific board, and offered an arbitrary member of that family as the answer. The tell was in the match set — a hidden esp32:esp32:esp32_family umbrella entry sitting alongside the named sibling. Fleet now recognises that shape and declines to name the board at all, falling back to the chip type or the MAC. Both are true, and neither is charming.

The port dropdown had the mirror-image bug. The rule was cur ?? first-serial, evaluated once and never revalidated, so unplugging a board left the dead selection in place: the rescan looked like it had done nothing, and the next upload aimed at a device that was not there. It is a pure function with tests now, because it is the kind of thing that is easy to get subtly wrong and impossible to notice.


The Tests That Needed a Guard

ArduinoCli.bin is only a path. Which means the whole subprocess layer — the spawn, the JSON parse, the three error messages a user actually reads — can be exercised by pointing it at a shell script. No fixtures, no arduino-cli, and no breaking someone’s machine to reach the failure paths.

The harness was flaky before it was right. Seven failures in twelve parallel runs, all ETXTBSY. Writing a script and then executing it is unsafe under concurrency: another thread’s fork inherits the still-open write descriptor, and the exec is refused until that child execs and drops it. The files were distinct. The hazard was the concurrency, not a shared path, so the fix is one guard keeping a single write-then-exec in flight at a time. Zero failures in fifteen runs afterwards.

Adding coverage measurement turned a guess into a number: cli.rs at 39% line coverage against 84% for the crate. It is 86% now, and the crate is 91%. esptool.rs sits at 62% and I know exactly why — its uncovered half searches PATH rather than taking a binary path, so the stub trick does not reach it.

The best return came from a test written purely for completeness. esptool 5 prints Chip type: where esptool 4 printed Chip is. It pads labels to twenty columns. On the C6, H2 and H4 families it prints only BASE MAC, with no bare MAC line at all. On the version installed on this machine, the chip type had been silently vanishing from the UI for as long as the feature had existed.

I found that writing a test. Not using the app.


What Two Days Taught

Write the protocol down before you write the protocol. The scope spec cost an hour, and the implementation cites it by section number. The one thing the spec failed to state — that sps is per channel — is the one thing that shipped broken.

A pin that records only what you asked for is not a pin. Record what it resolved to as well, and refuse when the two disagree. Anything less is a version number with good intentions.

Refusing to weaken someone’s tooling is a feature. It costs you code every time. Pay it. The alternative is an app that is pleasant right up until it is the reason something bad happened.

Being wrong is worse than being silent. A confident wrong label is not a partial success. In a panel whose entire job is telling one board from another, the name is the product.

The bug you find writing tests is the one you were already shipping. Coverage arguments are usually about hypothetical bugs. This one produced a live one inside the first hour.

I opened by saying sketch.yaml is the reason the project exists, and after two days that is still the only claim I am fully confident about. The editor is ordinary. The oscilloscope is the fun part. What I actually wanted was a build I can repeat in six months, and a list of my own boards that knows which one is which.

0.3.0 is tagged and pushed. The suite that proves any of this actually works against real hardware is still a document rather than a test.

That is the next commit.