# Printer profiles and post-slice delivery Printer support is organised **vendor first, then printer**. A vendor profile holds what all of a vendor's printers share. Each printer profile holds only what is different about that printer, and inherits everything else from its vendor. The options offered after a slice (print, send the files to Discord, package for Bambu) come from these profiles. A Bambu printer offers the Bambu handoff; a Klipper printer never does. ## Layout ``` print_buddy/printers/ base.py VendorProfile, PrinterModel, PrinterAdapter, start_code_problems flashforge/ __init__.py VENDOR Flashforge: Moonraker, ZMod START_PRINT start code, OrcaSlicer, .gcode ad5x.py MODEL flashforge.ad5x: 220 mm bed, 4-slot IFS, 0.25 to 0.8 mm nozzles ad5m.py MODEL flashforge.ad5m: 220 mm bed, single extruder (no IFS), 0.25 to 0.8 mm nozzles ad5m_pro.py MODEL flashforge.ad5m_pro: the AD5M geometry; enclosure, camera and filtration creator5.py MODEL flashforge.creator5: 256 mm bed, 4 independent toolheads (tools, not IFS slots) creator5_pro.py MODEL flashforge.creator5_pro: the Creator 5 toolheads; enclosure, chamber heat and filtration flashforge_stock/ __init__.py VENDOR Flashforge stock: LAN HTTP on port 8898, slicer-owned start code, OrcaSlicer, .gcode ad5x.py MODEL flashforge_stock.ad5x: the AD5X bed, one tool, four IFS bays, no Moonraker. IFS occupancy is read from /detail and is not verified on a live printer. (adapter.py, client.py, guard.py) klipper/ __init__.py VENDOR Klipper: Moonraker, PRINT_START/PRINT_END macros, OrcaSlicer, .gcode generic.py MODEL klipper.generic sovol/ __init__.py VENDOR Sovol: the Klipper profile under the Sovol name sv06.py MODEL sovol.sv06 octoprint/ __init__.py VENDOR OctoPrint: REST to Marlin, slicer-owned start code, OrcaSlicer, .gcode marlin.py MODEL octoprint.marlin bambulab/ __init__.py VENDOR Bambu Lab: opt-in read-only LAN MQTT status, Bambu start code, Orca or Bambu Studio, .gcode.3mf handoff a1.py MODEL bambulab.a1: 256 mm bed, 0.4 mm nozzle, single filament, slicing offline (adapter.py, guard.py, facts.py, profiles.py; see BAMBU-LAB.md) p1s.py MODEL bambulab.p1s: 256 mm bed, 250 mm Orca height, 0.4 mm nozzle, single filament, slicing offline a1mini.py, p1p.py, x1.py, x1c.py, x1e.py, p2s.py, h2s.py, a2l.py MODEL #127 Stage 1: the remaining single-nozzle models, same shape as the P1S (a2l.py joined in #356, from OrcaSlicer v2.5.0-alpha's profiles) h2d.py, h2dpro.py, x2d.py, h2c.py MODEL The dual-nozzle models (#127 Stage 2, #308; h2c.py joined in #356) prusa/ __init__.py VENDOR Prusa: PrusaLink LAN, slicer-owned start code, OrcaSlicer, .gcode/.bgcode mk4s.py MODEL prusa.mk4s: 250 x 210 mm bed, 220 mm height, single tool coreone.py MODEL prusa.coreone: 250 x 220 mm bed, 270 mm height, enclosed xl.py MODEL prusa.xl: 360 mm cube, 5 toolheads (single-tool jobs only) miniplus.py MODEL prusa.miniplus: 180 mm cube, Orca MINI profiles (adapter.py, client.py, guard.py, facts.py, profiles.py; see PRUSA.md) elegoo/ __init__.py VENDOR Elegoo: Moonraker, template-inline start code, OrcaSlicer, .gcode neptune4.py MODEL elegoo.neptune4: 230 x 230 mm bed, 265 mm height neptune4pro.py MODEL elegoo.neptune4pro: 230 x 230 mm bed, 265 mm height neptune4plus.py MODEL elegoo.neptune4plus: 325 x 325 mm bed, 385 mm height neptune4max.py MODEL elegoo.neptune4max: 426 x 426 mm bed, 480 mm height (adapter.py, guard.py, facts.py, profiles.py; see ELEGOO.md) elegoo_sdcp/ __init__.py VENDOR Elegoo (SDCP): SDCP LAN, slicer-owned start code, OrcaSlicer, .gcode/.bgcode centauricarbon.py MODEL elegoo_sdcp.centauricarbon: 256 mm cube, enclosed CoreXY (adapter.py, client.py, guard.py, facts.py, profiles.py; see ELEGOO.md) templates/orca// machine.json, process.json, filament.json per printer; a model with generated_templates (the A1) ships none: /orca-profiles/ ``` The `[printer] model` key is `.`, which is the package name and the module name. Existing keys did not change. ### What lives where | Vendor profile (`VENDOR = VendorProfile(...)`) | Printer profile (`MODEL = PrinterModel(...)`) | |---|---| | `connection`: `moonraker`, `octoprint`, `offline`, `bambulab_lan` or `flashforge_stock` | `bed_mm`, `max_print_height_mm` (printable area) | | `start_code`: `zmod`, `klipper_macro`, `marlin`, `bambu` or `flashforge_stock` | printable-area exclusions and motion envelope (in its Orca `machine.json` and adapter) | | `slicers`: allowed `[printer] slicer` values, first is default | `supported_nozzle_sizes`, `supported_nozzle_types`, `nozzle_diameter_mm` | | `fallback_slicer`: one isolated attempt after an Orca failure | `ifs_slots` / AMS / AMS Lite, multi-color support | | `file_formats`: what the printer takes, primary first | `adapter` class, Orca `templates` folder, protected-settings `baseline` | | `delivery` and `default_delivery`: post-slice options | `label`, `printer.name` | | `settings`: shared `firmware`, macro names, layout margins, prime tower, cold pull | any vendor setting it needs to override | Settings merge in this order, later wins key by key: 1. vendor `settings` 2. printer `settings` 3. `config/home.yaml` 4. `config/local.yaml` (or `PRINT_BUDDY_CONFIG`) 5. environment variables The loaded settings also carry `printer.vendor`, `printer.slicer` and `delivery.methods`. `tests/printers/golden/model_defaults.json` and `model_settings.json` were captured before the vendor layer existed. `tests/printers/test_vendor_profiles.py` checks that every existing printer still produces exactly those settings. The only differences are the three keys above. ## Config Existing configs keep working unchanged: ```yaml printer: model: flashforge.ad5x # the full vendor.printer key, as before ``` These forms are also accepted, and are stored as the full key: ```yaml printer: vendor: sovol # "bambu" is accepted for "bambulab" model: sv06 # the printer part; on its own it works when only one vendor has that name slicer: orca # orca everywhere; bambu_studio only where the vendor allows it (Bambu Lab) delivery: methods: [printer, discord] # a subset of the vendor's delivery options; default is the vendor default ``` Combinations the vendor does not offer stop startup and name the setting. Examples are a vendor that disagrees with a full model key, `slicer: bambu_studio` on a Klipper printer, and `bambu_handoff` on a Flashforge printer. Every existing vendor defaults to `methods: [printer]`, so its Discord flow is exactly what it was. Add `discord` to get a "Send the files here" button after slicing. ## Adding a printer A new Sovol printer is one module plus its Orca templates. Everything not listed comes from the Sovol vendor profile: ```python # print_buddy/printers/sovol/sv08.py """Sovol SV08 on stock Klipper. Templates: templates/orca/sovol_sv08.""" from ..base import PrinterModel from ..klipper.adapter import KlipperAdapter from . import VENDOR MODEL = PrinterModel( key="sovol.sv08", label="Sovol SV08 (Klipper)", adapter=KlipperAdapter, templates="sovol_sv08", vendor=VENDOR, supported_nozzle_sizes=(0.4,), settings={"printer": {"name": "Sovol SV08", "bed_mm": [350.0, 350.0], "max_print_height_mm": 345.0, "nozzle_diameter_mm": 0.4, "ifs_slots": 1}}, ) ``` Then add `print_buddy/templates/orca/sovol_sv08/{machine,process,filament}.json` and select it with `printer: {model: sovol.sv08}`. Discovery registers the new module automatically. A module that leaves out `vendor=` inherits its package's `VENDOR`. A module that names another vendor is refused. A new vendor is a package with an `__init__.py` defining `VENDOR = VendorProfile(key="", ...)`. `VendorProfile` checks its own consistency. An offline vendor cannot offer `printer`, and only Bambu start code can offer `bambu_handoff`. ## After a slice The Discord flow is unchanged up to the slice: preview, colors, copies, then "Slice and print now (bed is clear)", "Slice only", "Stop with colors saved" and "Cancel print request". The slice summary still shows per-plate artifacts, estimates and validation. A vendor without `printer` delivery (Bambu Lab) does not get the "Slice and print now" button. After **Slice only**: - **Only `printer` enabled** (the default for every existing printer): the same question as before, with the Start button and Cancel. - **More options enabled**: one menu with the options the profile enables: - **Start next plate · bed is clear**: the same per-job bed-clear button, and still the only way to print. It runs every existing print-now gate. - **Send the files here** (`discord`): attaches each plate's file to the thread. - **Package for Bambu (no upload)** (`bambu_handoff`): builds a `.gcode.3mf` per plate, with the plate preview PNGs the printer and the Bambu apps show (#275). - **Done, don't print** After a delivery the menu returns without that option ("Anything else for this slice?"). Each method runs to completion at most once per slice. Slicer choice, file delivery, printer handoff and print authorization are separate decisions. A delivery never creates, uses up or implies a bed-clear approval. **Binding.** When the slice returns, the receipt records each G-code's SHA256. A delivery sends only files listed in this job's `JOB.json` whose bytes still match. A changed, missing or unknown file gets "Not delivered: ..." and nothing is sent. **Discord size limits.** `[discord] upload_max_bytes` (default 9.5 MB, under Discord's 10 MB limit for servers without boosts) is the cap per file: - A file under the cap is attached as is. - A larger `.gcode` is zipped (G-code usually shrinks 4 to 6 times) and the zip is sent if it fits. - If it still does not fit, or it is already compressed (`.3mf`), it is not sent. The reply gives its size and its path on the Print Buddy host. Every file is one message with one `MEDIA:` attachment, labelled "Plate n of m". A summary line follows: "Sent n of m file(s) here. Too big for Discord: ... Failed: ...". **Said before the button, not after (#272).** The post-slice menu sizes each file before it offers "Send the files here". A plain `.gcode` over the cap is deflated once, in memory, to learn what its zip would weigh; a `.gcode.3mf` package counts as is. A file that cannot fit in any form is named in the same message with its size, the cap and its path on the Print Buddy host (for a Bambu package, `//sliced/.../bambu/plate_1.gcode.3mf`), and when nothing fits the button is left out, so the operator never picks a download that fails. Files under the cap are attached exactly as before. The per-file check at send time stays as a safety net for a borderline file. The cap is [`discord.upload_max_bytes`](SETTINGS.md) (`PRINT_BUDDY__DISCORD__UPLOAD_MAX_BYTES`); raise it only when your server's boost level allows larger uploads. Large files stay on the host; Print Buddy has no download server, so fetch them from the archive path it names. **Failures and retries.** A failed upload (the adapter reports `success=False`, or the send raises) is reported with the error, and the option stays in the menu. A retry sends only the files not yet delivered. Too-big files cannot be fixed by a retry, so they count as reported and the option closes. Every attempt is recorded under `deliveries` in the intake receipt. Expiry and cancellation behave as before. ## Bambu Lab | Capability | Status | |---|---| | Vendor profile, settings, delivery options | Implemented. Printer profiles: A1, A1 mini, P1P, P1S, P2S, X1, X1 Carbon, X1E, H2S (every single-nozzle model); LAN status/upload/start are opt-in (see BAMBU-LAB.md) | | Start-code guard | Implemented. After slicing, and again before packaging, every G-code line for a Bambu printer must be a plain G, M or T code. Klipper or ZMod macros (`PRINT_START`, `START_PRINT ... MESH=`, `CHANGE_ZCOLOR`, `LOAD_ZOFFSET`, `BED_MESH_*`) block the slice or the package | | `.gcode.3mf` packaging | Implemented, offline and deterministic. Contents: `Metadata/plate_.gcode`, its upper-case MD5 in `Metadata/plate_.gcode.md5`, `Metadata/slice_info.config` with the plate index and nozzle, and an empty `3D/3dmodel.model`, plus the plate previews `Metadata/plate_.png` (512 px) and `Metadata/plate__small.png` (128 px) from Orca's embedded thumbnail or an offline render of the sliced geometry (#275; `top_.png` and `pick_.png` are not written), laid out the way Bambu Studio exports a sliced plate. **Not yet verified on Bambu hardware or in Bambu Studio.** `printer_model_id` is not written | | Discord delivery | Implemented. Bambu Lab's primary format is `.gcode.3mf`, so "Send the files here" sends the packages | | Bambu Studio as the primary slicer | Implemented, opt-in with `printer.slicer: bambu_studio`, allowed only by the Bambu Lab profile. Runs the configured `[bambu] command` (Flathub Bambu Studio with `--unshare=network`) with the same CLI flags as the existing fallback: `--datadir`, `--load-settings`, `--load-filaments`, `--arrange`/`--orient`, `--slice 0`, `--outputdir`. Orca is not run and there is no fallback. Every Print Buddy guard still applies to its output. The real-slicer CI job covers OrcaSlicer only, so this path is tested with a mocked CLI | | LAN upload | Implemented opt-in (#187 stage c): FTPS upload with size readback, used by the gated LAN print path. Cloud upload is unsupported. Read-only LAN MQTT *status* is separate — see the telemetry row in [BAMBU-LAB](BAMBU-LAB.md) | | Starting a print | Opt-in (#187 stage d): the Start button appears only when the operator adds `printer` to `[delivery] methods`, with LAN configured and the printer in Developer Mode; the start then runs every gate (readiness twice, bed-clear approval, verified upload, telemetry confirmation). Otherwise no Start button is offered — open the package in Bambu Studio or Bambu Handy, or copy it to the SD card | | Pausing or stopping a print | Commands exist on the LAN client; the external Discord print-control module does not call them yet | | Telemetry | Read-only and opt-in: with `[bambu] lan_host`/`lan_serial` configured, the health check reads one LAN MQTT status snapshot. No listener. **Not tested on a printer** | | AMS / AMS Lite multi-color | Unsupported, per the printer profile (issue #127) | Bambu Studio is not treated as a submission path. Picking it as the slicer changes only which program writes the G-code. Live upload or print control would need its own evaluation of firmware, LAN versus cloud authentication and credential handling before any of it is built.