From 7b181b84f0ffd00f3859eb7777ae36ce10280ad0 Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 23 Aug 2026 10:36:50 +0200 Subject: [PATCH] Let a filled or foamed filament keep its own name (issue #2902) The reduction that gave an AMS slot a material type read PLA-AERO, PLA-GF, ASA-GF and PPS-GF as their base material, so a slot loaded with foaming or glass-filled filament went out saying plain PLA or ASA. That is worse than the bug it replaced. "PLA-AERO" matched nothing before, which was useless but honest; "PLA" matches every PLA plate in the queue, so the dispatcher would have sent one to filament that will not print it -- and the contract the first fix claimed, that it could only ever repair a slot, no longer held. @doncaruana caught PLA Aero on the issue. All four are values Bambuddy itself offers: filament_fields.json is the material list the Profiles editor puts in a dropdown, and the reduction table was assembled from the cloud filament names and the frontend preset parser without ever being checked against it. It is checked now, so the next type added to one and not the other fails a test rather than a print. ASA-AERO joins them from the cloud catalogue (GFB02). The table hyphenates because the slicers do, while a spool says "PLA Aero" and every Bambu preset name says "Bambu PLA Aero". Adjacent words are joined and taken when the join is a type exactly -- exactly, because letting the prefix and suffix rules reach across a space would make "Support for PLA" a type by its tail. Also from @doncaruana, and the better half of his point: a preset is chosen from a list the slicer defines, so it already knows its own type and nothing has to be read out of a product name. The resolver now hands that answer back and both assign routes prefer it. It cannot be the only source -- material is required on a spool and slicer_filament is not, and the spool this issue was reported for had no preset at all -- so the reduction stays as the fallback for spools without one. Two things had to move with it. The auto-unlink guard compared the slot's reported type against the reduced material, so a spool whose preset outranked its material column would have been unlinked from the slot it had just been assigned to; it now accepts any type the assign path could have written. And two lookups keyed by material took the catch-all for a type they had no row for, which sent an ASA-GF spool out at 200/240 -- too cold to extrude -- and preheated its chamber to nothing. Both fall back to the base material last, so PLA-CF, PETG-CF and PA-CF keep the rows they are listed with, and ASA-CF and ABS-GF pick up ranges they had been missing all along. What counts as a material name is decided by the base for the same reason: saying yes throws the value away and rescues the slot from the generic-material fallback, so the answer has to be no when that fallback has nothing to offer. ABS-GF reduces to a generic ABS the printer can resolve; PPS-CF reduces to nothing and is left as it stands. Adding a type to the table therefore cannot quietly change that answer, which is how these five slipped through in the first place. ------ Hand the bundled chamber-preheat table back the way it is read Every lookup of the per-filament chamber map happens after the keys are upper-cased, and the parser documents exactly that: keys uppercased, DEFAULT always present so the resolution loop can index it unconditionally. The three fallback paths returned the bundled constant as declared, with the lowercase "default" row the Settings editor writes and displays, so an install that had never opened the setting got a dict the loop could not read its fallback out of and used a hardcoded 0 for any filament without a row of its own. It reported the right number only because that bundled default is 0. Raising it would have changed nothing for everyone who had not customised the map, with the map in Settings still showing the value that was not being used. The test that should have caught this asserted the fallback under either spelling, and its docstring contradicted itself between title and comment. It pins the contract now, over all four ways the parser can fall back. --- CHANGELOG.md | 3 +- backend/app/api/routes/inventory.py | 14 +- backend/app/api/routes/spoolman.py | 7 +- backend/app/api/routes/spoolman_inventory.py | 12 +- backend/app/main.py | 36 ++- backend/app/services/print_scheduler.py | 29 +- .../app/services/slicer_filament_resolver.py | 59 +++- backend/app/utils/filament_types.py | 81 +++++- .../test_ams_slot_material_2902.py | 270 +++++++++++++++++- .../services/test_slicer_filament_resolver.py | 130 ++++++++- backend/tests/unit/test_scheduler_preheat.py | 68 ++++- .../utils/test_printer_filament_type_2902.py | 121 +++++++- frontend/src/utils/preheatFilamentTargets.ts | 7 +- 13 files changed, 785 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7afd5deb9..b9f47c7ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,11 @@ All notable changes to Bambuddy will be documented in this file. - **The Watchtower we recommend for daily builds is the maintained fork (#2917, reported by @CamelT0E)** — The daily-build instructions in the README, on Docker Hub and in every daily prerelease pointed at containrrr.dev/watchtower. That project has been archived and read-only since December 2025 and its last release, v1.7.1, is from November 2023, so anyone following the recommendation was being handed a container with Docker socket access that had not received a fix in over two years. Development continues in Nicholas Fedor's fork, which ships as `nickfedor/watchtower` and released v1.21.0 this month. All four references now point at watchtower.nickfedor.com and name the image, including the release-notes template in `docker-publish-daily-beta.sh` that produced the screenshot in the report — the READMEs alone would have left every future daily prerelease repeating the dead link. Existing images keep working; only the recommendation changed. ### Fixed +- **The bundled chamber-preheat table was unreadable to the code that reads it** — every lookup of the per-filament chamber map happens after the keys are upper-cased, but an install that had never opened the setting got the bundled table back exactly as declared, with its lowercase `default` row. The scheduler then looked for `DEFAULT`, found nothing, and used a hardcoded 0 for any filament without a row of its own. It reported the right number only because that bundled default is 0 — raising it would have silently changed nothing for everyone who had not customised the map. Both paths out of the parser now honour the one contract it documents. - **The archives API never reported which plate was printed (#2796, contributed by @sgiffhorn)** — `archive_to_response()` builds the archives response field by field and had no line for `plate_id`, so `GET /archives/`, the detail endpoint, search, PATCH and the project archive list all answered `plate_id: null` — for archives whose column was populated as well. `ArchiveResponse.plate_id` defaults to `None`, so Pydantic filled the null in without complaint and nothing ever raised. The column has been written since #2603 and is backfilled from the queue on startup, so the plate was recorded all along and simply could not be read back out; on the reporting instance 181 of 257 rows carry one. Archive cards now name the plate again — but only when it is not the first one. The queue records a plate for single-plate files too, because the print dialog auto-selects the only plate there is, so labelling every archive that has a `plate_id` would have put "Plate 1" on most cards in the grid and taken room from the truncated print name. A multi-plate archive printed from its first plate still identifies itself through the plate carousel, which has always gated on whether the source 3MF holds more than one plate. - **The first AMS sync after enabling Spoolman from Settings failed on every slot (#2903, diagnosed by @ojimpo)** — Spoolman rejects a spool whose `extra` dict carries a key it has not been told about, answering HTTP 400 `Unknown extra field tag.`. Bambuddy stores the tray UUID in `extra.tag`, so that key has to be registered before the first spool is created — and registration only ever ran from three hand-maintained lists that fire when the integration is *set up*: the Connect button, application startup, and two inline blocks in the inventory routes. Enabling Spoolman from the Settings page reaches none of them, so "Sync AMS Data" reported `Synced 0 spools with 3 errors` with vendor and filament creation succeeding and only spool creation rejected. Restarting Bambuddy cleared it, which made the failure look like a connectivity problem. The Connect button would also have fixed it, but it is not on screen by then: saving the settings initialises the Spoolman client as a side effect of syncing locations, the status endpoint reads any live client as "connected", and the UI shows the Connect button only while disconnected — so the one registration path reachable from the interface hides itself exactly when it is needed. Registration now travels with the write instead of the feature: every spool write that carries an `extra` dict registers the keys it is about to send, once per client, before it sends them. That covers all five places Bambuddy writes a tag — AMS sync, linking and unlinking a tag from a spool, and both inventory edit paths — and it closes the class rather than the instance, since a write that carries a key is now a write that registers it. `bambu_color_name` is the cautionary case: it never made it into the Connect or startup lists at all, and worked only because two call sites remembered to register it by hand. Registration stays best-effort — if it fails, the write is still attempted and reports exactly what it reported before, and the failure is not cached, so a Spoolman that was merely restarting is retried on the next write. - **The Spoolman connection status described Bambuddy's memory rather than Spoolman (#2903)** — "Connected" meant "some earlier request in this process left a client object behind", and around twenty code paths build one lazily, so the answer turned on which page had been opened rather than on anything about Spoolman. A switched-off integration could still report Connected from a leftover client; changing the URL kept reporting on the previous host; and saving the Settings page built a client as a side effect of syncing locations, which is how enabling Spoolman came to report Connected before anything had been set up. The status now asks the Spoolman that is configured right now, so the answer is the same whatever you opened first. The Disconnect button is gone with it: Spoolman is a stateless HTTP API with no session to close, so the button only dropped the client object that the next request rebuilt moments later, after which the status flipped back on its own — it looked like it had worked, then quietly undid itself. Turning the integration off is the enable toggle's job, and Connect remains as what it actually is, a way to re-check a Spoolman that is not answering. -- **An AMS slot assigned a PLA+ spool became unusable for PLA (#2902, reported by @doncaruana)** — Assigning a spool wrote its material straight into the slot's `tray_type`, and a slot that says "PLA+" satisfies nothing that asks for PLA: not OrcaSlicer, not Bambu Studio, and not Bambuddy's own dispatch matcher, which compares the printer's reported type to the one the 3MF declares. The same string then missed the filament-id lookup, so the slot went out with no `tray_info_idx` at all — the half-configured state a printer reverts from — and took the generic 200/240°C nozzle range instead of PLA's. PLA+ is not a special case: Bambuddy's own colour catalogue supplies the material dropdown, and about forty of its values are vendor product lines rather than filament types — HTPLA, PolyTerra PLA, PLA Matte, ASA Extrafill, Flexfill TPU 98A. All four routes that configure a slot now reduce the material to a type the printer knows before sending it, and the product name moves to `tray_sub_brands`, which is where Bambu Lab itself puts it — their catalogue has a preset named "eSUN PLA+" whose type is PLA. A material that cannot be placed is sent exactly as before rather than guessed at, so this can only repair a slot, never break a working one; and a material that already had a preset of its own keeps it, so "PETG HF" is not quietly downgraded to plain PETG. One thing that starts working as a result: a slot holding a calibrated preset is now reused when a same-material spool is assigned to it, which for these spools could never happen before. +- **An AMS slot assigned a PLA+ spool became unusable for PLA (#2902, reported by @doncaruana)** — Assigning a spool wrote its material straight into the slot's `tray_type`, and a slot that says "PLA+" satisfies nothing that asks for PLA: not OrcaSlicer, not Bambu Studio, and not Bambuddy's own dispatch matcher, which compares the printer's reported type to the one the 3MF declares. The same string then missed the filament-id lookup, so the slot went out with no `tray_info_idx` at all — the half-configured state a printer reverts from — and took the generic 200/240°C nozzle range instead of PLA's. PLA+ is not a special case: Bambuddy's own colour catalogue supplies the material dropdown, and about forty of its values are vendor product lines rather than filament types — HTPLA, PolyTerra PLA, PLA Matte, ASA Extrafill, Flexfill TPU 98A. All four routes that configure a slot now reduce the material to a type the printer knows before sending it, and the product name moves to `tray_sub_brands`, which is where Bambu Lab itself puts it — their catalogue has a preset named "eSUN PLA+" whose type is PLA. A material that cannot be placed is sent exactly as before rather than guessed at, so this can only repair a slot, never break a working one; and a material that already had a preset of its own keeps it, so "PETG HF" is not quietly downgraded to plain PETG. One thing that starts working as a result: a slot holding a calibrated preset is now reused when a same-material spool is assigned to it, which for these spools could never happen before. A filled or foamed variant is a type in its own right and is left alone: PLA-AERO, PLA-GF, ASA-GF and PPS-GF were being reduced onto their base material, so a plain PLA plate could have been dispatched onto foaming filament — every type the Profiles editor offers now reaches the slot intact, whether it is written "PLA-AERO" or "PLA Aero". And when a spool points at a slicer preset, the slot takes that preset's own filament type rather than one read out of the material column, since a preset is chosen from a list the slicer defines; the material is still what a spool without a preset is read from. Two lookups keyed by material had to learn the same distinction: a variant with no nozzle range of its own now takes its base material's rather than the 200/240 catch-all, so an ASA-GF spool is no longer sent out at ASA's minimum minus thirty degrees, and the preheat chamber target does the same — ASA-CF and ABS-GF reach a warm chamber for the first time, while PETG-CF and PA-CF keep the hotter rows they are listed with. - **A failed upload told you to check the SD card, whatever had actually gone wrong (#2899, reported by @grolmus)** — Every dispatch upload that failed carried the same sentence: "Failed to upload file to printer. Check if SD card is inserted and properly formatted (FAT32/exFAT)." The reporter got it after a TLS handshake failure and restarted the printer on the strength of it. That could not have helped — the handshake never reached the printer's filesystem, and the cool-off that made the next dispatch fail identically lives in Bambuddy's own memory, where power-cycling a printer does not reach. #2780 had already taken operator advice out of this failure's log line, for exactly the reason that the advice was known not to work; it survived in the string people actually read. The information to say something true was never missing. The FTP client separates five connect failures and three upload reply codes, each with its own log line — 553 even gets a spelled-out list of storage causes — and then handed the caller a bare true-or-false, so the dispatch had nothing to go on and guessed storage for all of them. The reason now travels with the result, and the message is chosen from it: a 553 or 552 keeps the card advice, which is the case it was written for, and quotes the printer's reply code so a queue entry and a support bundle can be lined up. A handshake failure says the file service answered without TLS and that the card is not involved. A refused connection points at the access code, a timeout at the network, and anything the client could not classify says so and points at the log rather than picking a plausible cause — a wrong instruction costs more than a vague one, because it sends someone to work on hardware that is fine. No message prescribes a power cycle, which is the restraint #2780 settled on. The failure notification carries the same sentence the queue shows, rather than its own fixed "Failed to upload file to printer", so a push and the screen can no longer disagree. - **One handshake failure took out three queued jobs and every retry they had (#2898, reported by @grolmus)** — A print dispatch that met a TLS handshake failure spent its whole retry budget without opening a single socket. The cool-off that a failed handshake arms (#2780) lives inside `connect()`, so it applied to everything — including the dispatch, whose four attempts two seconds apart were all answered from the gate rather than the network. The reporter's farm logs the shape exactly: the delete that clears the way for the upload took the SSL error at 11:10:04.956, the upload's first attempt started 8ms later, and attempts two through four took one to two milliseconds each. Because the cool-off runs for five minutes, the next two jobs queued for that printer failed the same way inside the same window, and on that farm the failure is transient — a manual connect a second later completes cleanly — so the retry the gate suppressed is the one that would have worked. The gate was serving two callers that want opposite things from it. The background sweeps that fetch a 3MF, a cover or a timelapse after a print walk about a hundred and ten candidate paths against one wedged printer with nobody waiting, and backing off for minutes is right for them; they keep today's behaviour untouched. A dispatch is one delete plus at most four upload attempts with someone watching a progress bar, so it now ignores the cool-off, as does a firmware upload, for the same reason. Callers that do respect the cool-off no longer sleep out a retry loop against it either: the loop stops at the attempt that armed the gate and says so, instead of spending three more attempts and six seconds on connections that cannot happen. What made this a log dive rather than a glance is fixed with it: the cool-off skip was the one connect failure that reported without naming its cause, and did so at debug level, so four identical reason-free warnings were all the operator saw. It now says at warning level that nothing was sent and how long the printer has left — once per cool-off rather than once per attempt, so that raising it does not recreate the log flood #2780 set out to stop. - **Archived projects crowded out the live ones in every project picker (#2888, reported by @e77)** — The reporter files each job under its own project and archives it when the job is done, so five active projects sat behind thirty-odd finished ones in the Project dropdown of the Edit Archive dialog — an unscrolled list of everything ever created, with no way to tell which entries were still live. That dropdown, the one on the pending-uploads panel, the bulk "Add to Project" dialog and the File Manager's folder link now leave archived projects out. Completed projects stay: a project marked completed says the work is done, not that it should be hidden, and filing a reprint under one is ordinary. The Archives right-click submenu had gone the other way and offered active projects only, so a completed project was reachable from the Edit dialog and not from the menu beside it; all five surfaces now apply the same rule. Whatever an archive is already filed under stays on its own list whatever its status — a `