Skip to content

Commit 2420480

Browse files
committed
update: add more backend methods, added callback calls after moonraker response and addition of tests for the new implementations
1 parent b75aa98 commit 2420480

16 files changed

Lines changed: 509 additions & 102 deletions

BlocksScreen/devices/amu/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
MMUState,
1717
SpoolmanSupport,
1818
)
19+
from .config_toggler import ConfigToggler
1920

2021
__all__: list[str] = [
2122
# manager
2223
"AMUManager",
24+
# config_toggler
25+
"ConfigToggler",
2326
# models
2427
"FilamentPos",
2528
"GateInfo",
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import logging
2+
import re
3+
4+
from pathlib import Path
5+
6+
logger = logging.getLogger(__name__)
7+
8+
AMU_FILES: list[str] = [
9+
"mmu/base/*.cfg",
10+
"mmu/optional/client_macros.cfg",
11+
"filament_manager.cfg",
12+
]
13+
14+
_AMU_PATTERNS: list[re.Pattern[str]] = [
15+
re.compile(rf"^(#?)(\[include {re.escape(f)}\])", re.MULTILINE) for f in AMU_FILES
16+
]
17+
18+
19+
class ConfigToggler:
20+
"""Manages commenting/uncommenting AMU cfg includes in printer.cfg"""
21+
22+
def __init__(self, config_path: Path) -> None:
23+
"""Store path, detects current state from file"""
24+
self._path: Path | None = None
25+
self._state: bool = False
26+
if config_path.exists():
27+
self._path = config_path
28+
self._state = self._detect_state()
29+
else:
30+
logger.warning("Config File not found %s", config_path)
31+
32+
def _detect_state(self) -> bool:
33+
if self._path is None:
34+
return False
35+
try:
36+
text = self._path.read_text()
37+
return all(re.search(rf"^\[include {re.escape(f)}\]", text, re.MULTILINE)
38+
for f in AMU_FILES
39+
)
40+
except OSError:
41+
return False
42+
43+
def toggle(self, activate: bool) -> bool:
44+
"""Comment or uncomment AMU includes. Returns True on success, False on no-op or error."""
45+
if self._path is None:
46+
logger.warning("No Config File available")
47+
return False
48+
if self._state == activate:
49+
return False
50+
replacement = r"\2" if activate else r"#\2"
51+
try:
52+
text = self._path.read_text()
53+
for pattern in _AMU_PATTERNS:
54+
text = pattern.sub(replacement, text)
55+
self._path.write_text(text)
56+
self._state = activate
57+
return True
58+
except OSError as e:
59+
logger.error(
60+
"ConfigToggler.toggle(activate=%s): read/write failed: %s", activate,e
61+
)
62+
return False
63+
64+
def is_configured(self) -> bool:
65+
"""Return True if AMU includes are currently uncommented"""
66+
return self._state

BlocksScreen/devices/amu/manager.py

Lines changed: 41 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
1+
from __future__ import annotations
2+
13
import logging
2-
import re
34
import typing
45
from pathlib import Path
56

67
from PyQt6 import QtCore
78

8-
from .models import MMUState
9+
from BlocksScreen.devices.amu.config_toggler import ConfigToggler
10+
11+
from .models import MMUState, SpoolmanSupport
12+
13+
if typing.TYPE_CHECKING:
14+
from BlocksScreen.lib.moonrakerComm import MoonWebSocket
15+
916

1017
logger: logging.Logger = logging.getLogger(__name__)
1118

1219
CONFIG_PATH: Path = Path("~/printer_data/config/printer.cfg").expanduser()
13-
AMU_FILES: list[str] = [
14-
"mmu/base/*.cfg",
15-
"mmu/optional/client_macros.cfg",
16-
"filament_manager.cfg",
17-
]
18-
19-
AMU_PATTERNS: list[re.Pattern[str]] = [
20-
re.compile(rf"^(#?)(\[include {re.escape(f)}\])", re.MULTILINE) for f in AMU_FILES
21-
]
2220

2321

2422
class AMUManager(QtCore.QObject):
@@ -39,55 +37,17 @@ class AMUManager(QtCore.QObject):
3937
pre_gate_changed: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal(
4038
int, bool, name="pre-gate-changed"
4139
)
40+
spool_fetched: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal(
41+
int, dict, name="spool-fetched"
42+
)
4243

43-
def __init__(self, parent: QtCore.QObject | None = None) -> None:
44+
def __init__(self, ws: MoonWebSocket, parent: QtCore.QObject | None = None) -> None:
4445
super().__init__(parent)
46+
self._config_toggler = ConfigToggler(CONFIG_PATH)
4547
self._amu_state = False
48+
self._ws = ws
4649
self._mmu_state: MMUState | None = None
4750
self._pre_gate_sensors: dict[int, bool] = {}
48-
self.__setup_configfile()
49-
50-
def __setup_configfile(self) -> None:
51-
"""Sets up local configfile variable"""
52-
self._config_filename: Path | None = CONFIG_PATH
53-
if not self._config_filename.exists():
54-
logger.warning("Config file not found %s", self._config_filename)
55-
self._config_filename = None
56-
57-
def _apply_patterns(self, state: bool) -> bool:
58-
"""Method that comments/uncomments the AMU_FILES from the printer.cfg according with state value
59-
60-
Args:
61-
state (bool): True: Uncomment, False: Comment
62-
63-
Returns:
64-
bool: True: Success, False: Failed
65-
"""
66-
if self._config_filename is None:
67-
logger.warning("_apply_patterns called but no config file available")
68-
return False
69-
70-
if self._amu_state == state:
71-
return False
72-
73-
replacement = r"\2"
74-
if not state:
75-
replacement = r"#\2"
76-
try:
77-
text: str = self._config_filename.read_text()
78-
for file in AMU_PATTERNS:
79-
text = file.sub(replacement, text)
80-
self._config_filename.write_text(text)
81-
self._amu_state = state
82-
return True
83-
except OSError as e:
84-
logger.error(
85-
"Failed to apply(state=%s) AMU System: could not read/write %s\n%s",
86-
state,
87-
self._config_filename,
88-
e,
89-
)
90-
return False
9151

9252
def toggle_amu_system(self, activate: bool) -> None:
9353
"""Enable or disable the AMU system by commenting/uncommenting config includes.
@@ -99,8 +59,10 @@ def toggle_amu_system(self, activate: bool) -> None:
9959
activate (bool): True to enable the AMU, False to disable it.
10060
10161
"""
102-
result: bool = self._apply_patterns(activate)
62+
result: bool = self._config_toggler.toggle(activate)
10363
self.amu_toggled.emit(result)
64+
if result:
65+
self.run_gcode_signal.emit("FIRMWARE_RESTART")
10466

10567
def get_state(self) -> MMUState | None:
10668
"""Returns current MMU state, None if not yet received.
@@ -115,9 +77,30 @@ def get_state(self) -> MMUState | None:
11577
def get_pre_gate_sensors(self) -> dict[int, bool]:
11678
return dict(self._pre_gate_sensors)
11779

80+
def is_amu_configured(self) -> bool:
81+
"""Return True if AMU includes are uncommented in printer.cfg."""
82+
return self._config_toggler.is_configured()
83+
11884
def is_amu_active(self) -> bool:
11985
"""Returns whether AMU includes are currently uncommented in printer.cfg"""
120-
return self._amu_state
86+
return self.is_amu_configured() and self._mmu_state is not None
87+
88+
def fetch_spool(self, gate: int, spool_id: int) -> None:
89+
"""Request spool data from Moonraker via WebSocket.
90+
91+
No-op if MMU state not received or spoolman_support is OFF.
92+
Emits spool_fetched(gate, data) on sucess; logs and emits nothing on error.
93+
"""
94+
if self._mmu_state is None:
95+
return
96+
if self._mmu_state.spoolman_support is SpoolmanSupport.OFF:
97+
return
98+
99+
def _on_result(result: dict | None) -> None:
100+
if result is not None:
101+
self.spool_fetched.emit(gate, result)
102+
103+
self._ws.api.get_spool(spool_id, _on_result)
121104

122105
def set_gate_info(
123106
self, gate: int, material: str, color: str, spool_id: int
@@ -239,3 +222,4 @@ def on_klippy_state(self, state: str) -> None:
239222
"""React to changes in klippy states"""
240223
if state.lower() != "ready":
241224
self._mmu_state = None
225+
self._pre_gate_sensors = {}

BlocksScreen/devices/amu/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class GateInfo:
4444
color: str
4545
color_rgb: tuple[float, float, float]
4646
spool_id: int
47+
weight_g: float | None = None
48+
mid_usage: bool = False
4749

4850
@property
4951
def is_available(self) -> bool:

BlocksScreen/lib/moonrakerComm.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,15 @@ def on_message(self, *args) -> None:
267267
self.klippy_state_signal.emit(response["result"]["klippy_state"])
268268
return
269269
else:
270+
_callback = _entry[2] if len(_entry) > 2 else None
271+
if _callback is not None:
272+
if "error" not in response:
273+
_callback(response.get("result"))
274+
else:
275+
logger.error(
276+
"WS request %s error: %s", _entry[0], response.get("error")
277+
)
278+
return
270279
if "error" in response:
271280
message_event = WebSocketMessageReceived(
272281
method="error",
@@ -302,21 +311,23 @@ def on_message(self, *args) -> None:
302311
except Exception as e:
303312
logger.info(f"Unexpected error while creating websocket message event: {e}")
304313

305-
def send_request(self, method: str, params: dict = {}) -> bool:
314+
def send_request(self, method: str, params: dict = {}, callback=None) -> bool:
306315
"""Send a request over the websocket
307316
308317
Args:
309318
method (str): Websocket method name
310319
params (dict, optional): parameters for the websocket method. Defaults to {}.
311-
320+
callback (callable, optional): Called with ``response["result"]`` when the
321+
response arrives. If None, the response is routed as a
322+
``WebSocketMessageReceived`` event to the parent widget. Defaults to None.
312323
Returns:
313324
bool: Whether the method finished and a request was sent
314325
"""
315326
if not self.connected or self.ws is None:
316327
return False
317328

318329
self._request_id += 1
319-
self.request_table[self._request_id] = [method, params]
330+
self.request_table[self._request_id] = [method, params, callback]
320331
packet = {
321332
"jsonrpc": "2.0",
322333
"method": method,
@@ -809,3 +820,13 @@ def history_get_job(self, uid: str):
809820
def history_delete_job(self, uid: str):
810821
"""Request delete job history"""
811822
raise NotImplementedError
823+
824+
# ---------------------------------AMU----------------------------------
825+
826+
def get_spool(self, spool_id: int, callback) -> bool:
827+
"""Request spool data from Moonraker's Spoolman proxy"""
828+
return self._ws.send_request(
829+
method="server.spoolman.get_spool",
830+
params={"spool_id": spool_id},
831+
callback=callback,
832+
)

BlocksScreen/lib/moonrest.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,26 @@ def get_server_info(self):
8787
"""
8888
return self.get_request(method="server/info")
8989

90+
def get_spool(self, spool_id: int) -> dict | None:
91+
"""GET /server/spoolman/spool/{spool_id} via Moonraker
92+
93+
Returns spool dict on success, None on HTTP/network/JSON error.
94+
"""
95+
response = self.get_request(f"server/spoolman/spool/{spool_id}")
96+
if not isinstance(response, dict):
97+
return None
98+
return response.get("result")
99+
100+
def set_spool_used_weight(self, spool_id: int, weight: float) -> bool:
101+
"""POST /server/spoolman/spool/{spool_id} to update used_weight.
102+
103+
Returns True on sucess, False on any error.
104+
"""
105+
response = self.post_request(
106+
f"server/spoolman/spool/{spool_id}", json={"used_weight": weight}
107+
)
108+
return response is not None
109+
90110
def firmware_restart(self):
91111
"""firmware_restart
92112
POST to /printer/firmware_restart to firmware restart Klipper

BlocksScreen/lib/panels/mainWindow.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ def __init__(self):
120120
if usb_config:
121121
gdir = usb_config.get("gcodes_dir", default=None)
122122
self.usb_manager: USBManager = USBManager(parent=self, gcodes_dir=gdir)
123-
self.amu_manager: AMUManager = AMUManager(parent=self)
124123
self.ws = MoonWebSocket(self)
124+
self.amu_manager: AMUManager = AMUManager(ws=self.ws, parent=self)
125125
self.notiPage = NotificationPage(self)
126126
self.mc = MachineControl(self)
127127
self.file_data = Files(self, self.ws)
@@ -202,7 +202,9 @@ 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)
205+
self.printer.register_callback(
206+
"filament_switch_sensor", self.amu_manager.on_pre_gate_update
207+
)
206208
self.printer.register_klippy_callback(self.amu_manager.on_klippy_state)
207209
self.amu_manager.run_gcode_signal.connect(self.ws.api.run_gcode)
208210
self.run_gcode_signal.connect(self.ws.api.run_gcode)

0 commit comments

Comments
 (0)