Install
$ npx skills add bbolinger/snapmaker-u1-toolkitREADME
# GitHub Repository: bbolinger/snapmaker-u1-toolkit
**URL:** https://github.com/bbolinger/snapmaker-u1-toolkit
**Author:** bbolinger
**Description:** Print to your Snapmaker U1 from your phone: Telegram, headless OrcaSlicer, Moonraker upload, camera verification, and a human-approved start gate.
**Homepage:** https://bbolinger.github.io/snapmaker-u1-toolkit
**Language:** Python
## Stats
- Stars: 19
- Forks: 0
- Open Issues: 2
- Commits: 317
- Created: 2026-06-22T19:01:11Z
- Updated: 2026-09-04T03:22:20Z
- Pushed: 2026-07-30T03:23:26Z
## README
# Snapmaker U1 Toolkit — Print From Your Phone with Telegram
[](https://github.com/bbolinger/snapmaker-u1-toolkit/actions/workflows/tests.yml)
Safely slice, preview, upload, monitor, and start [Snapmaker U1](https://snapmaker.com/snapmaker-u1) prints remotely through Telegram, OrcaSlicer, Moonraker, and an optional local AI agent.
Send an STL, 3MF, or ZIP from your phone. The toolkit analyzes the model, recommends print settings, slices it locally with OrcaSlicer, generates plate previews, verifies the loaded filament and toolhead, photographs the print bed, and asks for explicit approval before starting.

## What you can do
- Print to a Snapmaker U1 from your phone through Telegram.
- Slice STL and 3MF files with headless OrcaSlicer on Linux, WSL, or Windows.
- Preview model orientation, plate layout, toolpaths, and important print settings.
- Upload G-code through the U1's Moonraker/Klipper LAN interface.
- Verify toolheads and filament before heating or motion.
- Receive first-layer, final-layer, and completed-print camera photos.
- Require a fresh bed-camera image and human approval before every toolkit-started print.
- Run the deterministic CLI tools without AI, or add Hermes with a local LLM for the phone workflow.
The AI does not receive unrestricted printer control. Deterministic scripts own slicing, verification, uploading, monitoring, and the single-use print-start approval gate. See the [safety model](docs/SAFETY.md) for the exact boundary.
This is how AI should touch physical machines: **plan, explain, preview, ask, verify, then act only within a narrow approved boundary.**
## See it in action
A local model takes a ZIP of eight STLs from Telegram to a reviewed printing plate in about 160 seconds, entirely on local hardware. Nothing starts until a human approves the fresh bed photo.
https://github.com/user-attachments/assets/f3acea60-4ebe-4059-8158-92abd207f4ec
**Start here:** [print from your phone](docs/PRINT-FROM-PHONE.md) · [install the toolkit](#install) · [set up Telegram](docs/TELEGRAM-SETUP.md) · [run headless OrcaSlicer](docs/HEADLESS.md) · [troubleshoot](TROUBLESHOOTING.md)
---
## The flow
One flow handles everything. Send a single STL or a zip of twelve — **a lone
model is just a kit of one**, auto-detected, same entrypoint, same safety
boundary:
1. **Send a model.** A `.stl`/`.3mf`, or a zip of STLs (the common Printables
shape). The workflow ingests every part: footprints measured, oversized
parts flagged, hostile archives refused with a clean error instead of a
crash.
2. **Answer one decision form** — parts, print head, orientation, supports,
profile. On a tool-capable model it renders as **native buttons** (one
submit); on a small local model it falls back to a typed one-liner, or a
staged one-question-per-turn flow. The form surfaces live U1 state (which
filament is actually loaded on which head) and Orca's real mesh-topology
verdict (`floating cantilever` / `clean` / overhang fraction) so you pick
the pose Orca actually prefers. Either way **a script parses and validates
the answer; the model never interprets it** — only an opaque form id rides
through the model, and conflicting input fails loudly, never a silent guess.
3. **Arrange + slice** through OrcaSlicer onto as many plates as the bed
needs — T0→T<chosen> rewriting, Snapmaker thumbnail injection, real
Orca warnings surfaced, and a gcode-extent guard that refuses any plate
whose extrusion would leave the bed (built from a real incident, not a
hypothetical).
4. **Review two corroborating previews** derived from the *sliced gcode* (the
real toolpath): a precise top-down footprint and a 3D plate view of the same
per-part geometry with height added — same parts, same colors, same
positions the printer will execute. Plus a `review.md` flight plan generated
from the gcode's own config block: what will print, the ~12 settings that
matter, your decisions and overrides.
5. **Upload** every plate to the U1's Moonraker storage with `print=false`
(files land; the printer does NOT start).
6. **One bed-clear decision.** A fresh photo of the bed from the U1's onboard
camera arrives with the previews. Reply YES to start now, or NO to keep the
gcode uploaded without printing. **The YES is redeemed by the gateway
itself — the workflow hands the model no start command**, so a misbehaving
or prompt-injected agent has nothing to fire. The yes rides a single-use
token bound to the plan's revision + gcode hash and to your operator
identity in your private chat — if anything changed since you looked, or the
reply came from anyone else, the start refuses instead of printing stale
state.
7. **Gated start with a last exit.** The gate re-verifies material against
what's physically loaded, then opens a ~120s grace window before any command
reaches the printer: **tap the CANCEL button on the countdown, or reply
CANCEL** — both handled model-free by the gateway, so neither can be
swallowed by the agent's turn. Every outcome (started, cancelled, or
refused-with-reason) is messaged to you by the machinery, never narrated by
the model. Plate 1 is the only toolkit-started plate; plates 2..N start from
the Snapmaker app — the watchdog photographs every plate either way.
8. **Monitoring takes over** — first-layer photo, last-layer check, completion
(see [Always-on print monitoring](#always-on-print-monitoring--no-agent-required)).
Steps 1–5 are useful as CLI utilities even if you never touch an AI agent.
Steps 6–8 are where the operator gate makes the difference between "AI presses
print" and "AI safely shows you the print so you can press it."
### Also in the flow
The same entrypoint and the same bed-clear gate handle three more moves:
- **Reprint.** Say "reprint" (no file needed) and pick from your recent jobs.
The gcode already on the printer is reused — no re-slicing — the original
previews and review doc come back with a fresh bed photo, and the normal
gate runs with the same drift and identity checks.
- **Quantity.** On a single part, ask for 1–9 copies; they pack onto the plate
through the same arranger and split across plates when the bed fills.
- **Advanced settings.** An optional screen off the review card exposes infill
density and pattern, wall count, brim, fuzzy skin, and tree-vs-grid supports
— each defaulting to the profile's own value, each verified into the sliced
gcode.
## What This Is Not
- **It is not an autonomous printer driver.** No agent in this stack can start a print without an explicit operator yes/no. Normal starts are approved against a fresh U1 camera photo captured in-the-moment; if the camera is unavailable, the only alternative is an explicit, audited manual bed-verification path (never a silent skip).
- **It is not a generic slicer wrapper.** Specific profile resolution, T0→T<n> rewriting, Snapmaker thumbnail injection, and Moonraker storage discipline are baked in for the U1.
- **It is not a multi-printer abstraction yet.** The safety model and event contract are portable in principle. The implementation is U1-specific by design until the U1 experience is solid.
- **It is not a Hermes-only project.** Hermes is the convenient remote-control layer. Every workflow step has a CLI form and JSON event stream — wrap it with whatever you want.
## Safety Model
Hermes — and any other AI agent layered on top — can recommend, explain, and prepare a print, but the U1 toolkit owns the final safety checks and will not perform printer-affecting actions without an explicit operator approval tied to a specific request ID.
**The model is never given a way to start a print.** The workflow hands the agent no start command at all — when you reply YES at the bed-clear prompt, the gateway itself redeems it and runs the start gate; the agent runs nothing. That YES is single-use and bound to your operator identity in your private chat, so a wrong sender, a wrong conversation, or a stale plan all refuse. The agent's only unattended power over the printer is in the safe direction: it can help you *cancel* a pending start, never begin one. This boundary was not designed on paper — it was forced by a live incident where an earlier build let the model fire a start it had been handed, then hardened across several rounds of adversarial review. In the default single-host deployment a deliberately hostile agent is *contained* (every start is audited, gated, and cancellable) rather than cryptographically *prevented*; the separate-user boundary that would make it prevention is on the roadmap. The full boundary, and that honest limit, are documented in [docs/SAFETY.md](docs/SAFETY.md).
The default lifecycle:
```text
read state → slice → preview → upload-only → operator approval → start → monitor
```
Actions that always require explicit operator confirmation:
- Starting a print
- Resuming or canceling a print
- Heating nozzle or bed
- Moving axes
- Clearing alarms
- Changing tool state
- Anything that affects the physical printer
The workflow fails closed. If a check is unsure, it stops and asks rather than guessing. Bed-clear verdicts come from the operator looking at a real photo, not from the toolkit deciding the bed is "probably fine." Slicer profile mismatches abort BEFORE the slice. Upload that hits a filename collision asks before overwriting.
### What the operator approves
Every operator decision is concrete and tied to a specific artifact:
| Decision | What the operator sees |
|---|---|
| Parts / orientation | Parts thumbnail grid + Orca's mesh-topology verdict for the recommended pose |
| Tool / filament | Live U1 toolhead state ("T0: Generic white PETG (loaded)") |
| Preset | Recommended profile based on model class + your print history |
| Supports | Overhang verdict from a fast draft slice — Orca's real call, not face-angle |
| Pre-print review | A `review.md` flight plan generated from the sliced gcode's own config block — bound to the plan's revision + hash so what you read is what prints |
| **Bed clear** | A **real, fresh photo** of the bed from the U1's onboard camera. One yes/no. Default is no. |
If anything is unknown — printer state, tool, material, slicer metadata, bed visibility — the workflow stops and asks. No silent assumptions.
What the exchange actually looks like after the form is submitted:
> **Bot:** *(plate preview, 3D view, review.md, and a fresh bed photo arrive)*
> Sliced plate, review doc, and a fresh bed photo are attached. Bed clear and ready to print? Reply YES to start now, or NO to keep the gcode uploaded without printing.
>
> **You:** yes
>
> **Bot:** ⚠️ Snapmaker U1 print starting in 120s. Reply **CANCEL** to abort. Ignore this to let the print start.
If a re-slice or plan change happened between the photo and the "yes," the start refuses and re-asks with the new revision instead of starting on stale state.
The full per-action breakdown (the test-operator fence, the grace-period
cancel chain, and every allowed-vs-gated command) lives in
[docs/SAFETY.md](docs/SAFETY.md).
None of this is aspirational: the full test suite runs in CI on every change
(the deployed runtime is Python 3.13), and the cancel chain is
**live-verified on real hardware**, including a reproducible, no-printer-needed
drill anyone can run: [docs/verify-cancel-hook.md](docs/verify-cancel-hook.md).
## Always-on print monitoring — no agent required
The start decision needs an LLM + your explicit approval. **Watching the print once it's running does not** — that part is three quiet cron jobs, no agent turn, no LLM in the loop at all.
**Why first-layer specifically matters:** it's the earliest real tell a print is about to fail — bed adhesion problems, warping, a shifted part, wrong Z-offset all show up in the first few layers, long before you'd otherwise notice. A last-layer photo confirms it finished; a first-layer photo is the one that could actually save you hours of wasted filament and time if you catch it early.
| Job | Cadence | What it does |
|---|---|---|
| `u1_last_layer_watch.py` | every 1 min | Snaps a photo at **first-layer** (layers 2–5, the bed-adhesion check), at **last-layer** (final ~6 layers), and after a **pause/resume** (an extra confidence check). Delivers each straight to Telegram. Auto-dims the cavity LED a few minutes after the job completes/errors/cancels. |
| `u1_print_watchdog.py` | every 5 min | Silent health poll across *any* active U1 print, not just Hermes-started ones. Alerts once per distinct issue, with cooldown so it never spams. |
| `u1_print_history.py` | every 5 min | Appends to a durable print ledger. |
All three run in Hermes' `no_agent` cron mode — a plain script invocation with no persona and no model call, so there's nothing for a weak or a strong model to get wrong, and nothing that can fabricate a milestone that didn't happen (the photo either exists or the job says nothing). They watch *every* active print, whether it was started through this toolkit, the Snapmaker app, or anything else touching the same Moonraker.
## The Three Layers
The toolkit ships as three layers that build on each other. Pick your mode,
then install below.
### 1. CLI mode — useful without Hermes
Scriptable, deterministic, single-purpose tools that a U1 owner can use directly:
- Slice + preview a model
- Inspect printer state, profiles, print history
- Generate orientation renders
- Upload a job with `print=false`
- Review G-code metadata before printing
These are designed for shell scripts, cron jobs, manual workflows. No AI required.
### 2. Operator workflow — the staged experience
A multi-step state machine that walks an operator through the print decision. Emits structured JSON events at every step, so any frontend (Telegram bot, web UI, custom integration) can wrap it without re-implementing the logic.
This is the core product. It's what makes the toolkit feel like a responsible assistant instead of a generic API wrapper.
### 3. Hermes mode — the remote-control layer
A bundled Hermes skill (`3d-printer-slicing-automation`) that lets a Telegram-bridged Hermes agent drive the operator workflow on the user's behalf. The agent:
- Surfaces the workflow's questions and previews to the user verbatim
- Tool-calls the named scripts (never invents its own slicing path)
- Never decides bed-clear status on its own — the operator does, looking at a real photo
The skill is designed to work on small local models (`gemma4-26b-64k` and below) via [Ollama](https://ollama.com/). See [Hermes integration](#hermes-integration) for the full setup.
## Install
One install path. Everything below assumes a U1 reachable on your LAN and a
Linux (or WSL) host. Native Windows is experimental — see
[docs/WINDOWS.md](docs/WINDOWS.md) for what differs and how to validate it.
**Requirements:** Python 3.9+, `numpy` + `Pillow` (via `requirements.txt`), an
[OrcaSlicer 2.4.0+](https://github.com/OrcaSlicer/OrcaSlicer) CLI binary
(extracted AppImage is fine — full steps in
[Headless slicing setup](#headless-slicing-setup-no-gui--scripted)), and network
reachability to your U1's Moonraker port (default `7125`).
```bash
git clone https://github.com/bbolinger/snapmaker-u1-toolkit.git
cd snapmaker-u1-toolkit
python3 -m pip install -r requirements.txt
# Point the toolkit at your printer (.env is auto-loaded on first config read)
cp .env.example .env # edit: set SNAPMAKER_U1_HOST to your U1's LAN IP
# Fetch Snapmaker's stock U1 profiles (~217 files) + extract your own history
python3 tools/fetch_snapmaker_profiles.py
python3 tools/extract_profiles_from_printer.py # optional but recommended
# Verify (argparse usage text = your environment is ready)
python3 scripts/u1_slice_workflow.py --help
# Read-only status probe (no risk)
python3 scripts/snapmaker_u1_status.py
```
The networked steps (`extract_profiles_from_printer.py`, `snapmaker_u1_status.py`)
time out until `SNAPMAKER_U1_HOST` points at your printer's real LAN IP. The
`.env.example` default `192.168.1.100` is only a placeholder, so edit `.env`
first, or skip those two until the printer is reachable.
On Windows (PowerShell) the same steps apply with `Copy-Item .env.example .env`
and backslash paths; the data dir defaults to
`C:\Users\<you>\.local\share\snapmaker-u1` (override with
`$env:SNAPMAKER_U1_DATA_DIR`).
**Choosing a Python interpreter.** The workflow needs `numpy` and `Pillow` on
the Python that runs it and auto-detects a working interpreter (first that can
`import numpy, PIL` wins): `$U1_TOOLKIT_PYTHON`, then
`/opt/hermes/.venv/bin/python`, then a project-local `venv`/`.venv`, then the
Homebrew paths. If none has the deps, it exits listing every path it tried and
how to fix it. Cleanest isolated setup:
```bash
python3 -m venv venv
venv/bin/pip install -r requirements.txt
export U1_TOOLKIT_PYTHON=$PWD/venv/bin/python # add to your shell rc to persist
```
Connection, data-dir, and LED behavior are covered in
[Configuration](#configuration). If something fails, check
[TROUBLESHOOTING.md](TROUBLESHOOTING.md).
## First slice

The canonical entry point for a model or a kit zip (kit-of-one auto-detected):
```bash
python3 scripts/u1_slice_workflow.py model.3mf
```
Agent/Telegram wrappers should consume the event stream instead of
re-implementing the workflow:
```bash
python3 scripts/u1_slice_workflow.py model.3mf --json-events
```
Safe headless proof run — decisions passed as flags, upload-only, no printer
start (list your profile slugs with `python3 scripts/u1_profile_picker.py`):
```bash
python3 scripts/u1_slice_workflow.py model.3mf \
--tool T1 --material PETG --orient auto \
--profile 0_20_strength_snapmaker_u1_0_4_nozzle \
--supports auto --upload-only --yes
```
Without profiles the workflow exits with a `setup_required` event and points
you back at the fetch/extract tools from [Install](#install).
For the design rationale, architecture, and acceptance criteria, see
[`docs/DESIGN-CONTRACT.md`](docs/DESIGN-CONTRACT.md). For the public event
contract (every event the workflow + audit log emit, with payload shapes), see
[`docs/events.md`](docs/events.md).
## Hermes integration
**Install on Linux.** The deploy scripts are bash and `install.py` targets a
Linux venv (`bin/`, `lib/pythonX.Y/`). Run it inside the Hermes container, on a
Linux host, or under WSL / Git Bash on Windows — not raw Windows `cmd`. Hermes
typically ships `numpy` + `Pillow` in its bundled venv (verify:
`/opt/hermes/.venv/bin/python -c 'import numpy, PIL; print("ok")'`).
Run each step and let it finish before the next. Step 1 asks a `y/N` you have to
answer, so do not paste the whole block at once.
**1. Install the bundled skill** (answer `y` at the prompt):
```bash
hermes skills install bbolinger/snapmaker-u1-toolkit/skills/3d-printer-slicing-automation
```
**2. Deploy the workflow scripts** to the runtime paths the skill calls into:
```bash
bash deploy_to_runtime.sh
```
**3. Install both Hermes plugins and patch the gateway.** This installs the
`u1-form` tool plugin and the `snapmaker_u1` hook plugin (auto-skill load,
next-action guard, and the image / review-doc attachment injector). Add
`--venv <path>` if your Hermes venv is not `/opt/hermes/.venv`:
```bash
python3 adapters/hermes/install.py
```
`install.py` loads two plugins: `u1-form` and `snapmaker_u1` (a pip entry point,
installed editable so `git pull` updates it). Without this step the form, the
auto-skill trigger, and the image/review-doc attachments do not load. Watch for
its `[4/6] install the snapmaker_u1 hook plugin` and a `[6/6]` verify ending
`OK: hooks=...transform_llm_output`.
**4. Install both gateway hooks** (the operator YES that starts a print and the
reply/tap CANCEL that stops one), restart the gateway, and verify:
```bash
bash tools/install_hermes_u1_hooks.sh
hermes gateway restart
bash tools/install_hermes_u1_hooks.sh --verify
```
If you run these from inside a Hermes Desktop or gateway chat, run `hermes
gateway restart` from a **separate** terminal outside Hermes. The gateway
refuses to restart itself from within its own process (it would kill the command
mid-run), so the hooks stay unloaded until you restart it externally.
The YES/CANCEL hooks bind to one operator in one private Telegram DM. With a
single user id in `TELEGRAM_ALLOWED_USERS` the binding resolves itself;
otherwise set it explicitly in the runtime `.env`:
```bash
U1_OPERATOR_BINDING=telegram:<your-numeric-telegram-user-id>
```
Without the hooks installed, a YES at the bed-clear prompt does nothing
(fail-safe: the printer never starts) — `--verify` and the deploy script
both tell you loudly.
The deploy script verifies the deployed workflow actually starts (`✓ workflow
starts cleanly`); override target paths via `U1_DEPLOY_SCRIPTS` /
`U1_DEPLOY_TOOLS` / `U1_DEPLOY_SKILL` / `U1_DEPLOY_PROFILES` if your layout
differs from the Hermes default.

The skill tells Hermes to call `scripts/u1_slice_workflow.py`, follow the
workflow's events, default to upload-only, and fail closed at the bed-clear
start gate.
### Local model & serving requirements (form mode / button UX)
The button-based **form mode** (the rich Telegram UX in the demo) asks the local
model to emit one tool call. Small local models are inconsistent at tool-calling,
so form mode has hard serving requirements — verified end-to-end on
`gemma4-26b-64k` via [Ollama](https://ollama.com/) on an NVIDIA Quadro P6000:
- **Ollama 0.31.1 or newer.** Ollama 0.30.x has a gemma4 tool-call parser bug
([#15539](https://github.com/ollama/ollama/issues/15539),
[#15798](https://github.com/ollama/ollama/issues/15798),
[#15943](https://github.com/ollama/ollama/issues/15943)): the model's tool
call leaks into the message *content* as raw template tokens (`<|channel|>`,
`<|"|>`), the parser misses it, `finish_reason` is `stop`, and the agent
stalls with no buttons.
- **Run the model at low temperature (~0.2) for tool turns.** Gemma's default
Modelfile ships `temperature 1`, which is unreliable for tool calls (~2 of 3
succeed in testing — one run in three strands the operator). A temp-0.2 variant
is 3 of 3. Create one (it shares the same weights blob — no extra disk):
```bash
printf 'FROM gemma4-26b-64k:latest\nPARAMETER temperature 0.2\nPARAMETER num_ctx 65536\n' \
| ollama create gemma4-26b-64k-tool -f -
```
then point your agent's model at `gemma4-26b-64k-tool`.
- **The toolkit already does its part.** The `kit_form` event carries only a short
`form_id`; the full form definition is persisted to disk and loaded by the form
plugin, so the model never has to reproduce a large nested schema in its tool
call (what small models fail at). Nothing to configure — just don't downgrade
the bundled `u1-form` plugin.
All three are needed together: on 0.30.8 the model failed even with the flat
`form_id` call; on 0.31.1 the flat call works, but only at low temperature is it
reliable. If tool calls still fail, fall back to **text mode**
(`--interaction-mode text`) — the staged one-question-per-turn flow uses only
simple `terminal` calls that even small models handle reliably.
> **Hardware note (Pascal / older GPUs).** Ollama 0.31's `cuda_v13` runtime
> dropped Pascal (compute capability 6.1); it falls back to the bundled
> `cuda_v12`, so a P6000 / GTX-10-series still works today — but a future Ollama
> that drops `cuda_v12` would break it. Benign `driverInitFileInfo ... result=11`
> lines at startup are that fallback, not a failure.
### Gotcha for skill writers: Hermes attaches files via bare paths in text, not a tool parameter
If you fork this skill or write your own, Hermes' platform gateways (Telegram, Discord, Signal, etc.) deliver media to the user by scanning the agent's reply text for **bare absolute file paths** ending in known media extensions and auto-attaching whatever exists on disk. There is **no** `files=[...]` tool parameter the agent needs to call. See `gateway/platforms/base.py:extract_local_files()` in Hermes 0.15.2 for the canonical implementation.
What this means for your skill prompt:
- ✅ Tell the agent: *"emit the absolute path bare in your reply text"*
- ❌ Do NOT tell the agent: *"attach the file via the reply tool's files parameter"*
- ❌ Paths inside backticks or fenced code blocks are skipped — the agent must emit them as bare text
This caught me out during the first live test — the agent kept claiming it would "attach" renders but the gateway saw nothing to extract. See `TROUBLESHOOTING.md` for the full diagnosis if you hit the same.
## What's in here
| Script | What it does |
|---|---|
| `u1_config.py` | Centralized host/port resolution (env > JSON > default) |
| `u1_camera.py` | Camera capture via Snapmaker-specific websocket `camera.start_monitor`; auto-on/restore the cavity LED for each capture via `u1_led.photo_wrap` |
| `u1_led.py` | Cavity LED helper — CLI (`status / on / off / set --r/g/b/w / is-on`) and `photo_wrap()` context manager. The U1's cavity LED is white-only (`white_pin: PA10` in printer.cfg); the 4-channel API matches Klipper's interface, only WHITE has visible effect |
| `u1_toolmap.py` | Multi-tool material gate — declared vs detected material check |
| `u1_preflight.py` | Combined Moonraker state + camera freshness packet for "is it safe to start?" |
| `u1_upload_gcode.py` | Upload-only (`print_started=false`) with gates: idle state + tool/material match |
| `u1_slice_workflow.py` | Canonical end-to-end STL/3MF entry point: orient → render → slice → preview → upload-only/start gate |
| `u1_kit_workflow.py` | The unified kit workflow behind it — ingest, one decision form, arrange, slice, previews, bed-clear gate |
| `u1_last_layer_watch.py` | Watch active print for first-layer (2–5) and "last ~6 layers" milestones, snap photos; also auto-dims the cavity LED 5 minutes after `complete`/`error`/`cancelled` (`U1_LED_OFF_DELAY_SEC` overrides) |
| `u1_print_watchdog.py` | Quiet cron-driven health watcher with per-issue cooldown to avoid notification spam |
| `u1_print_history.py` | Append-only JSONL print ledger + canonical upserted JSON |
| `snapmaker_u1_status.py` | Read-only status probe |
| `snapmaker_u1_snapshot.py` | Websocket camera trigger helper |
| `tools/extract_profile_from_gcode.py` | One-shot extractor — turn a successful G-code into Snapmaker Orca process + filament JSONs |
| `tools/extract_profiles_from_printer.py` | Auto-pull recent G-codes off your U1 over Moonraker, run the extractor against each — one command, gets your real print history into `profiles/from-printer/` |
| `tools/fetch_snapmaker_profiles.py` | Fetch Snapmaker's official U1 stock profiles (machine + process + filament) from the upstream `Snapmaker/OrcaSlicer` GitHub repo into `profiles/snapmaker-stock/` |
| `tools/gcode_inject_thumbnail.py` | Add Snapmaker-app preview thumbnails to headless-sliced G-code (PIL renderer + base64 splice) |
| `tools/render_stl_orientation.py` | Pre-print orientation review — 4-view PNG (isometric, front, side, top) with overhang faces highlighted in orange |
## Configuration
`u1_config.py` resolves two things — the **connection** to the printer, and
the **data dir** where runtime state lives (configs, photos, ledgers).
### Connection (host/port)
1. **Environment variables**: `SNAPMAKER_U1_HOST`, `SNAPMAKER_U1_PORT`
2. **JSON file**: location from `SNAPMAKER_U1_CONFIG` env, default `<data-dir>/u1_config.json` (contains `{"host": "...", "port": 7125}`)
3. **Hardcoded default**: port 7125 only — host is required
### Data dir (where runtime artifacts live)
1. **`SNAPMAKER_U1_DATA_DIR`** env var (explicit override)
2. **`/opt/data/snapmaker_u1`** if it exists (auto-detects Hermes-style installs — for the agent setup these scripts came from)
3. **`~/.local/share/snapmaker-u1`** (community default, follows XDG Base Dir)
All host/port/data-dir lookups happen on first call — `import u1_toolmap` (or
any other script) never touches disk for config. The lookup only fails when
you actually run a command without any configuration.
See `.env.example` for a starting template.
### Cavity LED auto-control
The U1's `cavity_led` is white-only — Snapmaker's shipped `printer.cfg`
defines it as `[led cavity_led] / white_pin: PA10`, no R/G/B. Klipper's
`[led]` interface exposes all four channels regardless, but only the W
channel is physically wired. The toolkit drives the LED in two places
so the operator doesn't have to think about it:
- **Every camera capture** (`u1_camera.py photo`, and therefore every
milestone photo from `u1_last_layer_watch.py`) is wrapped in a
`u1_led.photo_wrap()` context manager:
- LED already on → no change, no flicker.
- LED off → turn on white (W=1), settle ~300 ms for the camera's
auto-exposure, capture, then restore the LED to off.
- **5 minutes after a print finishes** (`print_state` enters `complete`,
`error`, or `cancelled`) the LED is turned off, once per print. If you
manually turn it back on, it stays on — the watcher dedups by
`job_key = filename|total_layer` and won't re-fire for the same print.
**Tuning / disabling:**
- `U1_LED_OFF_DELAY_SEC=N` env var — grace window before auto-off. Default
`300`. Set `0` for immediate. Set a large value (e.g. `86400`) to
effectively disable the auto-off without removing the wiring.
- The wiring is **fail-soft**: if `cavity_led` isn't configured on your
printer (or the LED endpoint errors), the LED helper logs to stderr and
the photo/watcher keeps doing its primary job.
- Manual control via the CLI: `u1_led.py status / on / off / set --r --g --b --w`.
**Why:** photos taken at first/last-layer/post-resume milestones need
the LED on to be useful, but leaving the cavity bright forever after a
finished print is wasteful and surprising. The 5-minute grace gives you
time to inspect the bed before it goes dark.
## Profile sources
Your profiles should be *yours*: extracted from your own successful prints,
mapped to your own extruders. The toolkit ships with an empty `profiles/` dir on
purpose and gives you three honest sources: Snapmaker's stock profiles,
profiles extracted from prints that already succeeded on your printer, and
your own hand-tuned files. The full guide (first-run setup, supports
auto-detection, and building per-extruder profiles from print history) is in
[docs/PROFILES.md](docs/PROFILES.md).
## Headless slicing setup (no GUI / scripted)
Slicing from the CLI in a container, CI pipeline, or agent workflow, with no
OrcaSlicer GUI: which build to use, the 3-profile load order, a real
inheritance pitfall and its workaround, and the orientation-review and
thumbnail-injection tools. Full guide: [docs/HEADLESS.md](docs/HEADLESS.md).
## Optional: notify me when OrcaSlicer has an update
The toolkit ships a small checker that compares your installed `orca-slicer` version against the upstream latest release. **It does nothing unless you wire it into your scheduler.** Cloning the repo does not subscribe you to anything.
To enable, add one line to cron (Linux/macOS):
```
0 7 * * * /usr/bin/python3 /path/to/snapmaker-u1-toolkit/tools/check_for_updates.py
```
Behavior:
- **Silent when you're current.** No stdout → no cron email.
- **Single-line stdout when an update is available** — cron mails it via your usual cron-email setup. Example: `OrcaSlicer 2.4.1 available (you have 2.4.0). Patch (bug fixes, likely safe). Release notes: https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/v2.4.1`
- **Refuses to query GitHub more than once per 24h** regardless of how often you invoke it (cache at `~/.cache/snapmaker-u1-toolkit/update-check.json`). `--force` overrides for one-off "tell me now" runs.
- **Returns silently when GitHub is unreachable or the binary isn't present.** Never breaks your cron with stray stderr.
Compatibility note: Snapmaker upstreamed the U1 vendor profile into OrcaSlicer 2.4.0, and `tools/fetch_snapmaker_profiles.py` pulls fresh stock profiles from that upstream — patch/minor upgrades should keep slicing U1 prints. Major-version bumps may change CLI flags or profile schema — re-run the EGO trimmer regression after upgrading. The notifier's risk label ("patch / minor / major") flags this in the alert text.
If your `orca-slicer` binary lives anywhere other than `/opt/data/tools/orcaslicer/squashfs-root/bin/orca-slicer` (Hermes-container default), pass the path explicitly OR set the `ORCA_SLICER_BIN` environment variable in your crontab, otherwise the script silently can't probe your installed version and you'll never see notifications.
CLI:
```
python3 tools/check_for_updates.py # daily-cached check
python3 tools/check_for_updates.py --force # bypass cache, hit GitHub now
python3 tools/check_for_updates.py --orca-bin /path/to/orca-slicer # one-off
ORCA_SLICER_BIN=/path/to/orca-slicer python3 tools/check_for_updates.py # persistent env
```
## Reference docs
Real reverse-engineering notes from getting these scripts working — the kind of stuff Snapmaker doesn't document publicly:
| Doc | Topic |
|---|---|
| `references/snapmaker-u1-lan-probe-2026-06.md` | Open ports, working endpoints, API key handling |
| `references/snapmaker-u1-camera-websocket.md` | Snapmaker-specific `camera.start_monitor` websocket method |
| `references/snapmaker-u1-headless-slicing-and-toolhead-notes-2026-06.md` | OrcaSlicer CLI for headless slicing, tool naming gotchas |
| `references/snapmaker-u1-filament-detection-api-2026-06.md` | Filament presence/material detection objects |
| `references/snapmaker-u1-printhead-numbering-and-start-workflow-2026-06.md` | T0..T3 mapping to `extruder`..`extruder3` |
| `references/snapmaker-u1-last-layer-photo-watch-2026-06-21.md` | Last-layer event detection for milestone photos |
| `references/snapmaker-u1-toolmap-script-2026-06.md` | Material-gate design rationale |
| `references/snapmaker-u1-orca-moonraker.md` | OrcaSlicer + Moonraker integration |
| `references/snapmaker-u1-research.md` | First-pass research summary |
## Running the tests
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install pytest
pip install Pillow numpy # only needed for the thumbnail-injector tests
pytest -v
```
The suite (CI runs it on every pull request; the deployed runtime is Python
3.13) covers: config resolution (incl. 3-tier data-dir, `.env`
auto-loader with quoted/commented/walk-up edge cases, import-without-config
regression lock, and a smoke-runner that exercises every script's `main()`
to catch leftover undefined refs), material gate (incl. fail-closed on
corrupt map), upload pre-checks, G-code metadata parsing, print-history
ledger (incl. atomic-write contract + tmpfile cleanup on failure), profile
extraction (incl. multi-tool slice handling — `PETG;PETG;PLA;PLA` →
right value for the actual tool), thumbnail injection, upload-time
thumbnail wiring, status-probe `safe_to_upload` parity with the actual
upload gate, preflight `--host` override correctness, STL parsing + view
rotations + overhang detection + 4-view orientation sheet rendering,
bundled machine-profile completeness (standalone, klipper gcode flavor,
4 extruders, required slicing fields), printer-side profile extraction
(Moonraker list + download mocked, friendly errors).
Tests use mocked Moonraker responses — no real printer required. The
thumbnail-injection tests `importorskip` PIL/numpy, so they're harmless
to omit if you only want to run the safety-script tests.
## Release validation
Each tagged release is validated before publish: the full test suite in CI (on
the deployed Python 3.13 runtime), a fresh-clone install and script-help smoke test, the
active-print upload-gate safety check against a mocked Moonraker, and, for
releases that touch the safety boundary or the operator flow, a live run on a
real U1. Per-release evidence lives in the [CHANGELOG](CHANGELOG.md).
## Known limitations / design notes
1. **Single-printer scope**: scripts assume one U1. Multi-printer would need namespacing in the config + per-printer state dirs.
2. **Cron / always-on cadence**: the watchdog, last-layer, and history scripts are written to be cron-driven (the table above shows the defaults; the intervals are yours to set). They keep state on disk and are idempotent across runs, but they're not daemonized; your scheduler (cron, systemd timer, Hermes' cron, etc.) owns the cadence.
3. **U1 firmware coupling**: tested against Snapmaker U1 firmware on the version that ships Moonraker on port 7125. Other Snapmaker models, or future firmware revisions that change the `print_task_config` / `filament_detect` object shape, may surface field gaps. The `references/` docs capture what the current firmware does emit — start there if you're debugging a field-shape mismatch.
## License
MIT — see `LICENSE`.
## Contributing
PRs welcome, especially:
- Additional reference docs as new firmware behaviors are reverse-engineered
- Material gate enhancements (multi-tool prints, prime-tower extruder assignment)
- Multi-printer support (namespacing config + per-printer state dirs)
Please run `pytest` before submitting — all tests should pass. See
[CONTRIBUTING.md](CONTRIBUTING.md) for setup, conventions, and the
safety-model rules that PRs need to respect.
## Acknowledgments
These scripts were developed and validated against a real Snapmaker U1 over weeks of daily use. Real prints span single models and multi-part kits, generic and brand-specific PETG plus PLA, supports and no-supports, and run times from ~25 minutes to ~12 hours. The safety gate has both blocked a real material mismatch before any heat and carried full prints to completion, driven live over Telegram by a local model.
**Philosophy**: profiles should be YOUR profiles, extracted from YOUR successful prints, mapped to YOUR extruders. The included `profiles/` directory shows the *shape* of those files but is environment-specific. The toolmap gate enforces per-extruder material assignment so wrong-slot mistakes don't waste filament.
Bug reports and PRs from other U1 owners welcome — especially the profile-extraction methodology being tried on different setups (smooth PEI, glass beds, other PETG brands, multi-tool configurations).
Information
Repository
Language
Python
Created
2026/9/5
Updated
2026/9/5