Skip to content

Commit 9644cdc

Browse files
committed
FIX: AMU pre-gate sensor names, apply_diff field preservation, code audit
- Fix pre-gate sensor prefix: "Mmu Pre Gate N" -> "mmu_pre_gate_N" - Fix apply_diff losing weight_g/remaining_weight/bed_temp/mid_usage on gate rebuild - Add endless_spool_groups to MMUState; wired in from_status + apply_diff - Fix _apply_spool_data: early return on None state, flatten guard, restore bounds check - Fix on_load_cell_update: add startswith guard, fix float(None) on force key
1 parent 7289b44 commit 9644cdc

3 files changed

Lines changed: 73 additions & 46 deletions

File tree

BlocksScreen/devices/amu/manager.py

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ def _apply_spool_data(self, gate: int, data: dict) -> None:
6767
Extracts material, color, weight from the Spoolman response dict,
6868
emits MMU_GATE_MAP gcode to sync Klipper, and updates the local GateInfo weight.
6969
"""
70+
if self._mmu_state is None:
71+
return
7072
filament = data.get("filament", {})
7173
material = filament.get("material", "")
7274
color = filament.get("color_hex", "")
@@ -84,32 +86,25 @@ def _apply_spool_data(self, gate: int, data: dict) -> None:
8486
filament_name=filament_name,
8587
temperature=temperature,
8688
)
87-
if self._mmu_state is not None:
88-
if gate >= len(self._mmu_state.gates):
89-
logger.warning(
90-
"Gate index %d out of range (%d gates)",
91-
gate,
92-
len(self._mmu_state.gates),
93-
)
94-
return
95-
gates = list(self._mmu_state.gates)
96-
updates = {}
97-
if weight is not None:
98-
updates["weight_g"] = float(weight)
99-
if remaining_weight is not None:
100-
updates["remaining_weight"] = float(remaining_weight)
101-
self._emit_speed_gcode(gate, remaining_weight)
102-
if filament_name:
103-
updates["filament_name"] = filament_name
104-
if temperature is not None:
105-
updates["temperature"] = float(temperature)
106-
if bed_temp is not None:
107-
updates["bed_temp"] = int(bed_temp)
108-
if updates:
109-
gates[gate] = dataclasses.replace(gates[gate], **updates)
110-
self._mmu_state = dataclasses.replace(
111-
self._mmu_state, gates=tuple(gates)
112-
)
89+
gates = list(self._mmu_state.gates)
90+
if gate >= len(gates):
91+
logger.warning("Gate index %d out of range (%d gate)", gate, len(gates))
92+
return
93+
updates = {}
94+
if weight is not None:
95+
updates["weight_g"] = float(weight)
96+
if remaining_weight is not None:
97+
updates["remaining_weight"] = float(remaining_weight)
98+
self._emit_speed_gcode(gate, remaining_weight)
99+
if filament_name:
100+
updates["filament_name"] = filament_name
101+
if temperature is not None:
102+
updates["temperature"] = float(temperature)
103+
if bed_temp is not None:
104+
updates["bed_temp"] = int(bed_temp)
105+
if updates:
106+
gates[gate] = dataclasses.replace(gates[gate], **updates)
107+
self._mmu_state = dataclasses.replace(self._mmu_state, gates=tuple(gates))
113108

114109
def _emit_speed_gcode(self, gate: int, remaining_weight: float) -> None:
115110
"""Emit MMU_GATE_MAP SPEED=x for the gate based on the spool weight profile."""
@@ -258,7 +253,7 @@ def eject_gate(self, gate: int) -> None:
258253
self.run_gcode_signal.emit(f"MMU_EJECT GATE={gate}")
259254

260255
def eject_all_gates(self, num_gates: int) -> None:
261-
"""Fully eject filament from all gates sequentially(amu_manager.get_state().num_gates)
256+
"""Fully eject filament from all gates sequentially
262257
263258
Args:
264259
num_gates: Total number of gates(from MMUState.num_gates)
@@ -303,10 +298,10 @@ def on_object_updated(
303298
self.on_load_cell_update(values, object_name)
304299

305300
def on_pre_gate_update(self, values: dict, name: str) -> None:
306-
if not name.startswith("Mmu Pre Gate "):
301+
if not name.startswith("mmu_pre_gate_"):
307302
return
308303
try:
309-
gate = int(name.removeprefix("Mmu Pre Gate "))
304+
gate = int(name.removeprefix("mmu_pre_gate_"))
310305
except ValueError:
311306
logger.error("Failed to parse Pre-Gate: %s", name)
312307
return
@@ -316,15 +311,15 @@ def on_pre_gate_update(self, values: dict, name: str) -> None:
316311

317312
def on_load_cell_update(self, values: dict, name: str) -> None:
318313
"""Update gate weight from a Klipper load_cell sensor reading"""
319-
if self._mmu_state is None:
314+
if self._mmu_state is None or not name.startswith("load_cell_mmu_"):
320315
return
321316
try:
322317
gate = int(name.removeprefix("load_cell_mmu_"))
323318
except ValueError:
324319
logger.error("Failed parsing %s Load cell", name)
325320
return
326321

327-
weight = float(values.get("force", 0))
322+
weight = float(values.get("force") or 0)
328323
if gate >= len(self._mmu_state.gates):
329324
logger.warning(
330325
"Gate index %d out of range (%d gates)",

BlocksScreen/devices/amu/models.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class MMUState:
7777
operation: str = ""
7878
sensors: dict = dataclasses.field(default_factory=dict)
7979
gate_speed_override: tuple[float, ...] = dataclasses.field(default_factory=tuple)
80+
endless_spool_groups: tuple[int, ...] = dataclasses.field(default_factory=tuple)
8081

8182
@property
8283
def is_paused(self) -> bool:
@@ -110,6 +111,10 @@ def from_status(cls, data: dict) -> "MMUState":
110111
filament_name = data.get("gate_filament_name", [""] * num_gates)
111112
temperature = data.get("gate_temperature", [None] * num_gates)
112113
speed_override = data.get("gate_speed_override", [])
114+
weight_g = data.get("gate_weight_g", [None] * num_gates)
115+
remaining_weight_list = data.get("gate_remaining_weight", [None] * num_gates)
116+
bed_temperature_list = data.get("gate_bed_temp", [None] * num_gates)
117+
mid_usage_list = data.get("gate_mid_usage", [False] * num_gates)
113118

114119
gates: tuple[GateInfo, ...] = tuple(
115120
GateInfo(
@@ -121,6 +126,10 @@ def from_status(cls, data: dict) -> "MMUState":
121126
spool_id=spool_ids[i],
122127
filament_name=filament_name[i],
123128
temperature=temperature[i],
129+
weight_g=weight_g[i],
130+
remaining_weight=remaining_weight_list[i],
131+
bed_temp=bed_temperature_list[i],
132+
mid_usage=mid_usage_list[i],
124133
)
125134
for i in range(num_gates)
126135
)
@@ -145,6 +154,7 @@ def from_status(cls, data: dict) -> "MMUState":
145154
operation=data.get("operation", ""),
146155
sensors=dict(data.get("sensors", {})),
147156
gate_speed_override=tuple(speed_override),
157+
endless_spool_groups=tuple(data.get("endless_spool_groups", [])),
148158
)
149159

150160
def gate_for_tool(self, tool: int) -> int:
@@ -187,6 +197,10 @@ def apply_diff(self, diff: dict) -> "MMUState":
187197
scalar_fields["spoolman_support"] = SpoolmanSupport(
188198
scalar_fields["spoolman_support"]
189199
)
200+
if "endless_spool_groups" in scalar_fields:
201+
scalar_fields["endless_spool_groups"] = tuple(
202+
scalar_fields["endless_spool_groups"]
203+
)
190204
return dataclasses.replace(self, **scalar_fields)
191205
# Gate arrays changed — need full rebuild, but we lost the raw arrays
192206
# Pass current gate data + diff into from_status
@@ -199,6 +213,10 @@ def apply_diff(self, diff: dict) -> "MMUState":
199213
"gate_filament_name": [g.filament_name for g in self.gates],
200214
"gate_speed_override": list(self.gate_speed_override),
201215
"gate_temperature": [g.temperature for g in self.gates],
216+
"gate_weight_g": [g.weight_g for g in self.gates],
217+
"gate_remaining_weight": [g.remaining_weight for g in self.gates],
218+
"gate_bed_temp": [g.bed_temp for g in self.gates],
219+
"gate_mid_usage": [g.mid_usage for g in self.gates],
202220
}
203221
merged = {**dataclasses.asdict(self), **gate_data, **diff}
204222
return MMUState.from_status(merged)

tests/amu/test_manager_unit.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,20 @@ def test_from_status_gate_speed_override_defaults_to_empty(self, manager) -> Non
156156
manager.update_mmu_state(_FULL_STATUS)
157157
assert manager.get_state().gate_speed_override == ()
158158

159+
def test_from_status_parses_endless_spool_groups(self, manager) -> None:
160+
data = {**_FULL_STATUS, "endless_spool_groups": [0, 1, 3,0]}
161+
manager.update_mmu_state(data)
162+
assert manager.get_state().endless_spool_groups == (0, 1, 3, 0)
163+
164+
def test_from_status_endless_spool_groups_defaults_to_empty(self, manager) -> None:
165+
manager.update_mmu_state(_FULL_STATUS)
166+
assert manager.get_state().endless_spool_groups == ()
167+
168+
def test_apply_diff_updates_endless_spool_groups(self, manager) -> None:
169+
manager.update_mmu_state(_FULL_STATUS)
170+
manager.update_mmu_state({"endless_spool_groups": [0, 0, 1, 1]})
171+
assert manager.get_state().endless_spool_groups == (0, 0, 1, 1)
172+
159173

160174
class TestGcodeSignals:
161175
def test_set_gate_info(self, manager, qtbot) -> None:
@@ -265,54 +279,54 @@ class TestPregateSensors:
265279
def test_pre_gate_happy_path(self, manager) -> None:
266280
manager.update_mmu_state(_FULL_STATUS)
267281
data: dict[str, bool] = {"filament_detected": True}
268-
manager.on_pre_gate_update(data, "Mmu Pre Gate 0")
269-
manager.on_pre_gate_update(data, "Mmu Pre Gate 1")
270-
manager.on_pre_gate_update(data, "Mmu Pre Gate 2")
271-
manager.on_pre_gate_update(data, "Mmu Pre Gate 3")
282+
manager.on_pre_gate_update(data, "mmu_pre_gate_0")
283+
manager.on_pre_gate_update(data, "mmu_pre_gate_1")
284+
manager.on_pre_gate_update(data, "mmu_pre_gate_2")
285+
manager.on_pre_gate_update(data, "mmu_pre_gate_3")
272286
assert manager.get_pre_gate_sensors() == {0: True, 1: True, 2: True, 3: True}
273287

274288
def test_pre_gate_emits_signal(self, manager, qtbot) -> None:
275289
with qtbot.waitSignal(manager.pre_gate_changed) as blocker:
276-
manager.on_pre_gate_update({"filament_detected": True}, "Mmu Pre Gate 0")
290+
manager.on_pre_gate_update({"filament_detected": True}, "mmu_pre_gate_0")
277291
assert blocker.args == [0, True]
278292

279293
def test_non_mmu_sensor_ignored(self, manager, qtbot) -> None:
280294
with qtbot.assertNotEmitted(manager.pre_gate_changed):
281295
manager.on_pre_gate_update({"filament_detected": True}, "Toolhead Sensor")
282296

283297
def test_pre_gate_stores_state(self, manager) -> None:
284-
manager.on_pre_gate_update({"filament_detected": True}, "Mmu Pre Gate 2")
298+
manager.on_pre_gate_update({"filament_detected": True}, "mmu_pre_gate_2")
285299
assert manager.get_pre_gate_sensors() == {2: True}
286300

287301
def test_pre_gate_filament_not_detected(self, manager) -> None:
288-
manager.on_pre_gate_update({"filament_detected": True}, "Mmu Pre Gate 0")
289-
manager.on_pre_gate_update({"filament_detected": False}, "Mmu Pre Gate 1")
290-
manager.on_pre_gate_update({"filament_detected": True}, "Mmu Pre Gate 2")
291-
manager.on_pre_gate_update({"filament_detected": False}, "Mmu Pre Gate 3")
302+
manager.on_pre_gate_update({"filament_detected": True}, "mmu_pre_gate_0")
303+
manager.on_pre_gate_update({"filament_detected": False}, "mmu_pre_gate_1")
304+
manager.on_pre_gate_update({"filament_detected": True}, "mmu_pre_gate_2")
305+
manager.on_pre_gate_update({"filament_detected": False}, "mmu_pre_gate_3")
292306
assert manager.get_pre_gate_sensors() == {0: True, 1: False, 2: True, 3: False}
293307

294308
def test_pre_gate_empty(self, manager) -> None:
295309
assert manager.get_pre_gate_sensors() == {}
296310

297311
def test_pre_gate_multiple_emits(self, manager, qtbot) -> None:
298312
with qtbot.waitSignal(manager.pre_gate_changed) as blocker:
299-
manager.on_pre_gate_update({"filament_detected": False}, "Mmu Pre Gate 0")
313+
manager.on_pre_gate_update({"filament_detected": False}, "mmu_pre_gate_0")
300314
assert blocker.args == [0, False]
301315
with qtbot.waitSignal(manager.pre_gate_changed) as blocker:
302-
manager.on_pre_gate_update({"filament_detected": True}, "Mmu Pre Gate 1")
316+
manager.on_pre_gate_update({"filament_detected": True}, "mmu_pre_gate_1")
303317
assert blocker.args == [1, True]
304318
with qtbot.waitSignal(manager.pre_gate_changed) as blocker:
305-
manager.on_pre_gate_update({"filament_detected": False}, "Mmu Pre Gate 2")
319+
manager.on_pre_gate_update({"filament_detected": False}, "mmu_pre_gate_2")
306320
assert blocker.args == [2, False]
307321
with qtbot.waitSignal(manager.pre_gate_changed) as blocker:
308-
manager.on_pre_gate_update({"filament_detected": True}, "Mmu Pre Gate 3")
322+
manager.on_pre_gate_update({"filament_detected": True}, "mmu_pre_gate_3")
309323
assert blocker.args == [3, True]
310324

311325
def test_pre_gate_update_is_dict(self, manager) -> None:
312326
assert isinstance(manager.get_pre_gate_sensors(), dict)
313327

314328
def test_klippy_disconnect_clears_pre_gate_sensors(self, manager) -> None:
315-
manager.on_pre_gate_update({"filament_detected": True}, "Mmu Pre Gate 0")
329+
manager.on_pre_gate_update({"filament_detected": True}, "mmu_pre_gate_0")
316330
manager.on_klippy_state("disconnect")
317331
assert manager.get_pre_gate_sensors() == {}
318332

0 commit comments

Comments
 (0)