mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
Name an unnamed print stage "Preparing" on the card
New printers report stage numbers before Bambuddy learns their names, and the H2C still has several. Those reached the printer card verbatim, as "Unknown stage (72)" -- a number that means nothing to the person reading it, on the one line that otherwise says what the printer is doing. Every stage that has turned out to be unnamed so far has been part of the run-up to printing, so an unnamed one now reads as "Preparing". That is the same literal stage 74 already carries rather than a second spelling of the same idea, so a card cannot show two different words for the same situation depending on which number the firmware picked. Display only, and deliberately not pushed down into get_stage_name. That function also feeds the stage-transition log line and the once-per-session warning added to capture unnamed stages so they can be named in a later release; there the number is the entire diagnostic value, and replacing it with "Preparing" would hide the only thing that reports these. Both paths are pinned by tests asserting they disagree for an unnamed stage and agree for a named one, so a later tidy-up cannot quietly collapse them. The idle sentinels are untouched: 255 on A1/P1 and -1 on X1 mean "no stage", not an unnamed one, and still resolve to nothing rather than being swept up by the fallback. Nothing keys logic off stg_cur_name -- it is display-only in the printer card, the print dialog's printer selector and the stream overlay -- so all three improve and none change behaviour. No i18n either way: the whole stage table has always been English.
This commit is contained in:
@@ -25,6 +25,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **The Windows installer build is split in two so a signing request can wait for a human (SignPath Foundation)** — Release tags are Authenticode-signed through the SignPath Foundation OSS programme, and the production certificate does not sign on demand the way the self-signed test certificate does: every request has to be approved by hand in the SignPath UI, because the Foundation verifies what is being signed and which build it came from. The submitting action waits for that approval with a default timeout of 600 seconds, which is ample when the test policy approves automatically in seconds and far too short once the wait is a person noticing a tag went out. A tag pushed at night would have failed the run ten minutes later with the installer already compiled and thrown away. The compile now ends in its own job that uploads the unsigned artifact and stops; a second job downloads it, signs it, and does the release-facing work, with the wait raised to an hour. Because the artifact is uploaded before the wait begins and is addressed by id, a missed approval window is recovered by re-running the second job alone rather than rebuilding the installer — which is the reason to separate them rather than simply raise the timeout in place. The second job runs for unsigned builds too, so the daily prereleases that are deliberately left unsigned to preserve the signing quota keep going out through exactly one set of alias, artifact and release steps. The property that matters is unchanged and now recorded next to the steps that depend on it: none of the alias, upload or release-attach steps carry `always()`, so GitHub skips all three when signing fails or times out, and an unsigned `.exe` cannot reach a release. Nothing about the signed output changes, and the restructure behaves identically under the test policy — the request simply completes immediately instead of waiting — so it can be proven green before the production certificate arrives.
|
||||
|
||||
### Fixed
|
||||
- **A printer card said "Unknown stage (72)" where it now says "Preparing"** — New models report stage numbers before Bambuddy learns their names, and the H2C still has several. Until now those reached the card verbatim, as a number that means nothing to the person reading it, on a line that otherwise names what the printer is doing. Every stage that has turned out to be unnamed so far has been part of the run-up to printing, so an unnamed one now reads as "Preparing" — the same label stage 74 already carries, rather than a second spelling of the same idea. The substitution is display-only and deliberately not pushed down into `get_stage_name`: that function also feeds the stage-transition log line and the once-per-session warning that exists precisely to capture unnamed stages so they can be named in a later release, and there the number is the entire diagnostic value. Both paths are pinned by tests that assert they disagree for an unnamed stage and agree for a named one, so a future tidy-up cannot quietly collapse them and blind the thing that reports these. The idle sentinels (255 on A1/P1, -1 on X1) still resolve to no stage at all rather than being swept up as unnamed.
|
||||
- **An AMS slot card showed a multi-colour spool as one flat band (#2967, reported by @NeighborGeek)** — A Ziro "Colorful Mist" — yellow, cyan and pink, effect Tri Color — hovered on the printer card as a single pink rectangle, because a printer reports exactly one `tray_color` hex per tray and nothing else. Telemetry cannot describe a gradient or a surface effect and never will, so the header now paints the *spool's* own swatch whenever the bound spool carries extra colour stops or an effect, through the same builder the Inventory swatches use — the two surfaces cannot drift because there is one implementation. A plain single-colour spool keeps the flat colour it has always had, so the common case goes nowhere near the gradient path. Any stop at all counts, not just two: the colour layer ignores the base hex the moment stops exist, so a one-stop spool renders that stop rather than the slot's hex, and honouring it is what keeps the card agreeing with Inventory. The colour name and the print dialog's slot dropdown were the other two halves of the report and were already fixed on dev by #2875 and the slot-naming change that landed the day after it was filed. **Spoolman mode gained the gradient in the process**: Spoolman holds the extra stops in `multi_color_hexes` and the label renderer had been reading them for releases, but `_map_spoolman_spool` never returned them, so the identical roll registered in Spoolman rendered flat while the internally-managed one did not. Both now share one parser rather than reading the same field two ways. The asymmetry that remains is Spoolman's own and is pinned by a test rather than left to be rediscovered: it has no field for a surface effect — its only neighbouring field, `multi_color_direction`, describes how the stops are laid out, not that the roll is silk — so `effect_type` is None there instead of guessed at. The colour name moves onto the same scrim the vendor badge already uses once the background has more than one band, because a single hex cannot decide legibility across yellow, cyan and pink and the label sits dead centre where the background is likeliest to change under it; a single stop or an effect over one colour leaves a real base colour to test, and keeps the contrast rule it had. Wiki updated. 19 backend and 8 frontend regression tests.
|
||||
- **Everything the internal slicer produced was Bambu green, whatever filament was picked (#2977, reported by @fadudba)** — A slice through Bambuddy's own slicer came out with `filament_colour = #00AE42` every time: a green plate thumbnail regardless of the profile chosen, and a **Color mismatch** in the Print dialog against the AMS slot the job had just been correctly mapped to. The reason is that a colour is not a property of a filament *preset* in either slicer — it belongs to the project, and their GUIs set it from the plate — so nothing was attached to the preset Bambuddy sent by name and the CLI fell back to its own compiled-in default, which is Bambu green. Verified against a 02.08.02.61 sidecar with the reporter's exact triplet: as sent, `['#00AE42']`; with a colour written onto the same profile, the colour asked for. Each filament row in the slice dialog now carries an editable **colour swatch**, and the colour reaches both `project_settings.config` and `slice_info.config`, which is what the thumbnail and the AMS mapping actually read. The swatch is pre-filled from the colour that slot was designed with — read from the source 3MF's own project settings — then the preset's `default_filament_colour`, then the slicer's green. It is offered on single-filament sources too, because an **STL, and equally a mesh-only 3MF exported from CAD, has no colour anywhere else to inherit**; measured, a colourless source records `color="#00AE42"` in its own slice info, so a fallback that read the *last sliced* colour rather than the *designed* one would have been circular. `default_filament_colour` is deliberately not treated as the answer on its own: the CLI never reads it — a profile carrying only that still slices green — it is consumed by the GUI when a project is created, so it is read and rewritten as `filament_colour`, which the CLI does honour. Bambu's bundled filament profiles define it nowhere at all (zero occurrences across the whole shipped tree), which is why it can only be one link in the chain. A slot the user did not touch and that has no designed colour submits an empty string rather than the swatch's displayed default, because a sent colour outranks the preset's own and pinning the placeholder would silently discard the real colour of an imported OrcaSlicer profile that carries one. Slicer Pipelines pick up the same chain without carrying a swatch of their own. The control sits beside the filament dropdown, styled like it and the same height, showing the swatch and its hex together. That placement is the third attempt and the first that reads as a control: a bare swatch in the label row looked exactly like the read-only dot multi-colour rows had carried for releases, and adding the hex beside it only made it look like a caption on the label — so on a single-filament STL, the one source with no colour to inherit and therefore the case the control exists for, nothing suggested anything was settable. The swatch and the hex are wrapped in one label bound to the input, so a click anywhere on it opens the picker rather than only a 16px dot being live. The colour is also painted on the input directly rather than relying only on the browser's native colour-swatch pseudo-element, since an unlit swatch is indistinguishable from no swatch at all. Translated in all 14 locales, wiki updated, and covered by 38 backend and 14 frontend regression tests.
|
||||
- **A filament preset the slicer could not resolve was sliced as PLA at 200 °C without saying so** — Found while investigating #2977. Bambuddy names the preset to inherit and the sidecar resolves it against its bundled profile tree; when that tree does not contain the name, nothing rejects it. The CLI inherits nothing, falls back to its compiled-in defaults for every field, and returns a well-formed success — so a PETG preset whose name a sidecar image predates prints at PLA temperatures with no diagnostic anywhere. Measured against a 02.08.02.61 sidecar: an unresolvable name slices as `filament_type ["PLA"]` at `nozzle_temperature ["200"]` with `filament_ids [""]` and `filament_vendor ["(Undefined)"]`. Bambuddy now recognises that pair and logs a warning naming the slot and the preset that was picked, pointing at the sidecar image as the fix. The file is **kept rather than refused**, unlike the missing start G-code of #2838: this one prints, it is only wrong, and someone may well be slicing deliberately with a profile their sidecar predates — so the choice is theirs to make with the temperatures in front of them. Both signals are required together, which is what keeps it from firing on the two legitimate cases that look similar: a hand-written profile that simply never named a vendor still carries a real filament id, and a user's own cloud preset carries a vendor while legitimately having no bundled id.
|
||||
|
||||
@@ -8,7 +8,13 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.models.printer import Printer
|
||||
from backend.app.services.bambu_mqtt import BambuMQTTClient, MQTTLogEntry, PrinterState, get_stage_name
|
||||
from backend.app.services.bambu_mqtt import (
|
||||
STAGE_NAMES,
|
||||
BambuMQTTClient,
|
||||
MQTTLogEntry,
|
||||
PrinterState,
|
||||
get_stage_name,
|
||||
)
|
||||
from backend.app.utils.kprofile_lookup import build_slot_k_resolver
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1170,6 +1176,23 @@ def get_derived_status_name(state: PrinterState, model: str | None = None) -> st
|
||||
# X1 models use -1 for idle, A1/P1 models use 255 for idle
|
||||
# Valid stage numbers are 0-254
|
||||
if 0 <= state.stg_cur < 255:
|
||||
# A stage number the table does not cover is named "Preparing" rather
|
||||
# than "Unknown stage (72)". New models report stages before Bambuddy
|
||||
# learns their names -- the H2C still has several -- and the card is
|
||||
# the wrong place to say so: the number means nothing to the person
|
||||
# reading it, and every stage that has ever turned out to be unnamed
|
||||
# was part of the run-up to printing, so "Preparing" is both the more
|
||||
# useful answer and the more likely one.
|
||||
#
|
||||
# This is display only, and deliberately not pushed down into
|
||||
# `get_stage_name`. That function also feeds the stage-transition log
|
||||
# line and the once-per-session warning that exists precisely to
|
||||
# capture unnamed stages so they can be named later (bambu_mqtt.py
|
||||
# ~4100) -- there the number is the entire diagnostic value, and
|
||||
# replacing it with "Preparing" would hide the very thing that
|
||||
# reports these.
|
||||
if state.stg_cur not in STAGE_NAMES:
|
||||
return "Preparing"
|
||||
return get_stage_name(state.stg_cur)
|
||||
|
||||
# If not in RUNNING state, no derived status needed
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""A stage number Bambuddy cannot name reads as "Preparing" on the card.
|
||||
|
||||
New printers report stages before Bambuddy learns their names -- the H2C still
|
||||
has several -- and until now those reached the card as ``Unknown stage (72)``.
|
||||
The number means nothing to the person reading it, and the card is not where it
|
||||
belongs: every stage that has turned out to be unnamed so far was part of the
|
||||
run-up to printing, so "Preparing" is both the more useful answer and the more
|
||||
likely one.
|
||||
|
||||
The substitution is display-only. ``get_stage_name`` still reports the number,
|
||||
because it also feeds the stage-transition log line and the once-per-session
|
||||
warning that exists precisely to capture unnamed stages so they can be named in
|
||||
a later release. Replacing the number there would hide the only thing that
|
||||
reports these -- these tests pin that separation, not just the new label.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.app.services.bambu_mqtt import STAGE_NAMES, get_stage_name
|
||||
from backend.app.services.printer_manager import get_derived_status_name
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
class _State:
|
||||
"""Minimal PrinterState stand-in: only the fields this path reads."""
|
||||
|
||||
def __init__(self, stg_cur: int, state: str = "RUNNING") -> None:
|
||||
self.stg_cur = stg_cur
|
||||
self.state = state
|
||||
self.temperatures: dict = {}
|
||||
self.progress = 0
|
||||
self.layer_num = 0
|
||||
|
||||
|
||||
# Two numbers no firmware in the table uses. 72 is the one reported from an H2C
|
||||
# in the field (#2916's logging change was added for exactly this gap).
|
||||
UNNAMED = [n for n in (68, 70, 71, 72, 73, 75, 76, 80, 200, 254) if n not in STAGE_NAMES]
|
||||
|
||||
|
||||
class TestTheCard:
|
||||
@pytest.mark.parametrize("stage", UNNAMED)
|
||||
def test_an_unnamed_stage_reads_as_preparing(self, stage):
|
||||
assert get_derived_status_name(_State(stage)) == "Preparing"
|
||||
|
||||
def test_it_never_shows_the_raw_number(self):
|
||||
for stage in UNNAMED:
|
||||
assert "Unknown" not in (get_derived_status_name(_State(stage)) or "")
|
||||
|
||||
@pytest.mark.parametrize("stage", sorted(STAGE_NAMES))
|
||||
def test_every_named_stage_keeps_its_own_name(self, stage):
|
||||
# The substitution must not swallow the table it is standing in for.
|
||||
assert get_derived_status_name(_State(stage)) == STAGE_NAMES[stage]
|
||||
|
||||
def test_the_label_matches_the_one_the_table_already_uses(self):
|
||||
# Stage 74 is "Preparing" in STAGE_NAMES. An unnamed stage renders the
|
||||
# same string rather than a second spelling of the same idea.
|
||||
assert get_derived_status_name(_State(74)) == get_derived_status_name(_State(UNNAMED[0]))
|
||||
|
||||
def test_a_paused_printer_is_covered_too(self):
|
||||
assert get_derived_status_name(_State(UNNAMED[0], state="PAUSE")) == "Preparing"
|
||||
|
||||
|
||||
class TestTheIdleSentinelsAreUntouched:
|
||||
"""255 and -1 are "no stage", not unnamed stages, and must stay None."""
|
||||
|
||||
def test_a1_p1_idle_sentinel(self):
|
||||
assert get_derived_status_name(_State(255, state="IDLE")) is None
|
||||
|
||||
def test_x1_idle_sentinel(self):
|
||||
assert get_derived_status_name(_State(-1, state="IDLE")) is None
|
||||
|
||||
def test_the_sentinels_stay_none_even_while_running(self):
|
||||
# Out of range on both sides of 0..254; the temperature fallback below
|
||||
# has nothing to work with here, so the answer is still no stage.
|
||||
assert get_derived_status_name(_State(255)) is None
|
||||
|
||||
|
||||
class TestTheDiagnosticPathStillReportsTheNumber:
|
||||
@pytest.mark.parametrize("stage", UNNAMED)
|
||||
def test_get_stage_name_still_names_the_number(self, stage):
|
||||
# This is what the log line and the unnamed-stage warning print. If it
|
||||
# ever starts saying "Preparing", nobody can tell which stage to add.
|
||||
assert get_stage_name(stage) == f"Unknown stage ({stage})"
|
||||
|
||||
def test_the_two_paths_genuinely_disagree_for_an_unnamed_stage(self):
|
||||
stage = UNNAMED[0]
|
||||
assert get_derived_status_name(_State(stage)) != get_stage_name(stage)
|
||||
|
||||
def test_and_agree_for_a_named_one(self):
|
||||
assert get_derived_status_name(_State(74)) == get_stage_name(74)
|
||||
Reference in New Issue
Block a user