9 Commits
Author SHA1 Message Date
maziggy 9500c046c0 Ask which nozzle to feed when a Filament Track Switch is fitted
Load and Unload in the AMS slot menu did nothing on an H2C with the switch
fitted. The ams_change_filament command carries an optional extruder_id and
Bambuddy never sent it. That is correct on every printer without the switch,
and is what BambuStudio does there too -- each AMS is wired to one hotend, so
the firmware works the target out for itself and an explicit value would only
be a guess at something it already knows. Fit the switch and every AMS is
bound to one of its two inlets instead, either hotend is reachable from any
slot, and a command naming neither leaves the firmware nothing to act on. It
was discarded in silence.

Load now asks which hotend to feed, on the same terms as Bambu Studio: no
preselection, so a stray Enter cannot feed the wrong one, and the hotend
already fed from that very slot greyed out. Printers without a switch send a
byte-identical command and still load in one click. A switch fitted but not
yet set up -- any AMS still unassigned to an inlet -- refuses the load up
front rather than publishing one the firmware will drop, mirroring
DevFilaSwitch::IsReady, which likewise demands a switcher position on every
AMS.

Unload was addressed at the same time. It was aimed with tray_now, a single
value for the whole printer, so on any dual-nozzle machine with both hotends
loaded it unloaded whichever that field happened to name regardless of which
slot's menu was used. It now names the slot and resolves the holding hotend
from device.extruder.info, previously read for temperatures only. That
resolution is gated on the printer having reported two extruders:
single-nozzle machines do send the block, but nobody has read a single-nozzle
snow value off the wire, and staking every X1C, P1S and A1 unload on an
unverified encoding buys nothing where tray_now is already unambiguous.

Both new state fields ride the WebSocket and are in the broadcast key, and
both are computed in the REST status route as well -- that response is what
the page has before any push arrives, and leaving them at their defaults
would have told a correctly set-up machine that its switch was not set up.

Verified on H2C-1, AMS-A slot 3: loaded and unloaded from each hotend in
turn, all four correct. Covered by 18 MQTT unit tests, 4 status-dict tests,
7 integration tests and 6 component tests.

Two known stragglers, both deliberately left alone. Load on an AMS-HT slot
has never worked -- an HT unit is addressed by its unit id rather than
ams*4+slot, which these endpoints do not accept -- so unload there keeps the
printer-wide form it always used instead of gaining a slot it cannot name.
And a slot's K-profile still follows the AMS's plumbing rather than the
nozzle just loaded, so loading to the far hotend leaves the other one's
calibration bound; that is the same per-nozzle problem the filament and
K-profile redesign is scoped to fix.
2026-08-26 12:04:42 +02:00
maziggy 14f46510ba File the printer's calibration table under the nozzle it belongs to (issue #2854)
The K value on an AMS slot card went blank after a while and came back after a
backend restart. It is not the MQTT merge: that preserves a tray's k correctly.
H2-series trays have no k to preserve. Verified against the H2 wire capture in
logs/vp_wire -- every tray reports cali_idx and nothing else -- so the number on
the card is resolved from that index against the printer's calibration table in
state.kprofiles, and that table was a single global list.

An extrusion_cali_get response is the complete table for one nozzle diameter,
and the printer answers whoever asks; BambuStudio's queries arrive on the same
report topic we subscribe to. Every response was assigned straight to
state.kprofiles, so any one answer stood for the whole printer. The nightly
GitHub backup asks for 0.2, 0.4, 0.6 and 0.8 in turn and finishes on 0.8, which
holds nothing on a 0.4+0.6 machine: logs/bambuddy.log records exactly that at
17:15 on 2026-08-25, and the table was empty from then until something refilled
it. Responses are now bucketed by the diameter they describe, so an empty answer
for a size the printer does not have clears only that size. The three assign
paths that look an index up by nozzle_diameter get the same fix for free -- they
were quietly finding nothing whenever the last response was for another nozzle,
which is what spoolman_inventory has been logging as a stale kp.

Bucketing makes state.kprofiles a union, and cali_idx is numbered per nozzle, so
the index alone no longer identifies a profile. The REST serializer has keyed on
(extruder, cali_idx) since c5e005586; the WebSocket one still keyed on the index
alone, which meant the first render of a card could be right and every update
after it wrong. Both now share one resolver. It goes through the extruder the
slot feeds, and where that does not single out one profile -- a single-nozzle
printer that has been swapped, so both its tables sit under extruder 0 -- it
falls back to which diameters are actually fitted. Where neither settles it the
card shows nothing, because a blank space is a smaller error than confidently
printing the other nozzle's number. Deliberately no loosening to a bare cali_idx
lookup on a miss: that is the cross-nozzle bleed the extruder keying was added
to stop.

Nothing read the table on connect, which is the other half of the report. It
arrived by luck -- a visit to Profiles or Configure Slot, a backup, or the
printer answering someone else -- so a Bambuddy nobody had opened showed a card
with no K values at all, and "restart and they come back" was the printer
happening to broadcast rather than anything we did. It is now read once per
connection, on the same latch the stale-print reconcile uses. Only the fitted
diameters are asked for, one request on a single-nozzle printer and two on a
dual; probing the four sizes blind is what the backup does and what blanked the
table. The edge is gated on a nozzle diameter being known as well as on the
state being known, because the first push_status is what makes the state known
and does not always carry the nozzle fields -- latching there would spend the
connection's one attempt on a printer that could not yet say what was fitted.

Adopting an unsolicited table now logs at debug. It was the quietest way for the
card to change underneath us and there was no way to see it in a support bundle.

Three test files gained nozzles=[] on their PrinterState stubs. The field has
always been on the dataclass; the connect edge is simply the first thing on that
path to read it.
2026-08-25 17:52:02 +02:00
maziggy 7a42e0a7e5 Show which Filament Track Switch inlet each AMS feeds
With a switch fitted, an AMS is not wired to a nozzle any more. It is
plumbed into one of the switch's two inlets and reaches both nozzles
through it, so every unit reports its extruder as "not fixed" (0xE) and
ams_extruder_map comes back empty on these machines.

The printer card had nothing to fall back on but the AMS unit number, so
AMS-A was badged R and AMS-B was badged L purely because their unit ids
are 0 and 1, a third unit got no badge at all, and every one of those
labels was wrong. The SpoolBuddy assign modal had the same fallback in a
worse form, mapping anything that was not extruder 1 to R.

The binding turned out to need no new telemetry. BambuStudio reads it out
of bits 24-27 of the same AMS info string we already parse for the AMS
type and the extruder id -- 0 is In-B, 1 is In-A -- and it is only
meaningful when a switch is installed, because without one 0xE really
does mean an uninitialised unit and those bits carry nothing. That gates
the read, which in turn forced the switch block to be parsed before the
AMS block: _handle_ams_data runs early in _process_message and
_update_state only much later, so the binding was lost on every frame
that carried both. _parse_fila_switch is split out and called first, and
left in _update_state as well so that stays a complete absorb step.

The badge keeps L and R rather than A and B, because the lettering is
familiar and matches the physical layout. It is a different colour from
the plain nozzle badge, and its tooltip names the inlet in full, since
the letter is the inlet's position and not a claim about which nozzle
that AMS feeds -- the switch can route either inlet to either outlet. An
AMS still reporting a real extruder id keeps its ordinary badge, which
BambuStudio also treats as authoritative over any switch binding, and a
switch that has been fitted but not yet set up on the printer shows
nothing rather than a guess.

The print dialog's slot dropdown gets the same label. It replaces a
left/right hint that never once rendered: ftsExtruderForSlot compared
snow-encoded in[] values against global tray ids and could not match.
Decoding it correctly would not have saved it -- the firmware reports
which slot sits in each inlet and which nozzle each outlet feeds, but
never which inlet is currently paired with which outlet, so no per-slot
nozzle can be derived. That function is gone rather than fixed.

The dialog also points out when every filament a print needs sits behind
one inlet. Bambu's own guidance is that this is legal but slow: a change
between two filaments on the same inlet retracts the outgoing spool all
the way back to its AMS before the next can be fed up the shared tube,
where a change across the two inlets only retracts as far as the switch.
All on one inlet means every change in the job takes the slow path, and
moving a single spool fixes it. So it advises, it does not block.

Both views update live. Two things were stopping that. fila_switch and
ams_switch_inlet were absent from printer_state_to_dict, and the frontend
shallow-merges each WebSocket push over its cached status, so a field the
push omits keeps whatever the last full fetch left behind. And the
broadcast dedup key had no term for either, so "Join IN-B" on the printer
screen moved nothing: the binding is not in the tray component of that
key, and it is not in the AMS change-hash either, which covers tray
fields only and must stay that way because it drives Spoolman sync.

Assigning an AMS to an inlet remains printer-side. BambuStudio can read
the binding and has no command to write it -- its switch class is parse
and getters only, and the recommended-arrangement popup draws and
publishes nothing -- so there is no wire format for us to copy.

Adding the two fields to PrinterState broke four test modules whose
SimpleNamespace stubs predate them. The stubs are fixed rather than the
production reads made defensive: the real dataclass always carries both,
and a getattr in the dedup key would silently stop tracking the field if
it were ever renamed.
2026-08-16 15:09:00 +02:00
maziggy bf525661d3 Three fixes on top of the billing branch, all found by running the suite against
both dialects rather than one.

Postgres upgrades never got as far as the finance schema.

  database.py added on_billing_charge_failed with BOOLEAN DEFAULT 1. The 1 is a
  SQLite-ism; Postgres answers DatatypeMismatchError, and _safe_execute
  deliberately re-raises anything that is not an idempotency error, so
  run_migrations died there and rolled the whole transaction back. No finance
  tables, no columns, and the app does not start. Six lines above, the same
  change gets is_voided right with an is_sqlite() branch, so this was an
  oversight rather than a decision. Now branched the same way.

  This also explains the four test_security.py::TestBackupKeyFiles failures
  reporting "column print_archives.cost_center_id does not exist". That column's
  migration exists and works -- it simply never ran, because every startup
  aborted before committing. Reproduced against Postgres 16 by building a
  pre-billing schema from dev and upgrading over it: fails without this,
  completes with it, and re-running the migrations or starting from an empty
  database are both clean.

test_billing_run_id_migration.py failed on any Postgres-configured checkout.

  It builds its own SQLite engine, but run_migrations branches on the global
  dialect rather than the connection in hand, so on a box whose DATABASE_URL
  points at Postgres it emitted md5(random()::text) and btrim() into SQLite.
  Given the same fixture test_ldap_migration.py already carries for exactly this
  reason. The suite now agrees across dialects -- 9190 passed either way, where
  it used to be 9184 on one and 9183 on the other.

The kill switch could not tell a print Bambuddy started from one it merely
watched.

  Authorization fell back to a print_archives row in status="printing" matched on
  subtask_id. But on_print_start archives every print it observes, including ones
  started from Bambu Studio or Handy, and stamps them with the same status and
  subtask_id -- the code says as much where it notes "a print Bambuddy didn't
  dispatch". So a foreign print became authorized the moment its 3MF finished
  downloading, and _active_prints was rehydrated from it, making that permanent.
  The switch fired only inside the download race, and never afterwards. Neither
  test caught it: one stubs the authorization call to False, the other stubs the
  query to return an archive, so the real lookup was never exercised against a
  foreign print.

  Authorization now requires a marker Bambuddy writes itself: billing_run_id,
  minted per dispatch in the scheduler, or created_by_id carried over from the
  queue item. Failing that, it looks for a queue row in status="printing" on that
  printer -- committed before the MQTT send, and the only durable trace a
  library-file dispatch leaves, since those have no archive at send time and the
  row created for them moments later carries neither marker. That row cannot be
  tied to a subtask_id, so it defers rather than authorizes.

  Deferring also closes a false positive the previous version shared: a restart
  in the window between the send and the download left no archive at all, and a
  Bambuddy print was stopped as unauthorized. Stopping a print is irreversible
  and declining to act costs a log line, so ambiguity resolves that way.

  Tests cover an unmarked archive not being authorization and not entering
  _active_prints, either marker alone authorizing and rehydrating the fast path
  without touching the queue, an unmarked archive with a live dispatch deferring,
  a dispatch not yet archived deferring, and nothing at all being unauthorized.
2026-08-08 11:19:33 +02:00
behrinml dd1d40b0d4 implemented pr (worth fixing) feedback
update commit
2026-08-06 20:27:25 +02:00
behrinml 9a397f46e9 implemented pr feedback #2 2026-08-05 21:25:46 +02:00
behrinml b74862d7d2 PR feedback integrated 2026-07-30 21:35:01 +02:00
behrinml 0d6a28cd67 Merge remote-tracking branch 'upstream/dev' into feature/billing 2026-07-30 20:40:28 +02:00
behrinml 0be6f6d731 added tests for finance/cost center functionality 2026-06-24 11:01:16 +02:00