fusible 0.14.3

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

Fusible

The fuse for your firmware. An electronic linter and tscircuit project manager in Rust: it reads the Circuit JSON generated by tscircuit, checks the electrical and physical rules of the board, and when a rule fails, the fuse blows: the process exits with an error and firmware code generation never happens.

Fusible unifies hardware and firmware into a single monorepo contract using fusible.yml, wrapping development servers, dark-theme web previewers, artifact exports, and firmware codegen.

demo1 demo4

Project status

Phase Scope Status
1 (MVP) CLI, Circuit JSON parser, rules engine, diagnostics shipped
1.1 Project manifest (fusible.yml), toolchain detection, package manager shipped
2 Silicon pin-muxing (src/silicon/) for STM32, RP2040, ESP32-S3 shipped
2 Firmware codegen (src/codegen/pins.rs) for Embassy / Cortex-M shipped
2.1 Pure-Rust RS-274X Extended Gerber & Excellon drill generator shipped
2.2 Pure-Rust vector documentation exporter (PCB & schematic SVGs) shipped
2.3 Native Fusible Studio dev server with dark theme and live SSE reload shipped
2.4 Deterministic autorouter and bidirectional KiCad syncer shipped
2.5 Fuse-gated 3D enclosure generator on OpenCascade shipped
2.6 Multi-board projects: named boards, firmware, and bodies shipped

Prerequisites

For pure Circuit JSON projects, Gerber exports, SVG rendering, and linter checks, Fusible runs with zero external dependencies.

To compile React .tsx circuits or export 3D STEP models, install tscircuit globally or in your workspace:

# Using bun:
bun add -g tscircuit

# Or using npm:
npm install -g tscircuit

Inspect toolchain health anytime with fusible doctor (or run fusible doctor --install to provision it).

Installation

cargo install fusible

Quick start (monorepo)

Initialize a new hardware + firmware project:

fusible init --name sensor-node

This creates a minimal fusible.yml:

name: sensor-node

boards:
  main:
    entry: hardware/index.tsx
    out_dir: dist
    port: 3020
    mcu: stm32g431

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

One project, three worlds

A Fusible project links circuit boards, firmware, and physical bodies. Each section holds named entries, and names are unique across the 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 }

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 }                        # drawn in FreeCAD, Fusion, Onshape...
  knob:  { source: bodies/knob.step, build: python bodies/knob.py }   # code-CAD
  lid:   { generate: enclosure, board: sensor }              # Fusible owns and regenerates it
  • 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 dev need a board name.
  • A firmware or generated body must name its board: when the project has more than one. An authored body may name one too: it is then modeled in that board's coordinate frame, which is what the Studio overlay relies on. around: is still accepted as an alias of board: for bodies.
  • The single-board form (hardware:, mcu: { chip }, one firmware:, mechanical:) still loads. It becomes a board named main, a firmware named firmware, and a generated body named after the project, which keeps writing dist/mechanical/<project>.<format>.

Commands

1. Electrical linter (check)

fusible check                     # uses fusible.yml entry
fusible check hardware/index.tsx  # compiles .tsx on the fly and lints
fusible --input circuit.json      # direct Circuit JSON input
fusible check --mcu stm32g431     # validates alternate functions against silicon

Compiling .tsx through tscircuit stops after 120 seconds. Set FUSIBLE_BUILD_TIMEOUT_SECS to change the limit for boards that take longer to route.

Clean circuit (exit code 0):

Circuit validated (fuse intact)

Rule violated (exit code 1):

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

2. Fusible Studio dev server (dev / view / webview)

Starts the native dark-theme web previewer powered by an embedded Rust HTTP server and live-reload event streamer:

fusible dev                       # launches native Fusible Studio on port 3020, every board of the project
fusible dev sensor                # same studio, opened on one board
fusible view --port 3030          # custom port
fusible dev --legacy              # fallback to legacy tsci dev server

Features:

  • Instant startup with zero Node runtime required for Circuit JSON.
  • Live-reload streaming via Server-Sent Events (SSE) with 150ms debounced filesystem watching.
  • Vitrine brutalist dark UI palette (#000000, #131313, #353535, #86A881, #A8B365) and embedded Undefined monospace typography.
  • Interactive pan and zoom vector canvas for multi-layer PCB layout (01 // PCB LAYOUT) and schematic diagrams (02 // SCHEMATIC).
  • Interactive Three.js 3D board viewer (03 // 3D VIEW) rendering substrate, ENIG gold pads, copper vias, traces, and components with OrbitControls and view presets.
  • A project tree on the left lists the boards, firmware, and bodies of fusible.yml. Click a board to switch to it: schematic, PCB, 3D, parts, nets, and checks all follow. A dot shows each board's fuse state. Click a firmware to see the board it runs on, and a body to open the BODIES tab. The URL keeps the selection (?item=board:sensor), so a view can be bookmarked. The tree appears when the project has firmware, bodies, or several boards.
  • A RUNS tab starts check, build, and body from the browser, on one item (the play button in the tree) or on the whole project. It re-runs this same fusible binary, so the log and the result (OK, FUSE BLOWN, FAILED) match the terminal. One run goes at a time. Runs accept only names from fusible.yml, a JSON body, and a local Origin.
  • A PINS tab opens when you select a firmware. It lists the pin map of the board the firmware runs on, built by the code that writes pins.rs: each GPIO grouped by peripheral, with its signal, connected part, and Rust name (CAMERA_PINS.cam_d0). Pins with a pin_mux conflict are marked in red with the reason. Click a pin to inspect its net or connected part. BUILD runs the firmware build, and the tab shows its last result and whether pins.rs exists. The MCU comes from the board's mcu: setting.
  • The project tree collapses with [ or the chevron in its header, and Studio remembers the choice. Below 900px wide it becomes a modal drawer that opens from the button in the status bar: focus moves into it, the page behind it is inert, and Esc or a selection closes it and returns focus to the button. The tree, run list, and pin rows are real buttons, so they work from the keyboard. A failed run request (for example, a run is already going) shows its message above the log without replacing it.
  • BODIES tab showing the generated base and lid of the selected board, with lid and explode toggles. The 3D view has an ENCLOSURE toggle that draws the shell translucent around the board. The studio builds the model on demand from the first generated body of that board and rebuilds it when the circuit changes. Needs bun. Selecting an authored body (source:) in the tree draws that file instead, as one mesh in the board frame (mm, Z-up, X/Y equal to the PCB coordinates). GLB is served as is; STEP (.step, .stp) and STL are converted with occt-wasm and cached by path, modification time, and size, so an unchanged file is not converted again. The files are limited to 64 MiB. A missing file shows MISSING in the tree. Authored GLB files must already use millimetres and Z-up.
  • Real-time fuse status badge (glowing green when intact, pulsing red on blown fuse).
  • Diagnostics inspector showing rule violations, severity, and remediation advice.
  • PCB layer visibility toggles (top copper, bottom copper, ENIG pads, silkscreen).

3. Autorouter (route)

Multi-layer A* autorouter on pure Rust spatial R-trees (rstar). Nets route one at a time in a fixed order, and each finished trace becomes an obstacle for the next, so routed nets cannot short:

fusible route                             # routes entry circuit and writes dist/circuit.routed.json
fusible route --dry-run                   # verifies all nets can be routed without writing files
fusible route -o dist/routed.json         # custom destination
fusible route --clearance 0.2 --layers 2  # custom clearance and layer count

When routing congestion prevents a trace from completing, the fuse blows (exit code 1) and lists unrouted nets to protect physical fabrication.

4. Bidirectional KiCad syncer (diff and sync)

Bi-directional layout reconciliation between graphical CAD tools (KiCad PCB) and declarative code (tscircuit):

# Compare component placement and report layout drift:
fusible diff path/to/board.kicad_pcb

# Synchronize physical moves and rotations back into manual-edits.json:
fusible sync path/to/board.kicad_pcb --write

# Dry run sync without modifying disk:
fusible sync path/to/board.kicad_pcb --dry-run

5. Build (build)

Evaluates the boards, lints electrical rules, generates typed firmware pin bindings (pins.rs), and compiles firmware only if every board keeps the fuse intact:

fusible build             # every board, then every firmware
fusible build main-fw     # one firmware and the board it runs on

6. Fuse-gated deliverables (export)

Exports fabrication-ready Gerbers, KiCad projects, SVGs, 3D models (STEP/GLB), or documentation. All exports are fuse-gated: electrical rules are verified first. If a rule fails, the fuse blows and no artifacts are produced.

# Presets:
fusible export --preset fab       # RS-274X Gerbers (.zip) + Circuit JSON via native Rust
fusible export --preset cad       # KiCad project (.zip) + 3D STEP (.step)
fusible export --preset docs      # Native PCB SVG + Schematic SVG (<5ms)
fusible export --preset all       # Generates all export bundles

# Specific format exports:
fusible export -f gerbers -o dist/gerbers.zip
fusible export -f pcb-svg -o dist/pcb.svg
fusible export -f step
fusible export -f kicad_zip

# Bypass verification (for debugging/drafts):
fusible export --preset fab --skip-lint

7. Physical bodies (body)

Builds the bodies of the project. A body is either authored or generated:

  • Authored (source:): a file you made in any CAD tool. Fusible checks that it exists and never overwrites it. With build:, Fusible first runs that command from the manifest directory, for code-CAD tools such as build123d, CadQuery, or OpenSCAD.
  • Generated (generate: enclosure): a two-part enclosure (base + lid) around the board outline in Circuit JSON. Fusible owns the file and regenerates it on every run. Generation is fuse-gated like export.
fusible body                             # every body of the manifest
fusible body case                        # one body
fusible body sensor                      # the generated bodies around one board
fusible body -f all                      # step, stl, glb, and brep into dist/bodies/
fusible body --ventilation grid          # vent holes in the lid (grid | slots)
fusible body --wall-thickness 2.5 --height 25
fusible body --no-cutouts --no-mounting

A board without any body gets a default enclosure, so fusible body works without a bodies: section. mechanical, case, and housing remain aliases of body.

Start from the generated body, or design your own

Generation is a feature, not a lock. Two commands hand the design over to you:

fusible body eject lid                   # generate once into bodies/lid.step, then switch the manifest to source:
fusible body eject lid -o parts/lid.stl  # another file; the extension picks the format
fusible body kit sensor                  # reference kit in bodies/kit/sensor/
  • eject is fuse-gated like body (--skip-lint for drafts). It stages the model next to its final path and moves it into place only when generation succeeds, never replaces an existing file, and replaces fusible.yml in one step, keeping your comments. If the entry has a shape it cannot edit safely, it leaves the manifest alone, writes the model, and exits 2 with the lines to change by hand. Afterwards the body is authored: edit the file in your CAD tool and Fusible never touches it again.
  • kit writes kit.json for people who model from scratch: board outline and thickness, every hole (x, y, diameter, plated), every component footprint (x, y is the center, w x h the size, layer, touches_edge), and the openings where a component reaches an outline edge (side is +x, -x, +y, or -y). Each opening gives the cut as two points on the board edge (from, to). The notch block holds the generator's constants (margin, edge_tolerance, clearance, extra_depth): the cut runs outward from the edge for clearance + wall thickness + extra_depth. Holes with no diameter are left out and named in warnings. Units are mm in the pcb frame the enclosure generator uses. board.step is added when the board entry is a tscircuit source and a runner is installed; a runner that fails the export is a tool error (exit 2) after kit.json is written. A bare circuit file goes to bodies/kit/<file name>/. The kit is fuse-gated.
  • body keeps its plain form, so eject and kit are reserved words. A body named eject or kit is rejected when fusible.yml loads.

Enclosure details:

  • The outline comes from pcb_board (outline points, or width x height).
  • Each pcb_hole gets a screw boss with a pilot hole, and the board sits on the bosses.
  • Components that reach the board edge get a wall notch from the board surface up.
  • CLI flags override the body settings in fusible.yml, which override defaults.
  • Requires bun. The first run installs occt-wasm into ~/.cache/fusible/. IGES is not supported.
  • fusible dev previews the first generated body in its BODIES tab and as a 3D overlay, without the fuse gate.

8. Hardware package management (add, install, search)

Manage tscircuit packages, community footprints, and components without touching npm or tsci directly:

# Search footprints and components in the tscircuit registry:
fusible search esp32-s3

# Add a community component or registry footprint to hardware/:
fusible add @tsci/seveibar.smd-resistor

# Install all hardware dependencies:
fusible install

9. Toolchain diagnostics (doctor)

Inspects local toolchain health (bun, node, npm, npx, tsci) and verifies tscircuit runner availability:

fusible doctor
fusible doctor --install

Parts and costs

fusible bom                              # every board: parts, quantities, prices, totals
fusible bom main -f csv -o parts.csv     # one board, as CSV
fusible bom -f json                      # the full report for scripts

Fusible lists the components placed on each board, grouped by kind, value, and part number, and adds each body of the manifest as a 3D print. Prices, picks, and extra parts live in bom.yml next to fusible.yml, created on the first read; the Studio PARTS & COSTS view edits the same file.

currency: EUR
print:
  price_per_kg: 25
parts:
  resistor-10kohm: {unit_price: 0.02, supplier: LCSC, url: "https://example.com/c25744"}
  m3-screw: {description: M3x8 screw, quantity: 8, unit_price: 0.05}
  chip-esp32-s3: {picked: false}
prints:
  case: {grams: 42}

A key that matches a detected part (the id of fusible bom -f json) 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 by kind and value (marked ~), and a body with an STL gets its weight from the model volume (PLA, 40% effective fill). Both yield to what you type in bom.yml.

Exit codes

The codes are part of the contract, so CI can tell failures apart:

Code Meaning
0 Fuse intact: circuit validated / command succeeded
1 Electrical violations: the fuse blew, generation is blocked
2 Tool or usage error (unreadable/malformed input, bad arguments)

Architecture

src/
├── main.rs                 # CLI entry point, exit code mapping
├── cli.rs                  # Clap command definitions
├── assets/                 # Embedded dark-theme web dashboard & Undefined font
├── dev/                    # Native Fusible Studio dev server (tiny_http, SSE, notify)
├── router/                 # Deterministic multi-layer autorouter (rstar, A*)
├── kicad/                  # S-expression parser & bidirectional layout drift syncer
├── commands/               # Subcommand execution pipelines (check, dev, route, diff, sync, export, build)
├── config/                 # fusible.yml manifest loader and path resolution
├── gerber/                 # Pure-Rust RS-274X Gerber and Excellon drill generator
├── svg/                    # Pure-Rust vector renderers for PCB layout and schematics
├── silicon/                # Silicon database and alternate function pin multiplexer
├── codegen/                # Typed firmware pin bindings generator (pins.rs)
├── parser/                 # Serde Circuit JSON parser and source span tracking
├── linter/                 # Fail-closed rules engine and diagnostics
├── netlist/                # Bipartite graph IR of nets, components, ports, and traces
└── tscircuit/              # Runner discovery and external process control

Development

mise install        # installs lefthook
cargo test          # test suite (85+ unit and integration tests)
cargo clippy --all-targets -- -D warnings
cargo fmt --all -- --check

License

MIT