Skip to content

Commit 906632e

Browse files
committed
add: pre-gates and klippy callback system and start integrate Spoolman support
1 parent f504bd9 commit 906632e

8 files changed

Lines changed: 281 additions & 65 deletions

File tree

BlocksScreen/devices/amu/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@
1010

1111
from .manager import AMUManager
1212
from .models import (
13+
FilamentPos,
1314
GateInfo,
1415
GateStatus,
1516
MMUState,
17+
SpoolmanSupport,
1618
)
1719

1820
__all__: list[str] = [
1921
# manager
2022
"AMUManager",
2123
# models
24+
"FilamentPos",
2225
"GateInfo",
2326
"GateStatus",
2427
"MMUState",
28+
"SpoolmanSupport",
2529
]

BlocksScreen/devices/amu/manager.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,15 @@ class AMUManager(QtCore.QObject):
3636
bool, name="amu-toggled"
3737
)
3838

39+
pre_gate_changed: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal(
40+
int, bool, name="pre-gate-changed"
41+
)
42+
3943
def __init__(self, parent: QtCore.QObject | None = None) -> None:
4044
super().__init__(parent)
4145
self._amu_state = False
4246
self._mmu_state: MMUState | None = None
47+
self._pre_gate_sensors: dict[int, bool] = {}
4348
self.__setup_configfile()
4449

4550
def __setup_configfile(self) -> None:
@@ -107,6 +112,9 @@ def get_state(self) -> MMUState | None:
107112
"""
108113
return self._mmu_state
109114

115+
def get_pre_gate_sensors(self) -> dict[int, bool]:
116+
return dict(self._pre_gate_sensors)
117+
110118
def is_amu_active(self) -> bool:
111119
"""Returns whether AMU includes are currently uncommented in printer.cfg"""
112120
return self._amu_state
@@ -171,7 +179,6 @@ def load_gate(self, gate: int) -> None:
171179

172180
def unload(self) -> None:
173181
"""Unload the currently loaded filament by sending MMU_UNLOAD."""
174-
...
175182
self.run_gcode_signal.emit("MMU_UNLOAD")
176183

177184
def eject_gate(self, gate: int) -> None:
@@ -188,7 +195,7 @@ def eject_all_gates(self, num_gates: int) -> None:
188195
Args:
189196
num_gates: Total number of gates(from MMUState.num_gates)
190197
"""
191-
cmd = "\n".join(f"MMU_EJECT GATE={i}" for i in range(num_gates))
198+
cmd: str = "\n".join(f"MMU_EJECT GATE={i}" for i in range(num_gates))
192199
self.run_gcode_signal.emit(cmd)
193200

194201
def select_tool(self, tool: int) -> None:
@@ -199,6 +206,18 @@ def select_tool(self, tool: int) -> None:
199206
"""
200207
self.run_gcode_signal.emit(f"MMU_CHANGE_TOOL TOOL={tool}")
201208

209+
def on_pre_gate_update(self, values: dict, name: str) -> None:
210+
if not name.startswith("Mmu Pre Gate "):
211+
return
212+
try:
213+
gate = int(name.removeprefix("Mmu Pre Gate "))
214+
except ValueError:
215+
logger.error("Failed to parse Pre-Gate: %s", name)
216+
return
217+
detected = bool(values.get("filament_detected", False))
218+
self._pre_gate_sensors[gate] = detected
219+
self.pre_gate_changed.emit(gate, detected)
220+
202221
def update_mmu_state(self, data: dict, name: str = "") -> None:
203222
"""Receive an MMU status dict from Moonraker and update internal state.
204223
@@ -216,7 +235,7 @@ def update_mmu_state(self, data: dict, name: str = "") -> None:
216235
self._mmu_state = self._mmu_state.apply_diff(data)
217236
self.mmu_state_changed.emit(self._mmu_state)
218237

219-
220-
if __name__ == "__main__":
221-
manager = AMUManager()
222-
print(manager.toggle_amu_system(True))
238+
def on_klippy_state(self, state: str) -> None:
239+
"""React to changes in klippy states"""
240+
if state.lower() != "ready":
241+
self._mmu_state = None

BlocksScreen/devices/amu/models.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dataclasses
22
from dataclasses import dataclass
3-
from enum import IntEnum
3+
from enum import IntEnum, StrEnum
44

55

66
class GateStatus(IntEnum):
@@ -10,6 +10,32 @@ class GateStatus(IntEnum):
1010
EMPTY = -1
1111

1212

13+
class FilamentPos(IntEnum):
14+
"""State-machine position of the filament inside the MMU/extruder path."""
15+
16+
UNKNOWN = -1
17+
UNLOADED = 0
18+
HOMED_GATE = 1
19+
START_BOWDEN = 2
20+
IN_BOWDEN = 3
21+
END_BOWDEN = 4
22+
HOMED_ENTRY = 5
23+
HOMED_EXTRUDER = 6
24+
EXTRUDER_ENTRY = 7
25+
HOMED_TS = 8
26+
IN_EXTRUDER = 9
27+
LOADED = 10
28+
29+
30+
class SpoolmanSupport(StrEnum):
31+
"""Level of Spoolman integration configured in Happy-Hare."""
32+
33+
OFF = "off"
34+
READONLY = "readonly"
35+
PUSH = "push"
36+
PULL = "pull"
37+
38+
1339
@dataclass(frozen=True, slots=True)
1440
class GateInfo:
1541
index: int
@@ -33,9 +59,12 @@ class MMUState:
3359
tool: int
3460
gate: int
3561
filament: str # "Loaded" | "Unloaded" | "Unknown"
62+
filament_pos: FilamentPos
3663
action: str
3764
print_state: str
3865
reason_for_pause: str
66+
has_bypass: bool
67+
spoolman_support: SpoolmanSupport
3968
gates: tuple[GateInfo, ...]
4069
ttg_map: tuple[int, ...]
4170

@@ -87,9 +116,12 @@ def from_status(cls, data: dict) -> "MMUState":
87116
tool=data.get("tool", -1),
88117
gate=data.get("gate", -1),
89118
filament=data.get("filament", "Unknown"),
119+
filament_pos=FilamentPos(data.get("filament_pos", FilamentPos.UNKNOWN)),
90120
action=data.get("action", ""),
91121
print_state=data.get("print_state", ""),
92122
reason_for_pause=data.get("reason_for_pause", ""),
123+
has_bypass=data.get("has_bypass", False),
124+
spoolman_support=SpoolmanSupport(data.get("spoolman_support", SpoolmanSupport.OFF)),
93125
gates=gates,
94126
ttg_map=tuple(data.get("ttg_map", [])),
95127
)
@@ -117,12 +149,16 @@ def apply_diff(self, diff: dict) -> "MMUState":
117149
"gate_spool_id",
118150
}
119151
if gate_keys.isdisjoint(diff):
120-
# No changes
152+
# No gate array changes
121153
scalar_fields = {
122154
k: v for k, v in diff.items() if k in MMUState.__dataclass_fields__
123155
}
124156
if "ttg_map" in scalar_fields:
125157
scalar_fields["ttg_map"] = tuple(scalar_fields["ttg_map"])
158+
if "filament_pos" in scalar_fields:
159+
scalar_fields["filament_pos"] = FilamentPos(scalar_fields["filament_pos"])
160+
if "spoolman_support" in scalar_fields:
161+
scalar_fields["spoolman_support"] = SpoolmanSupport(scalar_fields["spoolman_support"])
126162
return dataclasses.replace(self, **scalar_fields)
127163
# Gate arrays changed — need full rebuild, but we lost the raw arrays
128164
# Pass current gate data + diff into from_status

BlocksScreen/lib/panels/mainWindow.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ def __init__(self):
202202
self.printer.extruder_update.connect(self.on_extruder_update)
203203
self.printer.heater_bed_update.connect(self.on_heater_bed_update)
204204
self.printer.register_callback("mmu", self.amu_manager.update_mmu_state)
205+
self.printer.register_callback("filament_switch_sensor", self.amu_manager.on_pre_gate_update)
206+
self.printer.register_klippy_callback(self.amu_manager.on_klippy_state)
205207
self.amu_manager.run_gcode_signal.connect(self.ws.api.run_gcode)
206208
self.run_gcode_signal.connect(self.ws.api.run_gcode)
207209

BlocksScreen/lib/printer.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ def __init__(self, parent: QtCore.QObject, ws: MoonWebSocket, /) -> None:
127127
self.request_available_objects_signal.connect(self.ws.api.get_available_objects)
128128
self.request_object_subscription_signal.connect(self.ws.api.object_subscription)
129129
self.query_printer_object.connect(self.ws.api.object_query)
130+
self._klippy_callback: typing.Callable[[str], None] | None = None
130131
self._callbacks: dict[str, typing.Callable[[dict, str], None]] = {}
131132

132133
def clear_printer_objs(self) -> None:
@@ -169,6 +170,17 @@ def register_callback(
169170
"""
170171
self.__inject_callback(object_type, callback)
171172

173+
def register_klippy_callback(self, callback: typing.Callable[[str], None]) -> None:
174+
"""Register a callback for klippy lifecycle state changes.
175+
176+
Args:
177+
callback: Callable with signature ``(state: str) -> None``.
178+
"""
179+
if not callable(callback):
180+
logger.warning("register_klippy_callback: not callable")
181+
return
182+
self._klippy_callback = callback
183+
172184
@QtCore.pyqtSlot(str, name="on_klippy_status")
173185
def on_klippy_status(self, state: str):
174186
"""Handles klippy update status
@@ -183,7 +195,11 @@ def on_klippy_status(self, state: str):
183195
"virtual_sdcard": None,
184196
}
185197
self.query_printer_object.emit(_query_request)
198+
if self._klippy_callback is not None:
199+
self._klippy_callback(state)
186200
return
201+
if self._klippy_callback is not None:
202+
self._klippy_callback(state)
187203
self.clear_printer_objs() # All other states clear it
188204

189205
@QtCore.pyqtSlot(list, name="on_object_list")
@@ -303,7 +319,6 @@ def _check_callback(self, name: str, values: dict) -> bool:
303319
_object_name = name
304320
if _object_type in self._callbacks:
305321
self._callbacks[_object_type](values, _object_name)
306-
return True
307322
if hasattr(self, f"_{_object_type}_object_updated"):
308323
_callback = getattr(self, f"_{_object_type}_object_updated")
309324
if callable(_callback):

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,17 @@ test: ## Unit + UI tests (excludes real D-Bus integration)
9999
test-fast: ## Stop on first failure, quiet output
100100
$(PYTHON) -m pytest -x -q $(PYTEST_IGNORE) $(TESTS)
101101

102+
test-fast-%: ## Stop on first failure, quiet output, scoped to tests/<folder>/
103+
$(PYTHON) -m pytest -x -q $(TESTS)/$*
104+
102105
test-unit: ## Unit tests only (*_unit.py)
103106
$(PYTHON) -m pytest $(PYTEST_FLAGS) $(TESTS)/*/*_unit.py
104107

105108
test-ui: ## UI tests only (*_ui.py)
106109
$(PYTHON) -m pytest $(PYTEST_FLAGS) $(TESTS)/*/*_ui.py
107110

108-
test-network: ## Network subsystem tests (unit + UI, no D-Bus)
109-
$(PYTHON) -m pytest $(PYTEST_FLAGS) $(PYTEST_IGNORE) $(TESTS)/network/
111+
test-%: ## Tests all files inside the /<folder>/
112+
$(PYTHON) -m pytest $(PYTEST_FLAGS) $(PYTEST_IGNORE) $(TESTS)/$*/
110113

111114
test-integration: ## D-Bus integration tests (requires live NetworkManager)
112115
$(NM_INTEGRATION) $(PYTHON) -m pytest $(PYTEST_FLAGS) $(TESTS)/*/*_integration.py

0 commit comments

Comments
 (0)