diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 162e733..7b85f66 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "shambu", - "version": "0.3.0", + "version": "0.4.0", "description": "Prepare, inspect and patch Bambu Studio projects (.3mf) from STL files, slice them headless with estimates, and read a Bambu Lab printer's status over LAN (read-only). Built on Bambu Studio's own command line.", "author": { "name": "Shav Vimalendiran" }, "homepage": "https://github.com/Shavvimal/shambu", diff --git a/CHANGELOG.md b/CHANGELOG.md index a3acb41..14fc52b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,104 @@ All notable changes to this project are documented here. The format follows ## [Unreleased] +## [0.4.0] - 2026-10-21 + +Post-mortem fixes for the G2 rev E plates (2026-10-20 to 10-21): a checker that refused a plate Studio printed, an +uncalibrated time estimate, no verb to edit a project, a two-filament slice that never ran, three versions of one tool. + +### Changed +- Layout: the support margin is a per-side `Spread` (tree estimate `min(26, max(8, 0.22 x height))` mm, normal 5 mm, + measured 20.9 mm on the housing's one side; `--probe-supports` measures each supported part with a solo slice and + keeps a support mask, cached under `/.probe/`). A pair needs the larger spread of the two plus a 1 mm line + allowance, not the sum. Footprints are raster silhouettes of the oriented mesh (0.5 mm), not bounding boxes. + `BottomLeftPacker` places by-layer plates (fewest plates, pocket candidates); `ShelfPacker` stays for + print-by-object. Known limit: the five plate-14 parts without `at` pack on two plates even with measured spreads; + the printed one-plate layout needs `at` for the feeler and the cradle (Studio's own arrange exits -21 on them). Brims are clipped by Studio and no longer fail a pair, the bed edge or the reach (`WARN brim`). + Raft and enforced layers count as support (`needs_support`). +- Report: every check line carries a kind: `FAIL body overlap`, `WARN margin rule` (shambu's estimate, the slice + decides), `WARN brim`, `PLACEMENT NOT DONE` (the packer gave up; no part is printed at the origin), a plate-split + line naming which parts went where. An unplaced part never reaches Studio, even with `--keep-going`. Studio exit + -101 names the two objects by spec key; -21 means Studio's own arrange could not fit the parts. +- Two filaments: `effective_filaments` counts a process-level `support_interface_filament` / `support_filament`, so the + right nozzle's reach (x >= 20.5 plus the interface spread) is checked and the tower reserve applies. `ExtruderReach` + is the one reach rule. Every unsliced export is re-sliceable: `RepairNozzleStats` fills an empty + `extruder_nozzle_stats` (the cause of Studio exit -100 "No valid nozzle found" on every re-slice of a shambu + export), `set_filament_map` keeps `filament_extruder_variant` in blocks, and verify refuses a file that is not + re-sliceable. `RunResult.summary()` shows the stderr line behind exit -100. +- Prime tower: `wipe_tower` per plate (`"auto"`, a spot `{x, y, width, rib_wall, brim}`, or `false`) replaces the fixed + (208, 208). `tower.py` ports Studio's size estimate (depth from the tallest part: 41.4 mm square plus 8 mm brim at + 218 mm; narrower is deeper) and places the tower on the silhouettes inside both nozzles' reach, one size per + project, falling back to no rib wall and widths 35..20; `TowerClear` FAILs name the blocking part. The seed + dual-material build that exited -102 now gets a pre-slice FAIL. +- Time: every estimate line reads "Studio estimate H h M min"; a measured range or a calibrated time appears only from + `shambu calibrate` data with its sample count. The two measured prints disagree (0.75 and 1.05 x Studio), so there + is no correction factor yet. +- doctor: a `shambu` section first (version from `__version__`, install kind from `direct_url.json`, commit), + `--expect-version X.Y.Z` (exit 3 on a mismatch with a Fix line; the skills run `shambu --version` first), + `--printer-timeout` (default 15 s), per-step timings; the Studio plist and profile errors no longer abort the + report. Printer discovery is bounded: `arp -an` with a 2 s timeout, parallel port probes scaled to the host count, + a 3 s MQTT connect, the whole read on a daemon thread with per-read state; failures are logged, never swallowed. +- Privacy: `printer status --json`, `printer filaments --json`, `printer raw` and every watch error show no IP, no + serial and no wifi field; the watch log and the calibration file never hold an identifier. +- Spec: `at` is documented as the min corner; `brim_margin` replaces both computed margins; new per-part + `support_spread: [mx, px, my, py]`; unknown keys are warned about in `spec`, `check` and `build`. +- REVIEW-RUBRIC: items 4, 5, 10, 11 and 13 rewritten for the new modules and the `touches` contract. +- Final review round: a filament group's spread is the per-side maximum of its members and keeps their probe masks; + `patch` classifies filaments as `check` does (height-range wall, infill, support and interface keys; support slots + only with support on; an interface-only slot grows by the interface spread); the packer bounds each filament by its + own growth (an interface-only nozzle no longer refuses a plate the checker accepts); `patch --tower` and + `slice --tower` size the tower with `tower.py` (Smooth timelapse included) and test it against the plate's outlines; + `--add-filament` repairs `extruder_nozzle_stats []` before the new slot; `slice` refuses an export without G-code; + printer job and file names, watch headers and `printer raw` drop the serial and access code; a refused patch is + exit 1 and a file that cannot be written exit 3 with one JSON document; packer results read as the packer's answer, + not a proof; two meshes of one name no longer overwrite each other under `sweep --write-spec`. +- Empirical replay (2026-10-21): the tree spread estimate is `min(26, max(8, 0.22 x height))` and is swept from the + bounding box (a packed plate-14b and plate-11 sliced -101 at the old estimate swept from the outline); `slice` names + the -101 pair; `--add-filament` tests the project's tower spot; `inspect` prints each object's min corner and size; + `calibrate` needs 20 layers and 30 minutes for a rate (`span_too_short`) and names a pre-0.4.0 log; a fixed part + off the bed says `at` is the min corner; a two-filament estimate shows the tower minutes and the measured options. + The tower is sized per plate from the filaments its objects use; per-volume and height-range filament keys follow + Studio's scope; the watch header is scrubbed as a structure; `slice` refuses an export missing a requested plate. +- The spread estimate is advisory: when packing with it splits a plate or leaves no tower spot, `check`, `build` and + `sweep` measure that plate's supported parts with the cached solo-slice probe, place again with the measurements + and report a `Spreads measured:` line; `--no-probe` keeps the estimate's answer. + +### Added +- `shambu patch --replace-mesh OBJECT[/PART]=STL` (anchors frame|centre|min, `--rotate-mesh`, `--lay-on-face`, a no-op + guard on canonical triangles, a size guard with the 90-degree hint, shared-mesh and `mesh_shared` refusal, paint and + height-range checks, verify before the atomic replace), `--add-filament "PRESET[:#RRGGBB][@EXTRUDER]"` (append-only, + slots 1..N byte-identical, the new slot normalised by a scratch Studio export, explicit vector schema, verified by a + slice; a colour default with a WARN when the preset has none), `--move OBJECT=X,Y[@PLATE]` (min corner, `+DX,+DY` + relative, resolved before applying, verified on reopen, silhouette clearance FAIL/WARN lines), `--tower X,Y[@PLATE]`, + `--repair-nozzle-stats`, `--dry-run`. Each patch declares `touches` and `Project.save()` raises on an undeclared + changed, added or removed entry. +- `shambu slice FILE.3mf [--plate N] [--no-check] [--tower X,Y[@P]] [--settings-from SPEC] [-o OUT]`: slice a saved + project through shambu's rules (nozzle stats repaired, prime volumes pinned, a fresh run folder, replace only after + the export reads back). `build --slice --no-check` and `slice --no-check` run a checked pass per plate first and + reslice without checks only after a -101 or a warning -100, with `Checks skipped:` lines in the report and + `checks_skipped` in result.json. +- `shambu studio [--out DIR] -- ARGS`: the logged escape hatch; the args pass through `CliCommand.parse` (no action, + flag order, path containment, refused flags), the command, log and exit meaning land in a run folder. +- `shambu sweep --write-spec OUT.json [--pick NAME]`: writes the winning variant as a full spec with a `_sweep` + provenance key (resolved spools, Studio version, command), meshes copied beside it. +- `shambu printer watch --log FILE --gcode3mf PROJECT [--plate N] [--wait-for-start] [--max-errors N] [--max-polls N]`: + timestamped polls (`read_at` UTC, speed level and percent, stages, task id), 60 s polling at the start and the end, + the final state recorded, errors as fixed-text categories. `shambu calibrate --log FILE... [--add] [--show]`: fits + a layer-11-and-up rate and a startup offset per printer model and material, flags paused, failed, non-Standard, + disagreeing and unlogged prints, stores a fingerprinted print in `~/Library/Application Support/shambu/calibration.json` + under a file lock, and offers a point estimate only from three qualifying prints within 0.15. +- `Status` fields `read_at`, `speed_level`, `speed_percent`, `stages`, `task_id`; `ends_at` is a dated UTC time. +- `shambu inspect` prints each filament's extruder and nozzle stat and a "not re-sliceable" line when it applies. +- Skills: `doctor --expect-version 0.4.0` after `shambu --version`; the setup skill installs the matching tag and + refreshes the marketplace before `/plugin update`; a hand-back gate with a "Cause: not established" rule; eight + evals from the post-mortem; `docs/errors/2026-10-21-three-plates-and-uncalibrated-estimates.md`. + +### Fixed +- `patch --map` collapsed `filament_extruder_variant` to one entry per filament, which made Studio log + "could not found extruder_type ... filament_index 2" and slice filament 2 with filament 1's flow ratio. +- `printer status --json` printed the printer's IP address. +- `doctor --json` could not be wrapped in `timeout` on macOS (there is none); the skill says so. + ## [0.3.0] - 2026-10-16 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e4b1b7..7641dbf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ binary or a printer; integration tests skip unless `SHAMBU_INTEGRATION=1` and a |---|---|---| | A new print recipe | `src/shambu/recipes.py` | `skills/shambu/references/07-print-recipes.md` | | A new 3MF patch | `src/shambu/patches.py` + `threemf.py` | `references/03-3mf-format.md` | -| A new limit rule or placer | `src/shambu/layout.py` | `references/01-source-cli.md` section 2 and 8 | +| A new limit rule or placer | `src/shambu/layout.py` | `references/06-slicer-rules.md`, `08-optimisation.md` section 11 and `00-findings.md` section 10 | | CLI flag handling | `src/shambu/studio.py` (`CliCommand`) | `references/01-source-cli.md` section 1 | | Printer fields | `src/shambu/printer.py` | `references/04-app-printer-control.md` | | Skill text | `skills/shambu/SKILL.md`, `skills/setup/SKILL.md` | the plugin docs | diff --git a/README.md b/README.md index 5fbe394..19cd6a9 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,9 @@ spec.json -> check -> build -> patch -> verify -> report.md (+ project - **Presets** are resolved from Bambu Studio's own profile files, including the `include` templates the CLI itself ignores (otherwise a project gets the generic start G-code instead of the printer's). -- **Placement** is computed, never left to the slicer's arrange: every part gets a stated bed rectangle, brim - margins, and a check against the bed, both nozzles' reach and height, TPU on the main nozzle, and the prime tower. +- **Placement** is computed, never left to the slicer's arrange: parts are packed on their mesh outlines (never + rotated) or put at the min corner the spec gives, then checked against the bed, both nozzles' reach and height, + TPU on the main nozzle, each neighbour's support spread and the prime tower. The slice is the final clash test. - **Export** runs the Studio binary headless with the flags that are known to work, in a private data and AppKit home so it can never hang on a hidden "reopen windows?" alert. - **Patch** fixes what the CLI leaves wrong in the 3MF (bed type per plate, all filament colours, the nozzle map @@ -54,17 +55,17 @@ As a Claude Code plugin: /shambu:setup ``` -`/shambu:setup` installs the CLI (`uv tool install "shambu @ git+https://github.com/Shavvimal/shambu"`) and runs +`/shambu:setup` installs the CLI (`uv tool install "shambu @ git+https://github.com/Shavvimal/shambu@v0.4.0"`) and runs `shambu doctor`. From then on, ask Claude to "make a plate from these STLs with PETG functional settings" and the `/shambu` skill does the rest. CLI only: ``` -uv tool install "shambu @ git+https://github.com/Shavvimal/shambu" -shambu doctor +uv tool install "shambu @ git+https://github.com/Shavvimal/shambu@v0.4.0" +shambu doctor --expect-version 0.4.0 # or without installing: -uvx --from "git+https://github.com/Shavvimal/shambu" shambu doctor +uvx --from "git+https://github.com/Shavvimal/shambu@v0.4.0" shambu doctor --expect-version 0.4.0 ``` ## Usage @@ -79,46 +80,71 @@ shambu build plate.json # + export with Studio's CLI, patch, verify, wr shambu build plate.json --slice # also slice headless: .gcode.3mf with time and filament estimates ``` -A spec names the printer preset, a process preset with recipes and overrides, the filaments (`"loaded"` takes the spool in that nozzle) and the -plates with their parts (a file with filament, count, recipes, settings, height ranges, optional fixed position; or -several files merged into one object). `build` writes `/.3mf`, `.report.md`, `.log` and -the CLI inputs under `/shambu/`. +A spec names the printer preset, a process preset with recipes and overrides, the filaments (`"loaded"` takes the +spool in that nozzle) and the plates with their parts (a file with filament, count, recipes, settings, height +ranges, and an optional `at`: the min corner of its box on the bed; or several files merged into one object). Each +`check` line names its kind: `FAIL body overlap`, `FAIL margin rule`, `WARN margin rule` (an estimated support +spread: the slice decides), `WARN brim` or `PLACEMENT NOT DONE`. `--probe-supports` measures each supported part's +spread with a cached solo slice. A two-filament plate gets a prime tower that `"wipe_tower": "auto"` sizes and +places. `build --slice` is the final test: Studio's exit -101 names the two objects that clash. `build` writes +`/.3mf`, `.report.md`, `.log` and the CLI inputs under `/shambu/`. **2. Find the best orientation and print sequence.** Score orientations without slicing, then let the slicer judge. ``` shambu orient bracket.stl # bed contact, support shadow, height per candidate face shambu sweep plate.json --orientations bracket --by-object # slices each variant: time, grams, support, waste, swaps +shambu sweep plate.json --orientations bracket --write-spec plate-best.json # writes the fastest variant as a spec shambu rotate part.stl -o out --lay-on-face 0,0,1 # write a turned copy; shambu split a.stl -o out ``` A part takes `"lay_on_face": [nx, ny, nz]` and `"rotate": [x, y, z]`; a plate takes `"print_sequence": "by object"` (one object after another: on a two-filament plate that drops the prime tower and the per-layer nozzle changes, 40 min instead of 1 h 24 on the measured plate). The sweep writes each variant to `/sweep//`. +`--write-spec OUT.json` writes the fastest variant that passed (or `--pick NAME`) as a full spec with the original +`outdir` and a `_sweep` key that records the variant and its Studio estimate; build that file. -**3. Inspect or patch an existing project.** +**3. Inspect, patch, edit or re-slice an existing project.** ``` shambu inspect project.3mf shambu patch project.3mf --bed-type "Textured PEI Plate" --colours "#1A1A1A,#F0F0F0" --map 1,2 \ --set "carrier_1:enable_support=1" --pause 1:10.2 --rename-plate "1:carrier D.6" --print-sequence "1:by object" +shambu patch project.3mf --replace-mesh housing=housing-v2.stl --dry-run # swap a mesh, keep name, settings, place +shambu patch project.3mf --add-filament "Bambu PLA Matte @BBL X2D 0.4 nozzle:#FCECD6@2" +shambu patch project.3mf --move housing=40,30 # the object's min corner, plate-local mm +shambu patch project.3mf --tower 172,150@1 # the prime tower's min corner on plate 1 +shambu slice project.3mf [--no-check] # headless slice of a saved project ``` -**4. Read the printer (read-only).** +`patch` changes the project in place (or to `-o`) and declares every entry it touches. The edit verbs check what +they change: `--replace-mesh` refuses a size change, a shared or painted mesh; `--add-filament` appends a slot and +verifies it with a slice (a preset with no colour takes the next unused default colour, with a warning); `--move` +and `--tower` report the same pair rule as `check`. `shambu slice --no-check` adds an unchecked pass only after a +-101 or a warning -100, as the GUI would, and the report lists what it skipped. `shambu studio -- ARGS` is the +logged last resort for anything else. + +**4. Read the printer (read-only) and measure prints.** ``` shambu printer filaments [--ip ] # the spool in each nozzle and slot, with the preset it resolves to shambu printer status [--ip ] shambu printer watch --every 300 # until the printer leaves RUNNING/PAUSE +shambu printer watch --wait-for-start --log print.jsonl --gcode3mf plate.gcode.3mf # log one print +shambu calibrate --log print.jsonl --add # store it; `calibrate --show` prints the fit per printer and material ``` +Every time in a report is "Studio estimate", Studio's own prediction. The watch log stamps each poll and never +holds the IP or the serial. A report adds the measured range of stored prints, and a calibrated time only when +three qualifying prints of that printer model and material agree. + `check` and `build` read the same report before resolving a spec: a filament written as `{"preset": "loaded"}` takes the spool in that nozzle with its colour, and a pinned preset that is not loaded fails the check (exit 1) unless `--allow-filament-mismatch`. With the printer unreachable the report starts with a WARNING and the GUI's last-used presets stand in. Add `--json` to any command for machine-readable output (`printer watch --json` prints one document per poll). -Exit codes: 0 ok, 1 a check or verification failed, 2 usage, 3 environment (binary, config, printer). +Exit codes: 0 ok, 1 a check or verification failed or a patch was refused, 2 usage, 3 environment (binary, config, printer, a file that cannot be written), 130 interrupted. ## Safety @@ -156,8 +182,12 @@ Exit codes: 0 ok, 1 a check or verification failed, 2 usage, 3 environment (bina - `support_filament` 0 ("default") lets Studio print a part's support with whichever filament is loaded. On a two-filament plate that put a filament-1 part's support base on the right nozzle at x < 20.5 (exit -102). The planner pins `support_filament` and `support_interface_filament` to the part's own filament when they are unset. -- Tree support spreads past a part's box: 15.6 mm on a 170 mm bracket, which conflicted at a 15 mm gap. The layout - keeps 0.25 x height, clamped to 5..25 mm, round a supported part, and checks extruder reach on that rect. +- Tree support spreads past a part's box and does not avoid a neighbour: 20.9 mm on one side of a 218 mm housing, + 15.6 mm under a 170 mm bracket. The checker estimates `min(26, max(8, 0.22 x height))` per side (normal support + 5 mm), keeps the larger of two facing spreads plus 1 mm between parts, and leaves the verdict to the slice + (exit -101); `--probe-supports` measures it. +- An unsliced CLI export writes `extruder_nozzle_stats: []`, and a later CLI slice of it exits -100 "No valid + nozzle found". Every export gets the repaired key, and verify refuses a file without it. - A mesh whose whole bbox is under 1 mm is in metres (URDF exports); Studio fails it with -100. `check` refuses it, and `shambu rotate --scale 1000` writes a mm copy. @@ -180,7 +210,7 @@ make install # uv sync --group dev make check # ruff, mypy, pytest claude plugin validate --strict . claude --plugin-dir . # try the skills from this checkout -uv tool install --reinstall . # install the CLI from this checkout +uv tool install --reinstall . # development only: a snapshot that does not follow the plugin; users install the tag (/shambu:setup) ``` Before a release, run the real-CAD loop in `data/README.md` (the folder is gitignored; copy your own STLs in). diff --git a/REVIEW-RUBRIC.md b/REVIEW-RUBRIC.md index 30e7f8d..23219cf 100644 --- a/REVIEW-RUBRIC.md +++ b/REVIEW-RUBRIC.md @@ -1,18 +1,18 @@ # shambu review rubric -Each item has a test a reviewer can run. An item passes only when its test passes. Run from -`~/.claude/skills/bambu-3mf/scripts/`. "rg" = ripgrep. +Each item has a test a reviewer can run. An item passes only when its test passes. Run from the repository root +(`src/shambu` is the package). "rg" = ripgrep. ## Architecture -1. **Front-end boundary.** Only `shambu/cli.py` and `bambu.py` read argv, stdin or print to stdout. - Fail if `rg -n "sys\.argv|input\(|print\(" shambu --glob '!cli.py'` returns a line, or a library module imports `shambu.cli`. +1. **Front-end boundary.** Only `shambu/cli.py` reads argv or stdin or prints to stdout. + Fail if `rg -n "sys\.argv|\binput\(|\bprint\(" src/shambu --glob '!cli.py'` returns a line, or a library module imports `shambu.cli`. 2. **Dependency injection.** `Pipeline`, `Bambu`, `PrinterService` receive their adapters (`StudioCLI`, `PresetResolver`, `Placer`, `LimitChecker`, `Discovery`) in `__init__`; tests construct them with fakes. Fail if `subprocess.` appears outside `studio.py`, `printer.py` (arp) and `facade.py` (open), or `paho` is imported outside `printer.py`. 3. **Constructors only store.** No `__init__` does I/O. (`StudioCLI.__post_init__` checks that the binary exists: that is a stat, documented.) -4. **Pure planning core.** `spec.py`, `assemble.py`, `layout.py`, `patches.py`, `recipes.py` import no `subprocess`, `socket`, `paho`, `zipfile`. - Fail if `rg -n "import (subprocess|socket|zipfile)|paho" shambu/{spec,assemble,layout,patches,recipes}.py` matches. -5. **Pattern economy.** Every Protocol or Strategy has two implementations or a test fake: `PresetSource` (DirectorySource, ChainedSource), `Placer` (ShelfPacker + test fake), `Rule` (five), `Patch` (eleven), `Discovery` (ArpProbe, SsdpListen), `Listener` (CLI logger + test recorder). +4. **Pure planning core.** `spec.py`, `assemble.py`, `layout.py`, `tower.py`, `edits.py`, `patches.py`, `recipes.py` import no `subprocess`, `socket`, `paho`, `zipfile`. + Fail if `rg -n "import (subprocess|socket|zipfile)|paho" shambu/{spec,assemble,layout,tower,edits,patches,recipes}.py` matches. +5. **Pattern economy.** Every Protocol or Strategy has two implementations or a test fake: `PresetSource` (DirectorySource, ChainedSource), `Placer` (ShelfPacker, BottomLeftPacker + test fake), `Footprint` (RasterFootprint, BoxFootprint), `SpreadProbe` (StudioProbe + test fake), `Rule` (Clearance, InsideBed, ExtruderReach, TpuOnMain, MeshSane, SequentialClearance, tower.TowerClear), `tower.TowerClearance` (FootprintClearance, BoxClearance + test fake), `Patch` (thirteen in `patches.py`, four in `edits.py`; the Protocol declares `touches`, `resolve_touches` and `verify`, and `PatchBase` gives their defaults, so no caller looks them up with `getattr`), `Discovery` (ArpProbe, SsdpListen), `Listener` (CLI logger + test recorder). 6. **Template Method is overridable.** A `Pipeline` subclass that overrides `export()` with a fake runs end to end in tests without Bambu Studio. ## Correctness @@ -20,10 +20,11 @@ Each item has a test a reviewer can run. An item passes only when its test passe 7. **CLI rules encoded, not remembered.** `CliCommand.args()` refuses a command with no action, orders `--no-check` before `--slice`, emits `--flag=0|1` for bools, joins `--export-3mf` to a bare name + `--outputdir`. Fail if a test for each rule is missing. 8. **Presets are complete.** `PresetResolver.for_cli("machine", "Bambu Lab X2D 0.4 nozzle")["machine_start_gcode"]` is longer than 10000 chars (the `include` templates are merged) and `check_compatible` passes for the X2D defaults. 9. **Assemble-list rules.** `assemble.validate()` flags: non-string `*_params` values, unknown keys, `pos_*` length mismatch, non-part subtype without `assemble_index`, height ranges on merged parts, > 36 plates, filament index > count. -10. **Placement is explicit and checked.** Every part has a computed bbox on the bed; `LimitChecker` flags overlap, bed exit, extruder 2 reach (x < 20.5 and the user's 235.5 limit) for the part's filament AND its height-range or sub-part filaments, TPU off the main nozzle, non-manifold meshes. -11. **3MF integrity.** A patch rewrites only `Metadata/*` entries, keeps entry order and compression, passes `zipfile.testzip()`, and applying the same `PatchSet` twice gives the same summary. `layer_config_ranges.xml` ids are build-order indexes. +10. **Placement is explicit and checked.** Every part has a computed bbox on the bed; `LimitChecker` flags overlap, bed exit, extruder 2 reach (x < 20.5 and the user's 235.5 limit) for the part's filament AND its height-range, sub-part and process support or interface filaments (one FAIL line per part and extruder), TPU off the main nozzle, non-manifold meshes. +11. **3MF integrity.** Each `Patch` declares `touches` (entry names or fnmatch patterns; none declared means `Metadata/*`), including the entries its slice invalidation removes or rewrites (`edits.SLICE_TOUCHES` plus `model_settings.config`). `Project.save()` compares every entry with the loaded bytes and raises, before writing, when an undeclared entry changed, was added or was removed, when a `3D/*` entry was added, removed or reordered, or when a `_rels` entry changed. Only the geometry edits in `edits.py` declare `3D/*` entries: the sub-model of the one object they name (resolved at apply time) and the item or component `transform` attributes of `3D/3dmodel.model`, rewritten by targeted text substitution. An edit verifies the temporary archive before it replaces the destination. Entry order, compression and dates stay; the output passes `zipfile.testzip()`; applying the same `PatchSet` twice gives the same summary and the same bytes. `layer_config_ranges.xml` ids are build-order indexes. + Test: `tests/test_edits.py::test_save_rejects_undeclared_entry_change`, `test_undeclared_deletion_raises`, `test_edit_applied_twice_is_idempotent`. 12. **Verify after export.** `Pipeline.verify()` compares bed type, process name, filament and plate and object counts, start/end G-code length and every process override against the written file. -13. **Explicit failure.** Missing preset, binary, STL, or a crashed/timed-out CLI raises a `BambuError` subclass or yields a `RunResult` with a non-zero code and a meaning string; no `except` returns `None` silently. `ruff check --select BLE001,E722` is clean except the two documented `# noqa: BLE001` in the facade's doctor and the MQTT disconnect. +13. **Explicit failure.** Missing preset, binary, STL, or a crashed/timed-out CLI raises a `BambuError` subclass or yields a `RunResult` with a non-zero code and a meaning string; no `except` returns `None` silently. The documented silent fallbacks: a malformed optional numeric setting takes Studio's default, with a comment at the parse (`layout._int`, `tower._f` raises instead for the tower inputs); an unreadable Studio log, a missing Studio version, an unreadable mesh outline and a malformed filament or nozzle map are never silent (a WARN line or a log warning naming the path). `ruff check --select BLE001,E722` is clean except seven documented `# noqa: BLE001`: two in the facade's doctor (the config and machine sections: one section's failure must not hide the others), two in the MQTT teardown (`loop_stop` and `disconnect` after a read), the injectable port probe in `ArpProbe` (a closed port is False; any other exception is logged and counted closed), the caller's `on_step` progress callback (logged, never fails the read), and the printer read's worker thread (`BaseException` caught to re-raise it in the caller's thread). ## Safety @@ -35,5 +36,5 @@ Each item has a test a reviewer can run. An item passes only when its test passe 17. **Types and lint.** `ruff check .` clean with the `pyproject.toml` rule set; `mypy shambu` reports no errors in `spec.py`, `assemble.py`, `layout.py`, `patches.py`, `presets.py`. 18. **Tests offline by default.** `python3 -m pytest -q` passes without the Studio binary, the network or paho (integration tests skip unless `SHAMBU_INTEGRATION=1`). -19. **CLI UX.** Every subcommand supports `--json` with exactly one JSON document on stdout and logs on stderr; exit codes 0 ok, 1 check failed, 2 usage, 3 environment error. `--help` works without paho. -20. **Docs.** Every module has a docstring that states what it handles and what it does not; `README.md` shows the three main flows (build a spec, patch a file, read the printer); the SKILL.md recipe points at `bambu.py`, not at hand-assembled flags. +19. **CLI UX.** Every subcommand supports `--json` with exactly one JSON document on stdout and logs on stderr; exit codes 0 ok, 1 a check failed or a patch was refused, 2 usage, 3 environment error (a file that cannot be written included), 130 interrupted. The exceptions: `printer watch --json` prints JSON lines, one document per poll; an argparse error prints its usage text on stderr before any JSON (exit 2). `--help` works without paho. +20. **Docs.** Every module has a docstring that states what it handles and what it does not; `README.md` shows the three main flows (build a spec, patch a file, read the printer); the SKILL.md recipe points at `shambu`, not at hand-assembled flags. diff --git a/docs/errors/2026-10-21-three-plates-and-uncalibrated-estimates.md b/docs/errors/2026-10-21-three-plates-and-uncalibrated-estimates.md new file mode 100644 index 0000000..06b55bf --- /dev/null +++ b/docs/errors/2026-10-21-three-plates-and-uncalibrated-estimates.md @@ -0,0 +1,87 @@ +# Error status: "three plates" from the checker, and estimates shown as times (2026-10-20 to 2026-10-21) + +Status: FIXED in the tool and the skill for 0.4.0 (section 9); the open questions in section 6 stay open. Severity: high (the tool told the owner a five-part layout needs three plates; the owner placed the same parts by hand in Studio with 10 to 16 mm between them and it sliced and printed). The owner's words: "it fits fine, what on earth were you on about?". Written up from the G2 rev E plates session, its post-mortem and the research that corrected the post-mortem. + +## 1. What happened, in order + +1. 2026-10-20: a plates subagent ran the installed `shambu check`: `PresetError: filament preset 'loaded' not found`, exit 3, three times. The CLI on PATH was 0.2.1 (a `uv tool install .` snapshot of a checkout, taken on 2026-10-13), the plugin's skill text 0.2.0, the checkout 0.3.0. From then on the session ran the checkout with `uv run --project`. Nothing compared the three versions. +2. Plates 11, 11a, 12 and 13 were built and sliced. Plate 12 was turned 45 degrees on the bed diagonal because the support margin refused it straight. +3. 20:26 to 20:43: housing, camera shell, mount and feeler bar on one plate, six specs in a row. Every check failed: `the parts need 2 plates`, `feeler_module_print is 226.4 mm deep (+16.0 margin): deeper than the bed area`, and overlap and bed-edge FAILs on every hand placement. The owner was told "three plates". Two plates were released (13 h 54 min, 379 g; 1 h 30 min, 37 g). +4. The assistant explained this with two claims: "the housing must print alone" and "the feeler needs the bed diagonal". Both were wrong: a plate with the mount beside the housing passed 16 minutes later, and the feeler now prints straight along the bed edge. +5. 20:57: the owner placed housing, shell and mount by hand in Studio: 10 h 36 min, 271 g (Studio's default process). +6. 10-21: the new housing mesh went into the owner's layout by unzipping the 3MF and rewriting its XML by hand (no verb existed). The first run silently kept the old mesh. A cradle was added: 18 h 0 min, 481 g. That plate printed. +7. 09:10: one `doctor` call "returned nothing". It never ran: the call was `timeout 60 uv run ... shambu doctor --json 2>/dev/null | python3 -c "json.load(...)"`, macOS has no `timeout` command, the shell sent "command not found" to `/dev/null`, and `json.load` failed on empty input 0.21 s later. doctor did not hang. +8. 09:13 to 09:21: a two-filament version (PLA interface) never sliced. `check` gave six overlap FAILs plus bed and reach FAILs on the layout now printing. Hand-run Studio calls failed: first `could not found extruder_type ...` (the per-extruder variant keys), then exit 156 on every variant. shambu's own two-part seed failed with -102. The assistant said "the reason is space, not the filament". With the tower off and with three parts removed it still failed, so that claim had no support. The real cause of 156 (found later, section 2): one empty key, `extruder_nozzle_stats: []`, in every unsliced CLI export. +9. Every time shown to the owner was Studio's estimate. The two prints with a progress log disagree. Bracket D.7 (2026-10-16, PETG, a standard-flow HS01 nozzle, the speed level not logged) ran at 0.75 x Studio mid-print and about 0.75 to 0.80 over the whole print (6 h 50 predicted, about 5 h 0 to 5 h 30 real). The E7a cradle (2026-10-21) ran at 1.05 x Studio between layers 135 and 182. + +## 2. Root cause, five layers + +1. **The checker over-reserved.** `layout.support_margin` grew every supported part by 0.25 x its height (5 to 25 mm) on all four sides, at every height, for any support type. The pair test added both neighbours' margins. Footprints were axis-aligned boxes. The 25 mm came from one measurement (13 to 15.6 mm, one side, below z 10). Studio has no clearance rule between objects before slicing; a real clash is exit -101 after slicing. +2. **The packer failed closed.** A shelf packer with no rotation and rows by depth. When it gave up it left the parts at the origin, and the report printed bed-edge FAILs for positions nobody chose. "Needs N plates" was its answer, not Studio's. +3. **The skill made the checker the judge.** "`check` until every line reads OK", and no rule against stating a cause. The assistant turned the checker's lines into "three plates" and into two causes no output showed. The hand-back template, which would have shown the check lines and no slice, was skipped. +4. **Versions and verbs.** Three versions ran in one session, and the CLI had no verb for mesh replace, add filament, move object or move tower, so the session hand-ran Studio nine times against the skill's rule. +5. **Unsliced exports were not re-sliceable.** The CLI writes `extruder_nozzle_stats: []` on an export without `--slice` (BambuStudio.cpp:6648-6686). A later CLI run reads the present key as authoritative, counts 0 nozzles and throws "No valid nozzle found" (BambuStudio.cpp:4101-4156, ToolOrdering.cpp:1477-1481), mapped to -100, exit 156, with the text on stderr only (BambuStudio.cpp:7413-7418). A one-filament plate of the same export failed the same way: it was never space and never the second filament. + +The time numbers are Studio's kinematic prediction (`prediction` in `slice_info.config`): heat waits and calibration count 0 s, and nothing scales the result per printer. shambu labelled them "estimate" in the report, but the hand-back template said "