Eric Callahan b4ddffd5d1 moonraker: refactor KlippyConnection
Move the KlippyConnection class into its own module.  Refactor
init to use loops rather than callbacks, this reduces complexity
of tracking and cancelling callback handles.

All Klippy state previously tracked by the Server is now in the
KlippyConnection.  This improves testing and makes the code
less ambiguous, ie: the `server.make_request()` method is not
as clear as `klippy.request()`.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2022-02-09 19:15:11 -05:00

215 lines
8.4 KiB
Python

# Helper for Moonraker to Klippy API calls.
#
# Copyright (C) 2020 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from __future__ import annotations
from utils import SentinelClass
from websockets import WebRequest, Subscribable
# Annotation imports
from typing import (
TYPE_CHECKING,
Any,
Union,
Optional,
Dict,
List,
TypeVar,
Mapping,
)
if TYPE_CHECKING:
from confighelper import ConfigHelper
from websockets import WebRequest
from klippy_connection import KlippyConnection as Klippy
Subscription = Dict[str, Optional[List[Any]]]
_T = TypeVar("_T")
INFO_ENDPOINT = "info"
ESTOP_ENDPOINT = "emergency_stop"
LIST_EPS_ENDPOINT = "list_endpoints"
GC_OUTPUT_ENDPOINT = "gcode/subscribe_output"
GCODE_ENDPOINT = "gcode/script"
SUBSCRIPTION_ENDPOINT = "objects/subscribe"
STATUS_ENDPOINT = "objects/query"
OBJ_LIST_ENDPOINT = "objects/list"
REG_METHOD_ENDPOINT = "register_remote_method"
SENTINEL = SentinelClass.get_instance()
class KlippyAPI(Subscribable):
def __init__(self, config: ConfigHelper) -> None:
self.server = config.get_server()
self.klippy: Klippy = self.server.lookup_component("klippy_connection")
app_args = self.server.get_app_args()
self.version = app_args.get('software_version')
# Maintain a subscription for all moonraker requests, as
# we do not want to overwrite them
self.host_subscription: Subscription = {}
# Register GCode Aliases
self.server.register_endpoint(
"/printer/print/pause", ['POST'], self._gcode_pause)
self.server.register_endpoint(
"/printer/print/resume", ['POST'], self._gcode_resume)
self.server.register_endpoint(
"/printer/print/cancel", ['POST'], self._gcode_cancel)
self.server.register_endpoint(
"/printer/print/start", ['POST'], self._gcode_start_print)
self.server.register_endpoint(
"/printer/restart", ['POST'], self._gcode_restart)
self.server.register_endpoint(
"/printer/firmware_restart", ['POST'], self._gcode_firmware_restart)
async def _gcode_pause(self, web_request: WebRequest) -> str:
return await self._send_klippy_request("pause_resume/pause", {})
async def _gcode_resume(self, web_request: WebRequest) -> str:
return await self._send_klippy_request("pause_resume/resume", {})
async def _gcode_cancel(self, web_request: WebRequest) -> str:
return await self._send_klippy_request("pause_resume/cancel", {})
async def _gcode_start_print(self, web_request: WebRequest) -> str:
filename: str = web_request.get_str('filename')
return await self.start_print(filename)
async def _gcode_restart(self, web_request: WebRequest) -> str:
return await self.do_restart("RESTART")
async def _gcode_firmware_restart(self, web_request: WebRequest) -> str:
return await self.do_restart("FIRMWARE_RESTART")
async def _send_klippy_request(self,
method: str,
params: Dict[str, Any],
default: Any = SENTINEL
) -> Any:
try:
result = await self.klippy.request(
WebRequest(method, params, conn=self))
except self.server.error:
if isinstance(default, SentinelClass):
raise
result = default
return result
async def run_gcode(self,
script: str,
default: Any = SENTINEL
) -> str:
params = {'script': script}
result = await self._send_klippy_request(
GCODE_ENDPOINT, params, default)
return result
async def start_print(self, filename: str) -> str:
# WARNING: Do not call this method from within the following
# event handlers:
# klippy_identified, klippy_started, klippy_ready, klippy_disconnect
# Doing so will result in a deadlock
# XXX - validate that file is on disk
if filename[0] == '/':
filename = filename[1:]
# Escape existing double quotes in the file name
filename = filename.replace("\"", "\\\"")
script = f'SDCARD_PRINT_FILE FILENAME="{filename}"'
await self.klippy.wait_connected()
return await self.run_gcode(script)
async def do_restart(self, gc: str) -> str:
# WARNING: Do not call this method from within the following
# event handlers:
# klippy_identified, klippy_started, klippy_ready, klippy_disconnect
# Doing so will result in a deadlock
# XXX - validate that file is on disk
await self.klippy.wait_connected()
try:
result = await self.run_gcode(gc)
except self.server.error as e:
if str(e) == "Klippy Disconnected":
result = "ok"
else:
raise
return result
async def list_endpoints(self,
default: Union[SentinelClass, _T] = SENTINEL
) -> Union[_T, Dict[str, List[str]]]:
return await self._send_klippy_request(
LIST_EPS_ENDPOINT, {}, default)
async def emergency_stop(self) -> str:
return await self._send_klippy_request(ESTOP_ENDPOINT, {})
async def get_klippy_info(self,
send_id: bool = False,
default: Union[SentinelClass, _T] = SENTINEL
) -> Union[_T, Dict[str, Any]]:
params = {}
if send_id:
ver = self.version
params = {'client_info': {'program': "Moonraker", 'version': ver}}
return await self._send_klippy_request(INFO_ENDPOINT, params, default)
async def get_object_list(self,
default: Union[SentinelClass, _T] = SENTINEL
) -> Union[_T, List[str]]:
result = await self._send_klippy_request(
OBJ_LIST_ENDPOINT, {}, default)
if isinstance(result, dict) and 'objects' in result:
return result['objects']
return result
async def query_objects(self,
objects: Mapping[str, Optional[List[str]]],
default: Union[SentinelClass, _T] = SENTINEL
) -> Union[_T, Dict[str, Any]]:
params = {'objects': objects}
result = await self._send_klippy_request(
STATUS_ENDPOINT, params, default)
if isinstance(result, dict) and 'status' in result:
return result['status']
return result
async def subscribe_objects(self,
objects: Mapping[str, Optional[List[str]]],
default: Union[SentinelClass, _T] = SENTINEL
) -> Union[_T, Dict[str, Any]]:
for obj, items in objects.items():
if obj in self.host_subscription:
prev = self.host_subscription[obj]
if items is None or prev is None:
self.host_subscription[obj] = None
else:
uitems = list(set(prev) | set(items))
self.host_subscription[obj] = uitems
else:
self.host_subscription[obj] = items
params = {'objects': self.host_subscription}
result = await self._send_klippy_request(
SUBSCRIPTION_ENDPOINT, params, default)
if isinstance(result, dict) and 'status' in result:
return result['status']
return result
async def subscribe_gcode_output(self) -> str:
template = {'response_template':
{'method': "process_gcode_response"}}
return await self._send_klippy_request(GC_OUTPUT_ENDPOINT, template)
async def register_method(self, method_name: str) -> str:
return await self._send_klippy_request(
REG_METHOD_ENDPOINT,
{'response_template': {"method": method_name},
'remote_method': method_name})
def send_status(self,
status: Dict[str, Any],
eventtime: float
) -> None:
self.server.send_event("server:status_update", status)
def load_component(config: ConfigHelper) -> KlippyAPI:
return KlippyAPI(config)