Repository navigation
feat(printer): macro passthrough and per-model alias commands - #721
Merged
Merged
Conversation
…settings and in 3MF plate pictures (Refs #722) The Orca CLI writes no thumbnails. Every code path that runs a slicer now asks it for 48x48/96x96/300x300 PNG (project/machine preset, PrusaSlicer flags) and draws what is missing from the toolpaths before the G-code is hashed or moved: 3d slice, orca_cli (3d prep, calibration), 3d retarget --gcode, 3d gcode-resume. A G-code without a thumbnails spec requires the three default sizes. A 3MF gets its missing Metadata/plate_N.png. tests/test_thumbnail_guard.py fails a module that runs a slicer without the layer. The legacy over-limit functions of the touched files were split into helpers (behavior-preserving) so the whole-file lint gate passes (#707). Known gap: the U1 plate generator, #724.
…ht names the fix and has --fix (Refs #722) 3d printer upload and start add the thumbnails FILE lacks in place before the snapshot is hashed (atomic, mtime kept, the slice-warnings record follows the new SHA-256, old and new hash printed) instead of sending a G-code that preflight refuses. print-preflight's thumbnails FAIL names the exact 3d gcode-thumbnails add command for the original file; --fix runs it. End-to-end tests run bin/3d against a fake slicer and a fake Moonraker server (no real printer). The legacy over-limit functions of printer.py, print_preflight.py and printer_ops/preflight.py were split into helpers (behavior-preserving) so the whole-file lint gate passes (#707).
…dict that survives light and registration drift The idle U1 at Z 200 (15 % of the plate in view) read "objects on the bed 14.3 %" on an empty clean plate: the ROI there is mostly the machine's back wall and the window behind the printer, the window light had changed, a cable had moved and the picture sat a pixel or two off the October reference. - difference(): a pixel matching the reference within shift_px (3) is unchanged; changed regions are opened with an open_px (5) square. classify(): below low_view (0.5) of the bed in view only a region of occupied_blob_low_px (1200) or occupied_min says occupied, a smaller change is unknown and names `bed check` and the reference age. The U1 frames of parts stay 2700-3900 px regions, a 16 px part on the K1 SE stays occupied. - evidence keeps the untouched camera frame beside the annotated one (.raw.jpg), pruned together. - `3d printer bed check NAME [--expect empty|part] [--no-move] [--json]`: the idle bed is moved in Z only (the guarded move of bed_view) to where the camera sees the plate, a fresh frame is judged and the bed put back; result empty | objects | part-present | part-gone | out-of-view | unknown | busy, the changed fraction, annotated + raw frame, exit 1 unless a definite answer that matches --expect. - `bed reference --capture --best-view` takes the empty-bed reference from the best view (bed_view picker: need_reference=False). - complexity helpers extracted from classify, _run_monitor, capture_reference, main (bed_state.py) and _ack, clear_ack (printer_ops/bed.py) so the staged files pass the 15/20 lint gate; behaviour unchanged. - fixtures: the Oct-16 reference and the two false-positive frames rebuilt from the annotated evidence (overlay inverted, outline/caption inpainted); the old classifier reads both occupied. Refs #723 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t came off the plate; `start --watch` 2026-11-04: a part came off the plate without a trace and the print kept running. The always-on watchers are read-only by design; this is the guard an agent starts for ONE job right after `3d printer start` and that acts on it. - bed_part.py: pixel half. Changed share of the plate region against an empty-plate baseline taken at the start of the job (homography-warped to the current Z with a camera calibration), how much of the learnt footprint is still changed (overlap) and how much changed beyond it (outside). `blind`, never "gone", when the plate is out of view or the baseline cannot stand. - jobwatch.py: pure judge. Footprint armed from layer 3 on two readings; part_gone / part_moved / spaghetti with 2-of-4 confirmation; signals from the always-on MoonrakerDetector (runout, heater loss, bed off, exception, G-code error, homing lost) and the printer's own noodle verdicts (p >= 0.8); `--on-fail auto|pause|cancel` (auto: cancel a gone part or a camera finding in layers <= 3, else pause). - jobwatch_actions.py: save frames + finding.json, PAUSE/CANCEL of the bound job only (re-read, same file, once, verified), `tg call me --say` (russian), klippy lines via `3d printer evidence`, `tg --photo`. Every step runs even if an earlier one failed. No G-code, no axis, no resume, no start. - jobwatch_sources.py / jobwatch_run.py: live Moonraker + camera source, fixture scenario source with a virtual clock (`--fixture`, `--dry-run`, `--tg-dry-run`), checkpoints L1@25/50/100 % and 25/50/100 %, one watch per printer, `--yes` required (not for dry-run/fixture). Exit 0 / 3 lost print / 1. - jobwatch_launch.py + `printer start --watch` (opt-in: it holds cancel authority) or a NOT WATCHED line with the command and the estimated duration. - printer.py: run() is a dispatch table, _list_row/_cmd_list/_start_snapshot split into helpers so the staged file passes the 15/20 lint gate; behaviour unchanged. - docs/commands/printer.md: Job watch, bed check; lib-map entry. - tests: fixture frames (empty, part, dark room, partial, spaghetti) and a fake Moonraker: a detached part cancels + calls + saves evidence; a lighting change on an empty plate or with the part standing does not. Refs #723 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ation (chained baseline) The job watch of a `watch.bed.roi` printer (K1 SE) judged the part only within z_tol (2 mm) of the baseline's Z, so the camera went blind after ~10 layers. bed_part.py now follows the plate instead: every healthy reading saves the affine transform that carries the empty first-layer baseline onto that frame (--save-track, next to the footprint); the next reading starts from it (--track) and refines it with ECC on the plate pixels only, without the part's footprint neighbourhood. Layer to layer the bed moves ~1 px, so the chain never jumps, and every step is anchored to the true empty baseline (no accumulating error). The footprint is carried along, the baseline is warped onto the frame and the compared region is the plate as it moved. A vanished part reads as overlap ~0 whatever the Z. Beyond z_tol a plate that cannot be followed (no texture, correlation < 0.6, a jump > 12 px since the last healthy reading) stays `blind`, never `gone`. Tests (frames of the real K1 SE bed with a synthetic shift+shrink drift): a healthy part over 30 layers/6 mm stays present, a detached part at layer 30 cancels, a lighting change at layer 30 is no finding, a camera jump beyond what can be followed is blind (empty or with the part), the pixel tool chains small steps to a 20 px drift that no single step could follow. Documented in the Job watch section, bed_part.py's docstring and the lib map. Refs #723 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… `watch.bed.light` config
A frame of a dark chamber is not comparable to the reference. printer_ops/bed_light.py finds the
light among Moonraker's objects (`led` / `neopixel` / `dotstar` via SET_LED, `output_pin` via SET_PIN,
by name: light, lamp, led, chamber), reads its state with a read-only objects/query, and a
LightSession switches it on when it is off and puts it back (also after a failure). It never touches
the light while a job prints or is paused (except `printing_ok`, the job watch's frame, and then only
when the light was off), never on a read-only printer, and a missing light, an unreadable object list
or a failed switch only say so in the LightReport.
`watch.bed.light: {object, on_gcode, off_gcode, settle_s, enabled}` is validated in printer_ops/bed.py;
a gcode macro must be SET_LED / SET_PIN or listed in trusted_macros, like clear_ack.
Refs #723
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… job watch is taken with the light on `bed check` / `bed status` (status_view) switch the printer's light on before the bed moves (the settle overlaps the Z travel; with --max-age only when a fresh frame is really taken) and restore it after the bed is back: light on, bed move, capture, bed restore, light restore. `bed reference --capture` (best view or direct) does the same; a saved --from frame never touches the light. The verdict gains a `light` key (found / object / was_on / switched / restored / on_for_frame / error / text) and the check / status / reference text a `light:` line; a printer without a light says `no light found` and is judged as before. The `watch` monitor only reads idle frames and never touches the light. LiveSource.grab (`3d printer watch NAME --job`) takes the frame with the light on when it was off and restores it right after: the one moment the light is touched during a print. bed_state also marks a verdict `occupied_confident` (a region of the normal occupied size with at least watch.bed.auto_view_min of the plate in view), which the start gate uses in the next commit. Refs #723 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y on a confident `occupied` `3d print-preflight --printer` / `3d printer start` on an idle printer now run the whole `bed check` path (`bed_state.py status --best-view`: best view, the printer's light on, everything put back) instead of the watcher's verdict at whatever Z the bed rests at; `--no-bed-move` still judges at the current Z, light on. FAIL only on a missing plate or a confident occupied (`occupied_confident`: a region of the normal occupied size with at least watch.bed.auto_view_min of the plate in view at the Z judged). `unknown`, and an occupied seen at a poor view (the U1 at Z 200 sees 15 % of the plate; the former misfire, 16.0 % changed by a window light), are a WARN with the reason and the exact `3d printer bed check NAME` to run. The line also shows what the light did. Tests: the two light_* fixtures at Z 200.2 -> WARN; the pre-#723 classifier's occupied at that view -> WARN; parts in a 89 % view -> FAIL; the demo sequence light on, bed move, capture, bed restore, light restore; e2e through bin/3d with a fake chamber light. docs/commands/printer.md (The light, Start gate, config, bed check, job watch) and the lib-map follow. preflight.py: read_gcode_meta, check_slots, check_moves and check_head_sequence are split into helpers (behaviour-preserving) so the file passes the complexity gate it is linted with whole. Refs #723 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t is never `occupied` When the brightness of the bed ROI differs from the reference's (dark/middle/bright quantiles outside x0.8..x1.25: day vs night, light on/off, a gamma), a pixel needs the colour difference AND a change of the local texture (luminance over its neighbourhood spread, lib/bed_norm.py). Pixels only the colour difference flags are the light: occupied by colour alone becomes unknown, never occupied; a real part changes both and stays occupied. At a low view the texture channel rescues an occupied but does not call clear. `thresholds.light_norm: 0` turns it off per printer. Proved on the fixtures with generated gain 0.25..4 / gamma 0.5..2 / colour-cast changes of the K1 and U1 empty frames, the window-light frames at Z 200.2, and the real parts. Refs #723 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e file to the loaded colour (Refs #728) The slot Tn check compared colours as strings: the beige PETG sliced as #E8D8B0 and shown by the U1 as #FEE5A5 FAILed and only --force got past it. The colour is now within --colour-delta-e (CIEDE2000, default 10) and the line shows the distance (this pair: 6.4); a material mismatch stays a FAIL. --fix (needs a live --printer) writes the printer's colour into the used tools' filament_colour/extruder_colour in the G-code and its project 3MF, keeps mode and mtime, moves the slice-warnings sidecar to the new hash, updates the manifest and prints old/new sha256. A head further than --fix-max-delta-e is refused (another filament).
… then point to the printer screen (Refs #728) tg-ctl answers `ask-forward not-posted … daemon declined: reason=send-failed` when its single sendMessage failed (2026-11-04 22:02/22:03: `getaddrinfo EAI_AGAIN api.telegram.org`, a resolver blip; plain `tg` worked a moment later). No card is posted then, so asking again is safe: the ask is repeated after 3, 10 and 30 s, each reported on stderr. Every other outcome stays final at once. When it still fails the refusal says how to start from the printer's own screen (the file is already stored) and which daemon log lines to read.
…ile and printer colour (Refs #728) `3d printer start NAME FILE --yes --fix-colour` rewrites FILE (and its project 3MF) to the colours the printer reports, prints old/new sha256, uploads the fixed file (its hash is part of the stored name) and then runs the preflight and starts: no --force, no Telegram. --colour-delta-e and --fix-max-delta-e are passed through. `3d printer slot set NAME T<n> --material M --color #RRGGBB [--brand B]` writes the head's filament record the other way round: it is `3d printers filament set NAME/T<n>` (U1: SET_PRINT_FILAMENT_CONFIG through /printer/gcode/script, read back), so it refuses a read-only/offline printer, a running job and official RFID filament, and has --dry-run. `slot show NAME` reads the heads. The printer command's legacy functions are split into helpers (the same decomposition as the open thumbnail PR) so the touched file passes the complexity gate.
…ot post (Refs #728) Through bin/3d with the hatch answering `ask-forward not-posted … reason=send-failed`: four requests, then a refusal that names the stored file and the printer screen; nothing started.
…d reference --refresh`, `watch.bed.auto_reference` A reference now records its plate type (`watch.bed.plate`), the printer's light state and its time. A frame is judged against the reference of its own light state first; another light state is a fallback marked `reference_light_mismatch` and judged by colour and texture. A reference older than `watch.bed.reference_max_age_days` (30) or of another plate type never makes `occupied`: it is `unknown`, with the age and the refresh command. `3d printer bed reference NAME --refresh [--assume-empty WHY]` re-captures from the best view with the light on and replaces the reference of the same plate type, light state and view, only when the very frame judged is a confident clear in a good view with the light on (else it refuses and stores nothing), or on the owner's recorded word. `watch.bed.auto_reference: true` does it after a confident clear check on an idle printer and journals each refresh to NAME.refs.log. Refs #723 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… share per Z; `watch.bed.views`, `camera`, `plate_mask`
For a printer without a camera calibration (the K1 SE) the check counted the plate as in the frame
at every Z. `bed calibrate` (idle, homed, writable printer only; refused otherwise, nothing moves)
moves the bed through a Z grid in Z only — the guarded move of `bed check` within its clearance
floor and the axis ceiling, with the light on, the bed restored in a `finally` (new
`bed_view.excursion`: lock, displaced-Z marker, SIGTERM) — takes a frame per Z, follows the plate
from its outline in the empty-bed reference / `watch.bed.plate_mask` frame to frame (chained ECC
affine transforms; the largest dark region as a rough fallback) and stores the visible share per Z
and the plate polygon in NAME/calibration.json. A plate that cannot be followed ends the chain:
those Zs are listed, not stored. `bed_state.z_match` uses the shares for a printer with neither a
camera calibration nor a roi (linear between Zs, unknown beyond them).
Config: `watch.bed.views: {Z: share}` overrides the learnt ones, `camera: URL` names the bed
camera's snapshot, `plate_mask` is the alias of `roi`.
Refs #723
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ord that the bed is empty
`3d printer bed check NAME --assume-empty WHY [--assume-empty-ttl MIN]` runs the normal check and
records who ($USER), when, why, the camera's verdict, the frame paths, Z and light as
assume-empty-<ts>.json in the evidence dir and as NAME.assumed.json {ts, why, ttl, job_marker} in
the state dir. The preflight `bed` line is then PASS with "owner confirmed empty at <time>: <why>"
(the camera is not asked) for the ttl (default 60 min) while no job has started since (Moonraker's
newest history job id / print_stats.filename are compared). It never overrides a printing printer
or a camera that sees the plate missing; an empty WHY is refused. The preflight FAIL/WARN lines of
the bed name the command.
Refs #723
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…loss, first layer (Refs #733) print_quality.py measures a frame in the plate region: fleck_frac (white neutral specks and strands: min-channel top-hat) and order (structure-tensor coherence). printer_ops/quality.py judges them against the job's own first layers: notice/warning/severe per issue, persistence, one alert per issue and level, bounded baseline drift, debris trend, first-layer floor; never critical. quality_report.py writes the per-job monitoring report (counts per severity, worst frame). Calibrated on the 12 U1 frames of the 2026-11-05 SIMBOL sheet job and 4 phone photos (docs/research/print-quality-monitor.md); fixtures in tests/data/quality (640 px, < 1 MB).
…onitoring report (Refs #733) printer watch NAME --job now measures every camera check for stringing debris, a lost bead pattern and an uneven first layer (jobwatch_quality.py). A warning or severe level is one tg --photo with a short Russian text (what is seen, what to change) per issue and level; it never pauses, cancels or rings the owner. When the watch ends — job complete, lost print or --duration — DIR/<date>-NAME- monitor-report/ gets report.json, report.md and worst-frame.jpg. --no-quality and <printer>.watch.quality (false or any QualityConfig field) control it. Tests replay the real U1 frames through a fixture scenario.
vision_quality.QualityTier feeds each printing frame (plate region at the print's Z from the camera context) to the quality judge and returns Findings: quality_<issue>_warning/severe -> a WARNING event with the debris-marked frame (tg --photo via the dispatcher), once per issue and level, never a pause; notice -> info. --once reports the numbers and the absolute verdict of one frame. VisionCycle calls on_print_end so the per-print monitoring report is written when a print ends or another starts. --no-quality and watch.quality control it.
…a; scorer/once tests (Refs #733) A K1 SE view of a part reads order 0.5 normally, so the absolute first-layer floor (0.6) fired a false first_layer warning on the job-watch fixtures. It is now a per-model profile (Snapmaker U1, the camera it was calibrated on) or a watch.quality key; elsewhere the first layers are judged against the job's own baseline only. The part-check tests of test_printer_jobwatch.py run with watch.quality: false. Adds FrameScorer / --once report tests proving the quality Finding appears next to a spaghetti model that says p 0.0.
… U1 literal (Refs #733) test_fleet_vision_parity forbids U1 literals in generic modules: the calibrated first-layer floor moves to lib/data/vision/quality_profiles.json, matched on the printer's model name.
macros.py holds what may be sent to a Klipper printer: identifier-only names, one-token values, no M112/EMERGENCY_STOP, a risk class per command and the gates (--yes, idle printer) derived from it. MoonrakerAdapter gains macro_catalog() and the request strings dry-runs print. parse_status is split into _job_state/_temps (same behaviour) because the lint gate rejects its cognitive complexity in a touched file. Refs #718 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lib/data/printer-aliases.yaml maps intents (home, level, load, unload, preheat, cooldown, pause, resume, park, U1 clean-nozzle/purifier/...) to the G-code each firmware understands; aliases.py layers adapter -> model -> the printer's own printer-aliases.yaml (user, then project), types every argument and reports an alias a model lacks instead of guessing. Refs #718 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…el alias subcommands macro list discovers a Klipper printer's macros (Moonraker objects/list + gcode/help; --offline from registry data; --dry-run prints the requests). macro run passes ONE macro with typed KEY=VALUE parameters through the adapter and prints exactly the line sent: refused for read-only/offline printers, names/values with newline, ;, quotes or M112/EMERGENCY_STOP, commands the printer does not define; --yes for a macro outside trusted_macros and for dangerous ones; an idle printer for motion/heating, not for pause/resume/cancel style. home, level, load, unload, preheat, cooldown, pause, resume, park and the U1-only clean-nozzle/level-heated/idle-position/purifier resolve through the alias table per adapter and model, and an alias a model lacks says so. No raw `gcode` passthrough (documented). --help and docs/commands/printer.md updated. Refs #718 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t say so Snapmaker/u1-klipper lava/printer.cfg defines neither, so the generic Klipper aliases are marked unsupported for the U1 model instead of failing at run time. Refs #718 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…to helpers and a dispatch table Behaviour-preserving, so the whole-file lint gate (cognitive 15 / cyclomatic 20) passes without a rig.yaml ratchet (#707, option b). _list_row -> _fleet_facts/_device_facts, _cmd_list -> _print_no_printers/_print_list_row, _start_snapshot -> _require_stored_file/_verify_remote_identity/ _preflight_report/_require_preflight_pass/_start_and_report, run() -> _SUBCOMMANDS table. The helper bodies are the same text as feat/gcode-thumbnails-everywhere (#725) and feat/slot-colour-fix (#730) so the hunks merge cleanly with either. macro, aliases and the alias fallback join the table. Refs #718 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
alex-mextner
force-pushed
the
feature/printer-macros-aliases
branch
from
November 5, 2026 10:27
e5a1565 to
a1d0e98
Compare
…e print-preflight --fix adds thumbnails and aligns colours (Refs #728)
…CI's story check names (Refs #722)
…l; bed, watch --job and slot all stay registered (Refs #723)
…nto feature/printer-macros-aliases; macro, aliases, slot, watch, bed stay registered; 0.79.0 carries the batch bump (Refs #718)
…s; tree unchanged (Refs #718)
3 tasks done
…oon' on 2026-11-05; make them relative (Refs #718)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #718
What
3d printer -n NAME macro list [--offline|--dry-run] [--all] [--json]— discovers the macros a Klipper printer defines (Moonraker/printer/objects/list+/printer/gcode/help);--offlineuses registry data only.3d printer -n NAME macro run MACRO [KEY=VALUE ...] [--yes] [--dry-run] [--offline]— checked passthrough of ONE macro through/printer/gcode/script; prints the exact line sent.3d printer -n NAME aliases [--json]and thin alias subcommands:home,level,qgl,z-tilt,pause,resume,preheat,cooldown,load,unload, and U1-onlylevel-heated,clean-nozzle,park,idle-position,purifier. Tablelib/data/printer-aliases.yamllayered adapter -> model -> the printer's ownprinter-aliases.yaml(user, then project). An alias a model lacks says so and lists what it has.Safety (all enforced before anything is sent)
typed tokens (identifier names, one-token values: no newline,
;, quotes, braces);M112/EMERGENCY_STOPnever;access: fullonly (read-only/offline refuse, also on--dry-run); command must be defined on the printer;--yesfor macros outsidetrusted_macrosand dangerous ones; idle printer for motion/heating (pause/resume/cancel style exempt);creality-stockhas no G-code channel and refuses. No rawgcode SCRIPT(documented why in docs/commands/printer.md).Lint gate (owner decision for #707: refactor, no ratchet)
rig.yamlis untouched.lib/commands/printer.pyis refactored behaviour-preservingly so every function is within cognitive 15 / cyclomatic 20:_list_row,_cmd_list,_start_snapshotandrunare split into helpers and a_SUBCOMMANDSdispatch table. The helper bodies are the same text as PR feat: a G-code can never lack its thumbnails (slice, prep, retarget, upload, start, preflight --fix) #725 (feat/gcode-thumbnails-everywhere) and PR feat(printer): slot colour by ΔE, --fix/--fix-colour, printer slot set, retry for a start --force approval Telegram could not post (Refs #728) #730 (feat/slot-colour-fix), so those hunks merge identically.origin/main(a109482). Merge-tree check: conflicts with feat: a G-code can never lack its thumbnails (slice, prep, retarget, upload, start, preflight --fix) #725 are only inrun()and the_SUBCOMMANDStable region (mymacro/aliasesentries and alias fallback); with feat(printer): slot colour by ΔE, --fix/--fix-colour, printer slot set, retry for a start --force approval Telegram could not post (Refs #728) #730 they are those plus its_start_snapshotadditions andslot. Whoever merges second rebases.parse_statusinmoonraker.pywas split into_job_state/_temps(same behaviour) for the same gate.printer-aliases.yaml(user, then project), not inprinter-devices.yaml/fleet/printers.yaml: no change todevices.py/fleet.py.Tests
tests/test_printer_macros.py(70) andtests/e2e/test_printer_macros_workflows.py(4) against a loopback fake Moonraker built from the realu1/k1sefleet entries; no real address is reachable.