fusible 0.20.6

Electronic linter: a fuse that blows to block firmware code generation
fusible-0.20.6 is not a library.

Fusible

One project for your circuit boards, your firmware, and the case around them. Fusible checks the whole design against rules. When a rule fails, the fuse blows: the command exits with an error, and nothing gets exported or generated.

Fusible reads the Circuit JSON that tscircuit produces, checks the electrical and physical rules of every board, and only then writes fabrication files, firmware pin bindings, and enclosures. It ships as a command line tool, a browser studio (fusible studio), and a desktop app.

demo1 demo4

What you get

Piece What it does
Linter Checks nets, shorts, clearance, decoupling, pin muxing, and cabled connectors.
Studio Live PCB, schematic, 3D, parts, firmware pins, bodies, and checks in the browser.
Fabrication Gerbers, drill files, KiCad projects, STEP, GLB, SVG, PDF. All behind the fuse.
Firmware Typed Rust pin bindings (pins.rs) for STM32, RP2040, and ESP32-S3.
Bodies Authored CAD files or a generated two-part enclosure, plus a kit to model your own.
Parts A bill of materials with prices, picks, and print costs.
Team A SYNC panel that shares the project through git, with no terminal.
KiCad The studio watches your .kicad_pcb and writes moved parts back into your TSX.

Install

cargo install fusible

Linting Circuit JSON, Gerbers, and SVG need nothing else. To compile .tsx boards or export STEP, install tscircuit and check the toolchain:

bun add -g tscircuit
fusible doctor            # reports bun, node, and tsci
fusible doctor --install  # installs what is missing

The desktop app (Windows, macOS, Linux) is attached to each GitHub release. It bundles bun, lists your projects, and runs fusible studio --desktop for each. The installers are not code-signed yet.

Quick start

fusible init --name sensor-node
fusible check
fusible studio

init writes a fusible.yml:

name: sensor-node

boards:
  main:
    entry: hardware/index.tsx
    mcu: stm32g431

firmware:
  app:
    board: main
    path: firmware
    framework: embassy
    pins_rs: firmware/src/pins.rs
    build_cmd: cargo build

fusible init --currency USD also writes a bom.yml.

The project file

A project has three kinds of named things. Names are unique across all three, so any command takes one as its target.

name: rover

boards:
  main:   { entry: boards/main/index.tsx, mcu: esp32-s3 }
  sensor: { entry: boards/sensor/index.tsx, mcu: rp2040, kicad: boards/sensor/sensor.kicad_pcb }

firmware:
  main-fw:   { board: main, path: firmware/main, pins_rs: firmware/main/src/pins.rs }
  sensor-fw: { board: sensor, path: firmware/sensor, pins_rs: firmware/sensor/src/pins.rs }

bodies:
  case: { source: bodies/case.step }
  knob: { source: bodies/knob.step, build: python bodies/knob.py }
  lid:  { generate: enclosure, board: sensor }

links:
  cable:
    from: main.J2
    to: sensor.J1
    wires:
      - { a: 1, b: 1, net: GND }
      - { a: 2, b: 3, nets: [TX, RX] }
  • fusible check checks every board and reports all violations together. fusible check sensor checks one.
  • fusible build sensor-fw checks the sensor board, writes its pin bindings, then builds that firmware.
  • fusible export main --preset fab writes into dist/main/fab/. With several boards, export and studio need a name.
  • A firmware or generated body names its board: when the project has several. An authored body may name one too: it is then modeled in that board's coordinate frame. around: still works as an alias.
  • The old single-board form (hardware:, mcu: { chip }, one firmware:, mechanical:) still loads.

Links between boards

A link says that a connector of one board is cabled to a connector of another. Each wire joins a pin a of from to a pin b of to. A pin is a number (1) or a name (TX).

  • No net: both ends must carry the same net name.
  • net: GND: both ends must carry GND.
  • nets: [TX, RX]: the first end must carry TX, the second RX. Use it for crossed wires.

The harness_mismatch rule blows the fuse when a connector or pin does not exist, a pin is wired to nothing, or the nets do not match. It runs after the boards pass, in fusible check and fusible build, when every board is checked.

Commands

check: the electrical linter

fusible check                       # every board of fusible.yml, then every link
fusible check sensor                # one board
fusible check hardware/index.tsx    # compiles TSX on the fly
fusible check --mcu stm32g431       # also validates pin muxing against the silicon
fusible --input circuit.json        # Circuit JSON directly

Compiling TSX stops after 120 seconds. Set FUSIBLE_BUILD_TIMEOUT_SECS to change that.

Fusible caches compiled circuits and exports in .fusible/cache. Unchanged sources skip tsci and print a ⚡ line. Set FUSIBLE_NO_CACHE=1 to bypass it.

the fuse blew: 1 electrical violation(s)
[unconnected_net] net "I2C_SDA" has only 1 connection(s) (minimum 2): it is floating
help: every net must connect at least two pads/ports: complete the routing in tscircuit

The rules: unconnected_net, ghost_net, shorted_nets, malformed_element, dangling_reference, trace_clearance, decoupling_capacitor, pin_mux, E_SCHEMATIC_SCALE_EXCESSIVE, harness_mismatch, and the three assembly rules (body_interference, hole_misaligned, connector_blocked). A rule that cannot prove a net is connected reports it. Silence is never the default.

studio: the studio

fusible studio               # every board, on port 3020
fusible studio sensor        # opened on one board
fusible studio --port 3030
fusible studio --desktop     # for the desktop shell

dev, view, and webview still work as aliases.

  • Project tree. Lists boards, firmware, and bodies, each with its fuse state. The URL keeps the selection (?item=board:sensor).
  • Views. PCB layout, schematic, 3D board, parts, nets, and checks follow the selected board. Layers toggle on and off.
  • RUNS. Starts check, build, and body from the browser with a live log. It runs the same fusible binary, so the result matches the terminal. One run at a time.
  • PINS. Select a firmware to see its pin map: each GPIO by peripheral, signal, connected part, and Rust name. Pins with a pin_mux conflict show the reason. BUILD runs the firmware build.
  • BODIES. Shows the generated base and lid with lid and explode toggles, or the selected authored body with the outline of its board: drawn beside it. GLB loads as is. STEP and STL convert with occt-wasm, cached by path, time, and size (64 MiB limit). Authored GLB must be in millimetres, Z-up.
  • PARTS & COSTS. Edits the same bom.yml as fusible bom.
  • SYNC. Appears when the project is inside a git work tree. SAVE stores your changes with a message, gets the others' work, puts yours on top, and sends it. GET UPDATES only gets theirs. When two people changed the same file, a card names the file and the author, and KEEP MINE or KEEP THEIRS decides. node_modules, dist, .fusible, and *.lck are never saved. Studio runs the real git with no shell and no prompt, so a missing sign-in fails with a message instead of hanging.
  • Narrow windows. The tree collapses with [ and becomes a drawer below 900px.

Studio serves only local requests. Run requests accept names from fusible.yml, a JSON body, and a local Origin.

route: the autorouter

fusible route                              # writes dist/circuit.routed.json
fusible route --dry-run                    # checks that every net can route
fusible route --clearance 0.2 --layers 2

A multi-layer A* router on R-trees. Each routed trace becomes an obstacle for the next, so routed nets cannot short. If a net cannot route, the fuse blows and the command lists the unrouted nets.

KiCad: diff, sync, and the studio watch

fusible diff board.kicad_pcb
fusible sync board.kicad_pcb --write
fusible sync board.kicad_pcb --dry-run

While the studio runs, it watches the board's KiCad file. KiCad owns placement. The TSX owns parts and nets.

  • Name the file per board with kicad: in fusible.yml. Without it, the studio uses the single .kicad_pcb beside the entry file or in a kicad/ folder.
  • On save in KiCad, the studio rewrites plain-number pcbX, pcbY, and pcbRotation props in your TSX, then rebuilds. An expression such as pcbX={base + 2}, a part without those props, or a flip to the other side is left alone and listed as a difference.
  • Before it writes a TSX file, the studio copies it to .fusible/backup/ and keeps the last 10 per file.
  • Added, removed, or renamed parts and changed nets are never written. A KICAD: n DIFFERENCES chip lists them. KICAD: ERROR means a file could not be read.

build

fusible build           # every board, then every firmware
fusible build main-fw   # one firmware and its board

Checks the boards and links, writes pins.rs, and builds the firmware only if the fuse is intact.

export: fuse-gated deliverables

fusible export --preset fab    # gerbers.zip + circuit.json into dist/fab/
fusible export --preset cad    # kicad.zip + board.step into dist/cad/
fusible export --preset docs   # schematic.svg, pcb.svg, schematic.pdf into dist/docs/
fusible export --preset all
fusible export -f gerbers -o dist/gerbers.zip
fusible export --preset fab --skip-lint    # drafts only

The rules run first. If one fails, no file is written. The PDF needs a local Chromium; without one, Fusible warns and writes the SVG only. Gerber, drill, and SVG output are pure Rust.

body: physical bodies

A body is authored (source:, a file from any CAD tool, with an optional build: command for code-CAD) or generated (generate: enclosure, a base and lid around the board outline). Fusible never overwrites an authored file. A generated body is fuse-gated and regenerated on every run.

fusible body                       # every body
fusible body case                  # one body
fusible body sensor                # the generated bodies around one board
fusible body -f all                # step, stl, glb, brep into dist/bodies/
fusible body --ventilation grid    # grid | slots
fusible body --wall-thickness 2.5 --height 25 --no-cutouts --no-mounting

A board without a body gets a default enclosure. mechanical, case, and housing are aliases of body. The enclosure comes from pcb_board: each pcb_hole gets a screw boss, and components that reach the board edge get a wall notch. Flags override fusible.yml, which overrides defaults. It needs bun, and the first run installs occt-wasm into ~/.cache/fusible/.

Start from the generated body, or design your own:

fusible body eject lid                    # writes bodies/lid.step and switches the manifest to source:
fusible body eject lid -o parts/lid.stl   # the extension picks the format
fusible body kit sensor                   # reference kit in bodies/kit/sensor/
  • eject is fuse-gated. It writes the model to a staging file, moves it into place only on success, never replaces an existing file, and edits fusible.yml in one step, keeping your comments. If the entry has a shape it cannot edit safely, it leaves the manifest alone and exits 2 with the lines to change by hand.
  • kit writes kit.json for people who model from scratch: the board outline and thickness, every hole, every component footprint (with z and height when the part has a 3D model), a keep_out box per modeled part (z0 to z1), and the wall openings where a component reaches an edge. The notch block holds the generator's constants. Units are mm in the PCB frame. board.step is added for TSX entries when a runner is installed.
  • eject and kit are reserved. A body with either name is rejected.

assembly: boards and bodies in one model

assembly:
  out_dir: dist          # default: dist
  format: glb            # glb, step, or all; default: glb
  parts:
    main:   { at: [0, 0, 0] }
    sensor: { at: [0, 0, 12], rotate: [0, 0, 90] }
    enclosure: {}        # a body follows the board it is modeled on
fusible assembly             # writes dist/assembly.glb
fusible asm -f all -o out    # assembly.glb and assembly.step in out/

Units are mm. rotate is in degrees around the fixed X, then Y, then Z axes, and applies before at. A body with a board (its board:, or the only board for a generated body) sits on that board, and its own at and rotate are added on top. That board must be a part too. A body without a board needs an at. A board left out of parts is not drawn.

The command is fuse-gated: every board it draws, and every board a generated body is fitted to, passes check first (--skip-lint skips it). A board needs its .tsx entry and a tscircuit runner, which exports its STEP model. A generated body is rebuilt for the assembly; an authored body is read from its source: (STEP or STL) after its build: runs. The models of each part land in dist/assembly/. Like body, it needs bun and occt-wasm.

Three rules compare the boards with the bodies and blow the fuse (exit 1, no assembly.* written):

  • body_interference: a body overlaps the board slab or a component's volume, or two bodies overlap.
  • hole_misaligned: a body has material near a board hole but no boss around it (fewer than 2 of 4 ring quadrants), or has any material inside the hole's bore (a pilot hole up to 0.5 mm narrower in radius is fine). A body fitted to a board (generated, or authored with board:) must also have a boss for every non-plated hole of that board: a hole with no body material near it blows the fuse. Plated holes (vias, pads) need no boss.
  • connector_blocked: a body stands in front of the opening a connector at the board edge needs.

Parts that only touch do not fire. Two bodies that overlap by more than 0.01 mm³ blow the fuse, so give a lip a clearance. Each run writes dist/assembly/check.json (fuse, and per violation the rule, parts, overlap volume in mm³, and position) and dist/assembly/preview.glb, which holds the bodies, the board slabs, and one collision:<n> node per overlap, even when the fuse blows. fusible check runs the rules too, when it checks every board. Without assembly: they are skipped. A declared assembly: without bun, or a body that is not a closed solid, is a tool error (exit 2), never a pass.

bom: parts and costs

fusible bom                         # every board
fusible bom main -f csv -o parts.csv
fusible bom -f json

Lists the placed components grouped by kind, value, and part number, and adds each body as a 3D print. bom.yml holds prices, picks, and manual parts:

currency: EUR
print:
  price_per_kg: 25
parts:
  resistor-10kohm: {unit_price: 0.02, supplier: LCSC}
  m3-screw: {description: M3x8 screw, quantity: 8, unit_price: 0.05}
prints:
  case: {grams: 42}

A key that matches a detected part overrides it. Any other key is a manual part. Print cost is grams / 1000 * price_per_kg. A part without a price gets an estimate, marked ~. bom never blows the fuse.

Packages and toolchain

fusible search esp32-s3
fusible add @tsci/seveibar.smd-resistor
fusible install
fusible doctor --json      # {"bun","node","tsci"}, exit 0

FUSIBLE_BUN names the bun binary for every command. Without a tsci, Fusible runs bun x --bun tsci.

Exit codes

Code Meaning
0 Fuse intact. The command succeeded.
1 Fuse blown. A rule failed and generation is blocked.
2 Tool error. Unreadable input, missing runner, or bad arguments.

CI can tell a bad design (1) from a broken setup (2).

CI

The repository is also a GitHub Action. It installs a release binary, sets up bun, runs fusible check, and fails the job when the fuse blows:

- uses: actions/checkout@v7
- id: fuse
  uses: KonoLabs/Fusible@v0.20.1
  with:
    target: ""        # optional board name or file; empty checks every board
    version: latest   # or a version such as 0.20.1

Other inputs: working-directory (default .) and install-deps (default true, runs bun install where a package.json sits). The fuse output is intact, blown, or error, from exit codes 0, 1, and 2.

Layout of the source

src/
├── cli.rs, dispatch.rs, main.rs   # commands and exit codes
├── commands/                      # check, build, export, body, bom, links, init, doctor
├── config/                        # fusible.yml: boards, firmware, bodies, links
├── parser/, netlist/, linter/     # pure Rust: Circuit JSON, net graph, rules
├── silicon/, codegen/             # pin muxing databases and pins.rs
├── gerber/, svg/, router/, kicad/ # fabrication, rendering, routing, KiCad sync
├── dev/                           # studio server (tiny_http, SSE, git, KiCad watch)
├── tscircuit/                     # runner discovery and process control
└── assets/                        # embedded studio
studio/                            # studio sources, built into src/assets/studio.bundle.js
desktop/                           # Tauri shell

The parser, netlist, and rules have no C dependencies. tscircuit, bun, and occt-wasm are used only for TSX, STEP, and bodies.

Development

cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test --locked
filet check .

Rebuild the studio bundle with cd studio && bun run build. See ROADMAP.md for what is next and CHANGELOG.md for what changed.

License

MIT