Open modular filament system for Bambu Lab printers — removes the 4-spool cap by automating the external-spool swap over MQTT
See the codeAn expandable, multi-printer Automatic Material System that breaks past the 16-slot ceiling of Bambu's stock AMS by driving the external-spool filament swap over local MQTT — no cloud, no proprietary AMS protocol.
What a Bambu printer will let you drive over MQTT — the commands confirmed against an A1 mini, the FTPS traps, and which parts are still a guess. Project page: amsozzer.com/projects/ams-x.
The spool-module firmware lives in its own repo, Amsozzer1/AMS-Firmware, and One ESP32, eight spools, two legs per filament move covers how it works and what is still missing.
Bambu Lab printers cap multi-material printing at the AMS's 4 slots (16 with the hub), and every slot is locked to Bambu's hardware. AMS-X routes around that limit instead of fighting it. The printer runs in its native external-spool mode, and a central server automates exactly what a person would do by hand at each material change: retract the spent filament, feed the next one until the printer's filament sensor trips, and resume the print. Because a "slot" is just "a module the server can drive," capacity is unbounded — dozens of spools, poolable across many printers.
The design documents are the source of truth for intent and rationale — start at docs/README.md.
AMS-X never authors hotend G-code. It drives Bambu's own filament-change routine over MQTT and owns only the filament logistics up to the printer's sensor. This keeps a clean boundary:
| AMS-X owns — filament logistics | Bambu owns — hotend physics |
|---|---|
| Which module, when; retract spent filament clear of the shared path; feed the next filament to the printer's inlet/sensor | Temperature, retract-from-melt-zone, load-to-nozzle, purge, wipe, resume |
The swap is driven by a hybrid trigger: the server parses the uploaded sliced 3MF into an ordered material-change plan (it is the source of truth for what comes next), while a live MQTT pause event is the trigger for when to act. Because the server also authors the change G-code, every planned pause is tagged — a stray user pause is handled as an exception, never mistaken for a swap.
PRINTING ──▶ PAUSE (MQTT) ──▶ UNLOAD current ──▶ SELECT next module ──▶ FEED
│
PRINTING ◀── LOAD + RESUME (MQTT) ◀── SENSE (filament sensor trips) ◀──┘
A Module is one interface with a swappable actuator behind it: today a ManualModule
prompts a human to perform the swap; a HardwareModule (motorized) drops into the same
contract later without touching the orchestrator.
A single brain is the only decision-making node. Every other part — the printer, the modules, the future ESP32 cluster controllers — is a non-sentient actuator that acts and reports; the brain handles every exception and is authoritative about which filament is loaded where.
┌────────────────────────── Brain — single source of truth ──────────────────────────┐
│ async Orchestrator · Printer state model · Job/3MF parser · Module registry │
│ MQTT broker (Mosquitto) · Inventory (Spoolman) · FastAPI + REST │
└──────┬──────────────────────────┬───────────────────────────────────┬───────────────┘
local MQTT + │ FTPS (LAN) local MQTT │ WiFi → MQTT │ (planned)
▼ ▼ ▼
Bambu printer Operator UI ESP32 cluster controller
(external-spool mode) (Next.js dashboard) one TMC2209 driver per module,
one module moves at a time
The server is layered so dependencies point downward — orchestration → job → printer/module → transport → inventory — with a small typed domain model at the core. The full class model is documented in docs/10-domain-model.md.
This is a monorepo. Each area has its own README with deeper detail.
| Path | What it is |
|---|---|
| server/ | The Brain — Python (FastAPI + async orchestrator + MQTT). Package layout mirrors the domain model. |
| frontend/ | Operator UI — a thin Next.js/React client that talks only to the Brain's REST API. |
| firmware/ | Pointer only. The firmware is its own repo: Amsozzer1/AMS-Firmware. |
| hardware/ | CAD, wiring, and BOM for the modules / hub / clusters. |
| deploy/ | Docker Compose for the CasaOS host (Brain + Mosquitto broker). |
| docs/ | Design docs: vision, architecture, protocol, domain model, roadmap, hardware. |
| Layer | Choices |
|---|---|
| Server | Python 3.11+, FastAPI, asyncio, paho-mqtt, Pydantic. Tooling: uv (env/deps), Ruff (lint + format), ty (types), pytest. |
| Frontend | Next.js 15 (App Router), React 19, TypeScript. No UI kit or data library — a hand-built design system and a tiny polling hook. |
| Transport | Local MQTT (TLS, LAN mode) for printer + cluster control; FTPS for LAN job upload; HTTP/REST for Spoolman inventory. |
| Firmware | ESP32 / Arduino via PlatformIO, C++17; TMC2209 stepper drivers. Written, not finished — see AMS-Firmware. |
| Deploy | Docker Compose on a CasaOS host — Brain plus a self-hosted Mosquitto broker. |
Rationale for each choice lives in docs/03-tech-stack.md.
The server runs in simulate mode by default — a built-in printer, FTP, and inventory simulator let the entire swap loop run end to end with no hardware.
Brain (server):
cd server
uv sync --extra dev # create .venv + install deps
AMSX_PORT=9001 uv run amsx # starts the API in simulate mode on :9001
Operator UI (frontend):
cd frontend
npm install
npm run dev # http://localhost:9000 (defaults to the Brain on :9001)
Exercise the swap loop with no printer — upload a sliced .gcode.3mf in the UI, confirm
the colour→module mapping, then drive the simulator through a swap:
curl -X POST 'http://127.0.0.1:9001/api/printers/{id}/sim/pause' # raise a swap prompt
# press-and-hold "mark done" in the Action Console, then:
curl -X POST 'http://127.0.0.1:9001/api/printers/{id}/sim/sensor?present=true' # resume
Quality gates:
cd server && uv run ruff check . && uv run ty check && uv run pytest
Software-complete for v0; the protocol thesis is verified on real hardware; motorized module hardware is the next physical build.
Proven and built
Written, not finished — the ESP32 cluster firmware now exists as
Amsozzer1/AMS-Firmware: about 900 lines of C++17,
eight modules on a shared step/dir bus with per-module enable, a pin map that arrives over MQTT
and is validated before anything is configured, and moves advanced in bursts so a multi-second
load never starves the MQTT keep-alive. It has been on the bench with four motors, not eight.
Module::sensedFilament() still returns false, so the per-module sensor is never read — which
costs more going backwards than forwards. On an unload arrived() is true the first time it is
evaluated, so the move stops before it pulses once and the motor never turns. Unloading does
not work yet.
Not yet built — the motorized HardwareModule on the server side, the N-module hub, and live
X1/P1 verification. These are Phase 1+ on the roadmap.
The phased plan (riskiest assumption first; no hardware until the software thesis holds) is in docs/04-roadmap.md, with the concrete v0 build in docs/07-v0-plan.md.
@todo decorator that
fails loudly and is greppable in one place — never a silent placeholder. See
CLAUDE.md..gitignore plus a gitleaks pre-commit hook. See
SECURITY.md.To be determined — tracked as open question #22 in docs/05-open-questions.md.
Python
66.9%
TypeScript
25.6%
CSS
7.5%
Open modular filament system for Bambu Lab printers — removes the 4-spool cap by automating the external-spool swap over MQTT
See the codeAn expandable, multi-printer Automatic Material System that breaks past the 16-slot ceiling of Bambu's stock AMS by driving the external-spool filament swap over local MQTT — no cloud, no proprietary AMS protocol.
What a Bambu printer will let you drive over MQTT — the commands confirmed against an A1 mini, the FTPS traps, and which parts are still a guess. Project page: amsozzer.com/projects/ams-x.
The spool-module firmware lives in its own repo, Amsozzer1/AMS-Firmware, and One ESP32, eight spools, two legs per filament move covers how it works and what is still missing.
Bambu Lab printers cap multi-material printing at the AMS's 4 slots (16 with the hub), and every slot is locked to Bambu's hardware. AMS-X routes around that limit instead of fighting it. The printer runs in its native external-spool mode, and a central server automates exactly what a person would do by hand at each material change: retract the spent filament, feed the next one until the printer's filament sensor trips, and resume the print. Because a "slot" is just "a module the server can drive," capacity is unbounded — dozens of spools, poolable across many printers.
The design documents are the source of truth for intent and rationale — start at docs/README.md.
AMS-X never authors hotend G-code. It drives Bambu's own filament-change routine over MQTT and owns only the filament logistics up to the printer's sensor. This keeps a clean boundary:
| AMS-X owns — filament logistics | Bambu owns — hotend physics |
|---|---|
| Which module, when; retract spent filament clear of the shared path; feed the next filament to the printer's inlet/sensor | Temperature, retract-from-melt-zone, load-to-nozzle, purge, wipe, resume |
The swap is driven by a hybrid trigger: the server parses the uploaded sliced 3MF into an ordered material-change plan (it is the source of truth for what comes next), while a live MQTT pause event is the trigger for when to act. Because the server also authors the change G-code, every planned pause is tagged — a stray user pause is handled as an exception, never mistaken for a swap.
PRINTING ──▶ PAUSE (MQTT) ──▶ UNLOAD current ──▶ SELECT next module ──▶ FEED
│
PRINTING ◀── LOAD + RESUME (MQTT) ◀── SENSE (filament sensor trips) ◀──┘
A Module is one interface with a swappable actuator behind it: today a ManualModule
prompts a human to perform the swap; a HardwareModule (motorized) drops into the same
contract later without touching the orchestrator.
A single brain is the only decision-making node. Every other part — the printer, the modules, the future ESP32 cluster controllers — is a non-sentient actuator that acts and reports; the brain handles every exception and is authoritative about which filament is loaded where.
┌────────────────────────── Brain — single source of truth ──────────────────────────┐
│ async Orchestrator · Printer state model · Job/3MF parser · Module registry │
│ MQTT broker (Mosquitto) · Inventory (Spoolman) · FastAPI + REST │
└──────┬──────────────────────────┬───────────────────────────────────┬───────────────┘
local MQTT + │ FTPS (LAN) local MQTT │ WiFi → MQTT │ (planned)
▼ ▼ ▼
Bambu printer Operator UI ESP32 cluster controller
(external-spool mode) (Next.js dashboard) one TMC2209 driver per module,
one module moves at a time
The server is layered so dependencies point downward — orchestration → job → printer/module → transport → inventory — with a small typed domain model at the core. The full class model is documented in docs/10-domain-model.md.
This is a monorepo. Each area has its own README with deeper detail.
| Path | What it is |
|---|---|
| server/ | The Brain — Python (FastAPI + async orchestrator + MQTT). Package layout mirrors the domain model. |
| frontend/ | Operator UI — a thin Next.js/React client that talks only to the Brain's REST API. |
| firmware/ | Pointer only. The firmware is its own repo: Amsozzer1/AMS-Firmware. |
| hardware/ | CAD, wiring, and BOM for the modules / hub / clusters. |
| deploy/ | Docker Compose for the CasaOS host (Brain + Mosquitto broker). |
| docs/ | Design docs: vision, architecture, protocol, domain model, roadmap, hardware. |
| Layer | Choices |
|---|---|
| Server | Python 3.11+, FastAPI, asyncio, paho-mqtt, Pydantic. Tooling: uv (env/deps), Ruff (lint + format), ty (types), pytest. |
| Frontend | Next.js 15 (App Router), React 19, TypeScript. No UI kit or data library — a hand-built design system and a tiny polling hook. |
| Transport | Local MQTT (TLS, LAN mode) for printer + cluster control; FTPS for LAN job upload; HTTP/REST for Spoolman inventory. |
| Firmware | ESP32 / Arduino via PlatformIO, C++17; TMC2209 stepper drivers. Written, not finished — see AMS-Firmware. |
| Deploy | Docker Compose on a CasaOS host — Brain plus a self-hosted Mosquitto broker. |
Rationale for each choice lives in docs/03-tech-stack.md.
The server runs in simulate mode by default — a built-in printer, FTP, and inventory simulator let the entire swap loop run end to end with no hardware.
Brain (server):
cd server
uv sync --extra dev # create .venv + install deps
AMSX_PORT=9001 uv run amsx # starts the API in simulate mode on :9001
Operator UI (frontend):
cd frontend
npm install
npm run dev # http://localhost:9000 (defaults to the Brain on :9001)
Exercise the swap loop with no printer — upload a sliced .gcode.3mf in the UI, confirm
the colour→module mapping, then drive the simulator through a swap:
curl -X POST 'http://127.0.0.1:9001/api/printers/{id}/sim/pause' # raise a swap prompt
# press-and-hold "mark done" in the Action Console, then:
curl -X POST 'http://127.0.0.1:9001/api/printers/{id}/sim/sensor?present=true' # resume
Quality gates:
cd server && uv run ruff check . && uv run ty check && uv run pytest
Software-complete for v0; the protocol thesis is verified on real hardware; motorized module hardware is the next physical build.
Proven and built
Written, not finished — the ESP32 cluster firmware now exists as
Amsozzer1/AMS-Firmware: about 900 lines of C++17,
eight modules on a shared step/dir bus with per-module enable, a pin map that arrives over MQTT
and is validated before anything is configured, and moves advanced in bursts so a multi-second
load never starves the MQTT keep-alive. It has been on the bench with four motors, not eight.
Module::sensedFilament() still returns false, so the per-module sensor is never read — which
costs more going backwards than forwards. On an unload arrived() is true the first time it is
evaluated, so the move stops before it pulses once and the motor never turns. Unloading does
not work yet.
Not yet built — the motorized HardwareModule on the server side, the N-module hub, and live
X1/P1 verification. These are Phase 1+ on the roadmap.
The phased plan (riskiest assumption first; no hardware until the software thesis holds) is in docs/04-roadmap.md, with the concrete v0 build in docs/07-v0-plan.md.
@todo decorator that
fails loudly and is greppable in one place — never a silent placeholder. See
CLAUDE.md..gitignore plus a gitleaks pre-commit hook. See
SECURITY.md.To be determined — tracked as open question #22 in docs/05-open-questions.md.
Python
66.9%
TypeScript
25.6%
CSS
7.5%