mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
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.
67 lines
3.0 KiB
Python
67 lines
3.0 KiB
Python
"""Resolve an AMS slot's K value from the printer's calibration table.
|
|
|
|
H2-series trays carry no ``k`` field of their own — only ``cali_idx`` — so the
|
|
K value on the AMS slot card (#2854) is looked up from the printer's
|
|
calibration table in ``state.kprofiles``. That table is not flat: the printer
|
|
numbers it **per nozzle**, so entry 16 exists under every nozzle it holds
|
|
profiles for and means a different profile on each.
|
|
|
|
``state.kprofiles`` is the union across nozzle diameters (see
|
|
``BambuMQTTClient._store_kprofiles``), which is what the assign paths need but
|
|
makes ``cali_idx`` alone ambiguous. Resolution here is therefore:
|
|
|
|
1. the slot's own extruder, which separates the two nozzles of a dual-nozzle
|
|
machine outright;
|
|
2. failing that, the diameters currently installed, which separates a live
|
|
table from one left behind by a nozzle that has since been swapped out.
|
|
|
|
If both fail to single out one profile the answer is ``None``. A blank space on
|
|
the card is a smaller error than confidently printing the other nozzle's number.
|
|
"""
|
|
|
|
from collections.abc import Callable
|
|
|
|
from backend.app.utils.fts_routing import slot_extruder
|
|
|
|
|
|
def build_slot_k_resolver(state) -> Callable[[int | None, int, int], float | None]:
|
|
"""Return ``resolve(cali_idx, ams_id, tray_id) -> k value or None``.
|
|
|
|
Built once per serialization pass and closed over the state, so the REST
|
|
and WebSocket views of the same card cannot answer differently.
|
|
"""
|
|
# (extruder, cali_idx) -> {nozzle_diameter: k}. The inner dict is what
|
|
# detects the ambiguity: more than one entry means two nozzles' tables both
|
|
# claim this index on this extruder.
|
|
table: dict[tuple[int, int], dict[str, float]] = {}
|
|
for kp in getattr(state, "kprofiles", None) or []:
|
|
if kp.slot_id is None or not kp.k_value:
|
|
continue
|
|
try:
|
|
k_value = float(kp.k_value)
|
|
except (ValueError, TypeError):
|
|
continue # Skip K-profile entries with unparseable values
|
|
try:
|
|
extruder = int(kp.extruder_id or 0)
|
|
except (ValueError, TypeError):
|
|
extruder = 0
|
|
table.setdefault((extruder, kp.slot_id), {})[str(kp.nozzle_diameter or "")] = k_value
|
|
|
|
installed = {str(n.nozzle_diameter) for n in (getattr(state, "nozzles", None) or []) if n.nozzle_diameter}
|
|
|
|
def resolve(cali_idx: int | None, ams_id: int, tray_id: int) -> float | None:
|
|
if cali_idx is None:
|
|
return None
|
|
extruder = slot_extruder(ams_id, tray_id, state.ams_extruder_map, state.ams_switch_inlet)
|
|
# Single-nozzle printers report everything under extruder 0, and that
|
|
# is also the right default when the routing is simply unknown.
|
|
by_nozzle = table.get((extruder if extruder is not None else 0, cali_idx))
|
|
if not by_nozzle:
|
|
return None
|
|
if len(by_nozzle) == 1:
|
|
return next(iter(by_nozzle.values()))
|
|
live = [k for nozzle, k in by_nozzle.items() if nozzle in installed]
|
|
return live[0] if len(live) == 1 else None
|
|
|
|
return resolve
|