# From Proof of Concept to Production: snapmaker_moonraker Reaches v1.1.0
In February, we published [Bridging the Snapmaker J1S to the Moonraker Ecosystem](/bridging-the-snapmaker-j1s-to-the-moonraker-ecosystem/), describing how we built an open-source protocol bridge that makes the Snapmaker J1S behave like a Klipper printer. At the time of writing, the bridge could send files from PrusaSlicer, monitor temperatures, track prints, and stream a webcam — but it was still missing pieces. Spoolman integration was on the roadmap. Dual-extruder control was incomplete. The J1S’s IDEX capabilities — Copy and Mirror mode — were untouched.
Four weeks and forty commits later, every item on that roadmap is done, and the bridge has picked up capabilities we hadn’t originally planned. This post covers the journey from v0.0.7 to v1.1.0: what changed, what we learned, and where the project stands today.
### The Token Problem That Wasn’t
The original post listed “token refresh and persistence” as a future challenge. Snapmaker printers authenticate HTTP API connections with a token that must be confirmed by tapping the touchscreen, and that token is lost on power cycle. We expected to need a token refresh mechanism.
It turns out we don’t. The J1S’s SACP protocol on port 8888 — the only communication channel the bridge uses — does not require token authentication at all. The token was only relevant to the HTTP API on port 8080, which we confirmed early on is closed on the J1S. Since the bridge routes everything through SACP, the touchscreen confirmation issue simply doesn’t apply. The token field still exists in the configuration file, but it’s vestigial — a reminder of the HTTP API path we never took.
This is a good example of how constraints can simplify a design. Being forced into SACP-only communication initially felt like a limitation, but it eliminated an entire class of authentication and session management problems.
### Teaching the Bridge to Speak GCode: The Post-Processor
The most significant new subsystem is the GCode post-processor, introduced in v0.0.8. The problem it solves is subtle but critical: the Snapmaker J1S’s touchscreen HMI reads metadata from a custom header block at the top of the GCode file. Without this header, the touchscreen displays no print information — no estimated time, no material type, no thumbnail preview. Luban generates these headers automatically; PrusaSlicer does not.
The post-processor runs as a two-pass pipeline during every file upload.
**Pass 1** scans the entire file to extract metadata: per-extruder temperatures from M104/M109 commands, bed temperature from M140/M190, bounding box from G0/G1 moves, per-tool filament usage in millimeters (tracking absolute versus relative extrusion separately for each tool), layer height, estimated print time, and filament type from slicer comments. It also detects M605 commands for IDEX mode selection — more on that later.
**Pass 2** transforms the GCode line by line. Three transformations happen here:
**Tool number remapping.** PrusaSlicer’s dual-extruder configuration assigns tools as T2 and T3 (one per physical printer in its internal model). The J1S expects T0 and T1. The post-processor remaps all tool numbers modulo 2 — T2 becomes T0, T3 becomes T1 — across tool change commands, temperature commands (M104/M109), and fan commands (M106/M107).
**Unused nozzle shutoff.** When a tool change occurs and the post-processor knows from its first-pass analysis that the previous tool won’t be used again in the file, it injects `M104 S0 Tx` to turn off that heater. On a dual-extruder printer where many jobs only use one nozzle for part of the print, this prevents an idle hotend from sitting at temperature unnecessarily — saving energy and reducing the risk of heat creep.
**Snapmaker V1 header generation.** The post-processor builds a 25-line metadata header in Snapmaker’s V1 format, containing the printer name, estimated time, total line count, per-extruder fields (nozzle diameter, material, temperature, retraction settings), bed temperature, work range bounding box, and — crucially — the IDEX extruder mode.
The header also includes a **thumbnail**. The post-processor extracts PrusaSlicer and OrcaSlicer thumbnail blocks (base64-encoded PNG data embedded in GCode comments) and converts them into the data URI format the J1S touchscreen expects. This means sliced files uploaded from Mainsail show proper thumbnail previews on the printer’s screen — a small detail that makes a real difference when managing multiple queued prints.
The post-processor is idempotent: if it detects a `;Header Start` marker already present, it returns the file unchanged.
### Native Print Control: Cutting Out the Middle Layer
The original bridge controlled prints by sending GCode commands (M24 for resume, M25 for pause) through SACP’s GCode execution channel. This worked, but it was an indirect path — we were asking the firmware to interpret text commands when we could talk to it in its native binary protocol.
In v0.1.0, print control was rewritten to use native SACP commands: `0xAC/0x04` for cancel, `0xAC/0x05` for pause, `0xAC/0x06` for resume. The bridge also gained a proper print start sequence using `sacp.StartScreenPrint()`, which initiates the print through the touchscreen MCU — ensuring the HMI stays in sync with the actual print state.
A related improvement in v0.0.8 was the **double-disconnect upload pattern**. When uploading a file, the bridge discovered that the J1S touchscreen needs time to index the new file before it can be printed. The solution is a specific sequence: upload the file, disconnect from SACP, wait three seconds for the HMI to finalize its file index, reconnect, and then issue the print command. Without this pause, the touchscreen doesn’t recognize the file and the print fails to start. This was one of those behaviors that no documentation describes — it took repeated real-printer testing to identify and solve.
The upload sequence also gained **auto-start** capability: when a file is uploaded from Mainsail with the “start print” option, the bridge handles the full upload → disconnect → reconnect → start cycle automatically.
### Dual Extruder Control: Two Nozzles, Two Fans, Two Spools
The original bridge could read temperatures from both extruders but couldn’t independently control them. v0.2.0 introduced full dual-extruder temperature control, intercepting M104/M109 (extruder) and M140/M190 (bed) commands from the Mainsail console and routing them through SACP’s `SetToolTemperature` (CommandSet `0x10`, CommandID `0x02`) and `SetBedTemperature` (`0x14/0x02`) commands. The `ACTIVATE_EXTRUDER` Klipper command switches which extruder Mainsail considers “active.”
v1.0.0 extended this to **fan control**. The J1S has independent part cooling fans per extruder, and the bridge now exposes them as separate Moonraker fan objects — `extruder_partfan` and `extruder1_partfan`. M106/M107 commands are routed to the correct physical fan based on the P parameter (with the same mod-2 remapping as tool numbers), and Klipper’s `SET_FAN_SPEED` command works for direct fan control from the Mainsail interface.
### Spoolman: From Single Spool to Per-Extruder Tracking
The original post listed Spoolman integration as the primary remaining milestone. v0.0.7 delivered the initial implementation: browse and select spools from Mainsail’s Spoolman panel, report filament consumption back to the Spoolman server during prints, and health-check the Spoolman connection.
But the first implementation tracked only a single spool — fine for single-extruder jobs, wrong for a dual-extruder printer. v1.0.0 rebuilt the tracking to be per-extruder.
The changes touched every layer:
**Database storage** went from a single `spoolman.spool_id` key to `spoolman.spool_id.0` and `spoolman.spool_id.1`, with automatic migration of the old key to tool 0 on first startup.
**The API** gained a `tool` parameter on both the HTTP endpoints (`GET/POST /server/spoolman/spool_id`) and the WebSocket RPC handlers, matching the extended Moonraker API that Mainsail’s Spoolman panel uses for multi-extruder setups.
**Filament accounting** in the GCode post-processor was fixed to track extrusion per-tool independently. The original single `lastAbsE` accumulator was split into `lastAbsE[2]`, one per extruder. Without this fix, a dual-extruder job would report all filament usage against whichever tool was active last.
**Usage reporting** during prints calculates deltas per-tool from the GCode line number (mapped against per-tool cumulative filament arrays extracted during post-processing) and sends independent PUT requests to the Spoolman server for each tool’s spool. Reports only fire when the delta exceeds 0.1mm, avoiding noise from travel moves and retractions.
A final piece landed post-v1.0.0: the bridge now exposes a `tool_spool_map` in the Spoolman API response, which is what Mainsail’s multi-extruder Spoolman panel reads to display spool assignments per tool. Without this field, Mainsail falls back to single-spool mode regardless of how many spools are configured on the backend.
The result: Mainsail shows the correct spool assigned to each extruder, filament usage is tracked independently, and the Spoolman server has accurate per-spool consumption data — the same setup we run on our single-extruder Klipper machines, but extended for dual extrusion.
### IDEX Copy and Mirror Mode
The J1S is an IDEX (Independent Dual Extrusion) printer, meaning its two print heads can move independently. Beyond standard dual-material printing, IDEX enables two productivity modes: **Copy** (both heads print the same part simultaneously, doubling throughput) and **Mirror** (both heads print mirrored copies, useful for symmetric parts). Getting these modes to work through the bridge required coordinating three separate mechanisms.
**GCode detection.** PrusaSlicer signals IDEX mode via `M605 S2` (Copy/Duplication) or `M605 S3` (Mirror) in the start GCode. The post-processor’s first pass detects these commands and records the mode.
**V1 header.** The detected mode is written into the Snapmaker V1 header as `;Extruder Mode:Duplication` or `;Extruder Mode:Mirror`. This was the critical discovery — the J1S HMI reads this header field to configure the print mode before executing GCode. Without the correct header, the printer ignores M605 entirely and prints in default mode.
**SACP SetPrintMode command.** As an additional reliability measure, the bridge sends SACP command `0xAC/0x0A` with the appropriate mode byte (`0x02` for Duplication, `0x03` for Mirror) after upload and before starting the print. This ensures the firmware is in the correct mode even if the HMI header parsing has edge cases.
v1.1.0 also ships with **twelve PrusaSlicer printer profiles** covering all three modes (Default, Copy, Mirror) with eight quality presets each (from 0.08mm ultra-fine to 0.28mm draft). The Copy and Mirror profiles use half-bed dimensions (150×200mm) to account for the mirrored/duplicated print area, and include the correct M605 commands in their start GCode. These profiles mean a user can go from installing the bridge to printing in IDEX Copy mode without manually configuring anything in PrusaSlicer.
### Security Hardening
v0.2.1 was a dedicated security release. The bridge is a network service that accepts file uploads and executes commands on a printer — surface area that warrants careful attention.
**Path traversal prevention.** All file operation endpoints (upload, download, delete, move, copy, directory creation) now validate that resolved paths stay within the GCode storage directory. Attempts to escape via `../` sequences are rejected before any filesystem operation occurs.
**Namespace injection prevention.** The database API’s namespace parameter is validated against a strict allowlist pattern, preventing injection of arbitrary keys into the persistent storage.
**Header sanitization.** HTTP response headers constructed from user-supplied values (like filenames in Content-Disposition) are sanitized to prevent header injection attacks.
**WebSocket message size limits.** WebSocket connections enforce a maximum message size to prevent memory exhaustion from oversized payloads.
**Systemctl command validation.** The service management API (used by Mainsail to restart/stop services) validates service names against a strict allowlist before passing them to systemctl.
None of these were responses to discovered vulnerabilities — they were proactive hardening based on reviewing attack surface. For a service running on a Raspberry Pi on a local network, the risk is low, but the cost of doing it right is also low.
### Temperature History and the Ring Buffer
One of the quieter improvements in v0.2.0 was the addition of a **temperature history store** — a 1200-reading ring buffer per sensor that feeds Mainsail’s temperature graph. The original bridge reported only current temperatures; Mainsail’s graph showed a single point that jumped with each update. With the ring buffer, the graph displays a smooth rolling history, matching the behavior users expect from a real Moonraker instance.
### SACP Connection Resilience
The original post noted that the SACP connection “occasionally times out, though auto-reconnect recovers quickly.” v0.1.1 addressed this with a proper retry mechanism: when the bridge loses its SACP connection, it attempts to reconnect up to five times with a two-second delay between attempts. The delay matters because the J1S firmware sometimes needs time to clean up a dropped connection before accepting a new one — reconnecting too quickly results in a refused connection.
The bridge also gained **disconnect/connect commands** accessible from Mainsail’s service management panel in v0.2.0, allowing manual reconnection without restarting the entire bridge process.
### Cross-Platform Builds
Starting with v0.1.1, the CI pipeline produces binaries for three platforms: Linux x86_64, Windows x86_64, and macOS ARM64 (Apple Silicon). The primary use case remains the Raspberry Pi SD card image, but cross-platform binaries mean the bridge can run on a desktop machine for development or testing — or on any Linux server on the network, not just a Pi.
### Where We Are Now
The original post ended with a list of future work. Here’s where each item stands:
| Original Roadmap Item | Status |
|—|—|
| Spoolman filament tracking | Done (v0.0.7), upgraded to per-extruder (v1.0.0) |
| 0% progress for touchscreen-initiated prints | Resolved — native SACP print control means prints are always started through the bridge |
| SACP connection reliability | Done (v0.1.1) — 5-attempt retry with configurable delay |
| Token refresh/persistence | Not needed — SACP requires no authentication |
And the capabilities that weren’t on the original roadmap but arrived anyway:
– GCode post-processor with Snapmaker V1 headers, tool remapping, nozzle shutoff, and HMI thumbnails
– IDEX Copy and Mirror mode support with PrusaSlicer profiles
– Full dual-extruder temperature and fan control
– Security hardening across all network-facing endpoints
– Cross-platform binaries
– Mainsail config file editor and temperature history graphs
The version number tells the story. In February, v0.0.6 was a functional proof of concept — it could do the core workflow (slice → upload → print → monitor) but with rough edges and missing pieces. Today, v1.1.0 is a production tool that handles every printing mode the J1S supports, tracks filament per-extruder, provides proper security boundaries, and ships with ready-to-use slicer profiles.
The Snapmaker J1S is now a full citizen in our print farm. It slices from the same PrusaSlicer installation, uploads to the same Mainsail interface, tracks filament in the same Spoolman instance, and monitors prints with the same Obico server as every other machine on the floor. The protocol bridge that started as a way to avoid using Luban has become a complete fleet integration layer.
### What’s Left
The project isn’t finished — software never is — but the remaining items are refinements rather than missing capabilities:
– **Z baby-stepping** (M290) is accepted but not yet implemented via SACP. This is a convenience feature for live first-layer adjustments.
– **Broader printer support.** The bridge is built for the J1S, but the SACP protocol is shared across Snapmaker’s lineup. The GCode post-processor already generates V0 headers for the A150/A250/A350/Artisan models. Extending full support to other Snapmaker printers is architecturally feasible, though each model will have its own protocol quirks to discover.
– **Community testing.** The project has been developed and tested against a single J1S. More users means more edge cases, firmware versions, and network configurations — the kind of real-world validation that no amount of solo testing can replace.
### Open Source & AI Disclosure
snapmaker_moonraker remains available on [GitHub](https://github.com/goeland86/snapmaker_moonraker) under the MIT License. It builds on the SACP protocol work from sm2uploader by macdylan and snapmaker-sm2uploader by kanocz.
This project continues to be developed with assistance from Claude (Anthropic). Every commit in the repository is co-authored, reflecting a human-AI collaboration workflow that has proven effective for this kind of protocol-level systems programming — particularly for the tedious but critical work of binary protocol parsing, where an AI collaborator that doesn’t lose track of byte offsets and endianness across a multi-hour session is genuinely useful.




