|
5 | 5 | import time |
6 | 6 | import logging |
7 | 7 | import subprocess |
| 8 | +import shlex |
| 9 | +import ast |
8 | 10 | import paho.mqtt.client as paho |
9 | 11 |
|
10 | 12 | from ..utils import Sentinel |
11 | 13 | from .power import PowerDevice |
| 14 | +from ..common import WebRequest |
| 15 | + |
| 16 | +from typing import ( |
| 17 | + TYPE_CHECKING, |
| 18 | + Any, |
| 19 | + Union, |
| 20 | + Optional, |
| 21 | + Dict, |
| 22 | + List, |
| 23 | + TypeVar, |
| 24 | + Mapping, |
| 25 | + Callable, |
| 26 | + Coroutine |
| 27 | +) |
| 28 | +FlexCallback = Callable[..., Optional[Coroutine]] |
12 | 29 |
|
13 | 30 |
|
14 | 31 | def shell(command): |
@@ -37,6 +54,11 @@ class Kobra: |
37 | 54 | _remote_mode = None |
38 | 55 | _total_layer = 0 |
39 | 56 |
|
| 57 | + # GCode handlers |
| 58 | + gcode_handlers: dict[str, FlexCallback] = {} |
| 59 | + status_patchers: List[Callable[[dict], dict]] = [] |
| 60 | + print_data_patchers: List[Callable[[dict], dict]] = [] |
| 61 | + |
40 | 62 | def __init__(self, config): |
41 | 63 | self.server = config.get_server() |
42 | 64 | self.power = self.server.load_component(self.server.config, 'power') |
@@ -70,6 +92,7 @@ def tool_function(*args): |
70 | 92 | logging.info('Starting Kobra patching...') |
71 | 93 |
|
72 | 94 | self.patch_status_updates() |
| 95 | + self.patch_gcode_handler() |
73 | 96 | self.patch_network_interfaces() |
74 | 97 | self.patch_spoolman() |
75 | 98 | self.patch_simplyprint() |
@@ -172,22 +195,50 @@ def mqtt_print_file(self, file): |
172 | 195 | vibration_compensation = self.get_app_property('40-moonraker', 'mqtt_print_vibration_compensation').lower() == 'true' |
173 | 196 | flow_calibration = self.get_app_property('40-moonraker', 'mqtt_print_flow_calibration').lower() == 'true' |
174 | 197 |
|
175 | | - payload = f"""{{ |
176 | | - "type": "print", |
177 | | - "action": "start", |
178 | | - "msgid": "{uuid.uuid4()}", |
179 | | - "timestamp": {round(time.time() * 1000)}, |
180 | | - "data": {{ |
181 | | - "taskid": "-1", |
182 | | - "filename": "{file}", |
183 | | - "filetype": 1, |
184 | | - "task_settings": {{ |
185 | | - "auto_leveling": {'1' if auto_leveling else '0'}, |
186 | | - "vibration_compensation": {'1' if vibration_compensation else '0'}, |
187 | | - "flow_calibration": {'1' if flow_calibration else '0'} |
188 | | - }} |
189 | | - }} |
190 | | - }}""" |
| 198 | + print_request = { |
| 199 | + 'type': 'print', |
| 200 | + 'action': 'start', |
| 201 | + 'msgid': str(uuid.uuid4()), |
| 202 | + 'timestamp': int(time.time() * 1000), |
| 203 | + 'data': { |
| 204 | + 'taskid': '-1', |
| 205 | + 'filename': file, |
| 206 | + 'filetype': 1, |
| 207 | + 'task_settings': { |
| 208 | + 'auto_leveling': 1 if auto_leveling else 0, |
| 209 | + 'vibration_compensation': 1 if vibration_compensation else 0, |
| 210 | + 'flow_calibration': 1 if flow_calibration else 0 |
| 211 | + } |
| 212 | + } |
| 213 | + } |
| 214 | + |
| 215 | + print_data = print_request["data"] |
| 216 | + |
| 217 | + for patcher in self.print_data_patchers: |
| 218 | + print_data = patcher(print_data) |
| 219 | + |
| 220 | + print_request["data"] = print_data |
| 221 | + |
| 222 | + logging.info(f'[Kobra] print data : {json.dumps(print_data)}') |
| 223 | + |
| 224 | + payload = json.dumps(print_request) |
| 225 | + |
| 226 | + # payload = f"""{{ |
| 227 | + # "type": "print", |
| 228 | + # "action": "start", |
| 229 | + # "msgid": "{uuid.uuid4()}", |
| 230 | + # "timestamp": {round(time.time() * 1000)}, |
| 231 | + # "data": {{ |
| 232 | + # "taskid": "-1", |
| 233 | + # "filename": "{file}", |
| 234 | + # "filetype": 1, |
| 235 | + # "task_settings": {{ |
| 236 | + # "auto_leveling": {'1' if auto_leveling else '0'}, |
| 237 | + # "vibration_compensation": {'1' if vibration_compensation else '0'}, |
| 238 | + # "flow_calibration": {'1' if flow_calibration else '0'} |
| 239 | + # }} |
| 240 | + # }} |
| 241 | + # }}""" |
191 | 242 |
|
192 | 243 | self.mqtt_print_report = False |
193 | 244 | self.mqtt_print_error = None |
@@ -284,8 +335,16 @@ def patch_status(self, status): |
284 | 335 | # Remove path prefix from file path |
285 | 336 | status['virtual_sdcard']['file_path'] = status['virtual_sdcard']['file_path'].replace('/useremain/app/gk/gcodes/', '') |
286 | 337 |
|
| 338 | + for patcher in self.status_patchers: |
| 339 | + status = patcher(status) |
| 340 | + |
287 | 341 | return status |
288 | 342 |
|
| 343 | + def register_status_patcher(self, patcher: Callable[[dict], dict]): |
| 344 | + self.status_patchers.append(patcher) |
| 345 | + |
| 346 | + def register_print_data_patcher(self, patcher: Callable[[dict], dict]): |
| 347 | + self.print_data_patchers.append(patcher) |
289 | 348 |
|
290 | 349 | def patch_status_updates(self): |
291 | 350 | from .klippy_apis import KlippyAPI |
@@ -388,27 +447,106 @@ def get_klippy_info(me): |
388 | 447 | setattr(Server, 'get_klippy_info', wrap_get_klippy_info(Server.get_klippy_info)) |
389 | 448 | logging.debug(f' After: {Server.get_klippy_info}') |
390 | 449 |
|
391 | | - def patch_mqtt_print(self): |
| 450 | + def register_gcode_handler(self, cmd, callback: FlexCallback): |
| 451 | + logging.info(f'> Registering gcode handler for {cmd}...') |
| 452 | + self.gcode_handlers[cmd.upper()] = callback |
| 453 | + |
| 454 | + def patch_gcode_handler(self): |
392 | 455 | from .klippy_apis import KlippyAPI |
| 456 | + from .klippy_connection import KlippyConnection |
| 457 | + |
| 458 | + async def handle_gcode(me, script, delegate_run_gcode: Callable[[], Coroutine]): |
| 459 | + parts = [s.strip() for s in shlex.split(script.strip()) if s.strip()] |
| 460 | + logging.warning(f"hook on gcode received: {json.dumps(parts)}") |
| 461 | + cmd = parts[0] |
| 462 | + |
| 463 | + logging.warning(f"hook on gcode cmd: {cmd}") |
| 464 | + handlers = self.gcode_handlers.keys() |
| 465 | + # join handlers |
| 466 | + handlers = ', '.join(handlers) |
| 467 | + logging.warning(f"hook on gcode handlers: {handlers}") |
| 468 | + |
| 469 | + if cmd in self.gcode_handlers: |
| 470 | + logging.warning(f"hook on gcode cmd found: {cmd}") |
| 471 | + args = {} |
| 472 | + for part in parts[1:]: |
| 473 | + if '=' in part: |
| 474 | + key, value = part.split('=', 1) |
| 475 | + args[key] = value |
| 476 | + else: |
| 477 | + args[part] = None |
| 478 | + |
| 479 | + logging.warning(f"hook on gcode args: {json.dumps(args)}") |
| 480 | + result = await self.gcode_handlers[cmd](args, delegate_run_gcode) |
| 481 | + result_str = "None" if result is None else "Any" |
| 482 | + logging.warning(f"hook on gcode result: {result_str}") |
| 483 | + |
| 484 | + if result is None: |
| 485 | + return None |
| 486 | + |
| 487 | + return await result |
| 488 | + else: |
| 489 | + logging.warning(f"hook on gcode cmd not found: {cmd}") |
| 490 | + return await delegate_run_gcode() |
| 491 | + |
| 492 | + def wrap_request(original_request: KlippyConnection.request): |
| 493 | + async def request(me: KlippyConnection, web_request: WebRequest): |
| 494 | + logging.warning(f"hook on request") |
| 495 | + |
| 496 | + rpc_method = web_request.get_endpoint() |
| 497 | + if rpc_method == "gcode/script": |
| 498 | + |
| 499 | + script = web_request.get_str('script', "") |
| 500 | + if script: |
| 501 | + async def delegate_run_gcode(): |
| 502 | + return await original_request(me, web_request) |
| 503 | + |
| 504 | + return await handle_gcode(me, script, delegate_run_gcode) |
| 505 | + |
| 506 | + return await original_request(me, web_request) |
| 507 | + |
| 508 | + return request |
| 509 | + |
| 510 | + def wrap_run_gcode(original_run_gcode: KlippyAPI.run_gcode): |
| 511 | + async def run_gcode(me: KlippyAPI, script: str, default: Any = Sentinel.MISSING): |
| 512 | + logging.warning(f"hook on run gcode: {script}") |
| 513 | + |
| 514 | + async def delegate_run_gcode(): |
| 515 | + return await original_run_gcode(me, script, default) |
| 516 | + |
| 517 | + return await handle_gcode(me, script, delegate_run_gcode) |
393 | 518 |
|
394 | | - def wrap_run_gcode(original_run_gcode): |
395 | | - async def run_gcode(me, script, default = Sentinel.MISSING): |
396 | | - if self.is_goklipper_running() and script.startswith('SDCARD_PRINT_FILE'): |
397 | | - self._total_layer = 0 |
398 | | - filename = re.search("FILENAME=\"([^\"]+)\"$", script) |
399 | | - filename = filename[1] if filename else None |
400 | | - if filename and self.is_using_mqtt(): |
401 | | - self.mqtt_print_file(filename) |
402 | | - return None |
403 | | - return await original_run_gcode(me, script, default) |
404 | 519 | return run_gcode |
405 | 520 |
|
406 | | - logging.info('> Send prints to MQTT...') |
| 521 | + logging.info('> Adding gcode handler...') |
| 522 | + |
| 523 | + logging.debug(f' Before: {KlippyConnection.request}') |
| 524 | + setattr(KlippyConnection, 'request', wrap_request(KlippyConnection.request)) |
| 525 | + logging.debug(f' After: {KlippyConnection.request}') |
407 | 526 |
|
408 | 527 | logging.debug(f' Before: {KlippyAPI.run_gcode}') |
409 | 528 | setattr(KlippyAPI, 'run_gcode', wrap_run_gcode(KlippyAPI.run_gcode)) |
410 | 529 | logging.debug(f' After: {KlippyAPI.run_gcode}') |
411 | 530 |
|
| 531 | + def patch_mqtt_print(self): |
| 532 | + async def handle_gcode_print_file(args: dict, delegate_run_gcode): |
| 533 | + logging.info(f'[Kobra] Print file: {args}') |
| 534 | + if self.is_goklipper_running(): |
| 535 | + self._total_layer = 0 |
| 536 | + filename = args["FILENAME"] if "FILENAME" in args else None |
| 537 | + logging.info(f'[Kobra] Print file: {filename}') |
| 538 | + |
| 539 | + if filename and self.is_using_mqtt(): |
| 540 | + logging.info(f'[Kobra] MQTT print file: {filename}') |
| 541 | + self.mqtt_print_file(filename) |
| 542 | + return None |
| 543 | + |
| 544 | + logging.info(f'[Kobra] Not MQTT print file: {filename}') |
| 545 | + return await delegate_run_gcode() |
| 546 | + |
| 547 | + logging.info('> Send prints to MQTT...') |
| 548 | + self.register_gcode_handler('SDCARD_PRINT_FILE', handle_gcode_print_file) |
| 549 | + |
412 | 550 | def patch_bed_mesh(self): |
413 | 551 | from .klippy_connection import KlippyConnection |
414 | 552 |
|
|
0 commit comments