moonraker: update protocol for data received from klippy

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Arksine 2020-08-13 20:45:03 -04:00
parent 291f2b0e91
commit 4a57dba586
3 changed files with 146 additions and 86 deletions

View File

@ -21,7 +21,7 @@ MAX_UPLOAD_SIZE = 200 * 1024 * 1024
# These endpoints are reserved for klippy/server communication only and are
# not exposed via http or the websocket
RESERVED_ENDPOINTS = [
"list_endpoints", "moonraker/check_available"
"list_endpoints", "gcode/subscribe_output"
]
@ -34,8 +34,10 @@ def _status_parser(request):
for v in vals:
if v:
parsed += v.decode().split(',')
if parsed == []:
parsed = None
args[key] = parsed
return args
return {'objects': args}
# Built-in Query String Parser
def _default_parser(request):

View File

@ -22,7 +22,7 @@ from utils import ServerError, MoonrakerLoggingHandler
INIT_MS = 1000
CORE_PLUGINS = [
'file_manager', 'gcode_apis', 'machine',
'file_manager', 'klippy_apis', 'machine',
'temperature_store', 'shell_command']
class Sentinel:
@ -43,6 +43,16 @@ class Server:
'klippy_uds_address', "/tmp/klippy_uds")
self.klippy_iostream = None
self.is_klippy_ready = False
self.gc_response_registered = False
self.klippy_state = "disconnected"
# XXX - currently moonraker maintains a superset of all
# subscriptions, the results of which are forwarded to all
# connected websockets. A better implementation would open a
# unique unix domain socket for each websocket client and
# allow Klipper to forward only those subscriptions back to
# correct client.
self.all_subscriptions = {}
# Server/IOLoop
self.server_running = False
@ -58,10 +68,6 @@ class Server:
# they do not return a response to Klippy after execution
self.pending_requests = {}
self.remote_methods = {}
self.register_remote_method(
'set_klippy_shutdown', self._set_klippy_shutdown)
self.register_remote_method(
'response', self._handle_klippy_response)
self.register_remote_method(
'process_gcode_response', self._process_gcode_response)
self.register_remote_method(
@ -69,6 +75,7 @@ class Server:
# Plugin initialization
self.plugins = {}
self.klippy_apis = self.load_plugin(config, 'klippy_apis')
self._load_plugins(config)
def start(self):
@ -172,19 +179,40 @@ class Server:
continue
try:
decoded_cmd = json.loads(data[:-1])
method = decoded_cmd.get('method')
params = decoded_cmd.get('params', {})
cb = self.remote_methods.get(method)
if cb is not None:
cb(**params)
method = decoded_cmd.get('method', None)
if method is not None:
# This is a remote method called from klippy
cb = self.remote_methods.get(method, None)
if cb is not None:
params = decoded_cmd.get('params', {})
cb(**params)
else:
logging.info(f"Unknown method received: {method}")
continue
# This is a response to a request, process
req_id = decoded_cmd.get('id', None)
request = self.pending_requests.pop(req_id, None)
if request is None:
logging.info(
f"No request matching request ID: {req_id}, "
f"response: {decoded_cmd}")
continue
if 'result' in decoded_cmd:
result = decoded_cmd['result']
if not result:
result = "ok"
else:
logging.info(f"Unknown command received: {data.decode()}")
err = decoded_cmd.get('error', "Malformed Klippy Response")
result = ServerError(err, 400)
request.notify(result)
except Exception:
logging.exception(
f"Error processing Klippy Host Response: {data.decode()}")
def _handle_stream_closed(self):
self.is_klippy_ready = False
self.gc_response_registered = False
self.klippy_state = "disconnected"
self.klippy_iostream = None
self.init_cb.stop()
for request in self.pending_requests.values():
@ -216,30 +244,31 @@ class Server:
self.init_cb.stop()
async def _request_endpoints(self):
try:
result = await self.make_request("list_endpoints", {})
except ServerError:
result = await self.klippy_apis.list_endpoints(default=None)
if result is None:
return
endpoints = result.get('hooks', {})
static_paths = result.get('static_paths', {})
endpoints = result.get('endpoints', {})
for ep in endpoints:
self.moonraker_app.register_remote_handler(ep)
mutable_paths = {sp['resource_id']: sp['file_path']
for sp in static_paths}
file_manager = self.lookup_plugin('file_manager')
file_manager.update_mutable_paths(mutable_paths)
# Subscribe to Gcode Output
if "gcode/subscribe_output" in endpoints and \
not self.gc_response_registered:
try:
await self.klippy_apis.subscribe_gcode_output()
except ServerError as e:
logging.info(
f"{e}\nUnable to register gcode output subscription")
return
self.gc_response_registered = True
async def _check_available_objects(self):
try:
result = await self.make_request("objects/list", {})
except ServerError as e:
result = await self.klippy_apis.get_object_list(default=None)
if result is None:
logging.info(
f"{e}\nUnable to retreive Klipper Object List")
f"Unable to retreive Klipper Object List")
return
missing_objs = []
for obj in ["virtual_sdcard", "display_status", "pause_resume"]:
if obj not in result:
missing_objs.append(obj)
req_objs = set(["virtual_sdcard", "display_status", "pause_resume"])
missing_objs = req_objs - set(result)
if missing_objs:
err_str = ", ".join([f"[{o}]" for o in missing_objs])
logging.info(
@ -249,32 +278,51 @@ class Server:
async def _check_ready(self):
try:
result = await self.make_request("info", {})
result = await self.klippy_apis.get_klippy_info()
except ServerError as e:
logging.info(
f"{e}\nKlippy info request error. This indicates that\n"
f"Klippy may have experienced an error during startup.\n"
f"Please check klippy.log for more information")
return
is_ready = result.get("is_ready", False)
# Update filemanager fixed paths
fixed_paths = {k: result[k] for k in
['klipper_path', 'python_path',
'log_file', 'config_file']}
file_manager = self.lookup_plugin('file_manager')
file_manager.update_fixed_paths(fixed_paths)
is_ready = result.get('state', "") == "ready"
if is_ready:
self._set_klippy_ready()
await self._set_klippy_ready()
else:
msg = result.get("message", "Klippy Not Ready")
msg = result.get('state_message', "Klippy Not Ready")
logging.info("\n" + msg)
def _handle_klippy_response(self, request_id, response):
req = self.pending_requests.pop(request_id, None)
if req is not None:
if isinstance(response, dict) and 'error' in response:
response = ServerError(response['message'], 400)
req.notify(response)
else:
logging.info(f"No request matching response: {response}")
def _set_klippy_ready(self):
async def _set_klippy_ready(self):
logging.info("Klippy ready")
# Update SD Card Path
result = await self.klippy_apis.query_objects(
{'configfile': None}, default=None)
if result is None:
logging.info(f"Unable to set SD Card path")
else:
config = result.get('configfile', {}).get('config', {})
vsd_config = config.get('virtual_sdcard', {})
vsd_path = vsd_config.get('path', None)
if vsd_path is not None:
file_manager = self.lookup_plugin('file_manager')
file_manager.register_directory(
'gcodes', vsd_path, can_delete=True)
else:
logging.info(
"Configuration for [virtual_sdcard] not found,"
" unable to set SD Card path")
# Register "webhooks" subscription
try:
await self.klippy_apis.subscribe_objects({'webhooks': None})
except ServerError as e:
logging.info("Unable to subscribe to webhooks object")
self.klippy_state = "ready"
self.is_klippy_ready = True
self.send_event("server:klippy_state_changed", "ready")
@ -286,10 +334,36 @@ class Server:
def _process_gcode_response(self, response):
self.send_event("server:gcode_response", response)
def _process_status_update(self, status):
def _process_status_update(self, eventtime, status):
if 'webhooks' in status:
# XXX - process other states (startup, ready, error, etc)?
state = status['webhooks'].get('state', None)
if state is not None:
if state == "shutdown":
self._set_klippy_shutdown()
self.klippy_state = state
self.send_event("server:status_update", status)
async def make_request(self, rpc_method, params):
# XXX - This adds the "response_template" to a subscription
# request and tracks all subscriptions so that each
# client gets what its requesting. In the future we should
# track subscriptions per client and send clients only
# the data they are asking for.
if rpc_method == "objects/subscribe":
for obj, items in params.get('objects', {}).items():
if obj in self.all_subscriptions:
pi = self.all_subscriptions[obj]
if items is None or pi is None:
self.all_subscriptions[obj] = None
else:
uitems = list(set(pi) | set(items))
self.all_subscriptions[obj] = uitems
else:
self.all_subscriptions[obj] = items
params['objects'] = dict(self.all_subscriptions)
params['response_template'] = {'method': "process_status_update"}
base_request = BaseRequest(rpc_method, params)
self.pending_requests[base_request.id] = base_request
self.ioloop.spawn_callback(
@ -297,7 +371,7 @@ class Server:
result = await base_request.wait()
return result
async def _kill_server(self):
async def _stop_server(self):
# XXX - Currently this function is not used.
# Should I expose functionality to shutdown
# or restart the server, or simply remove this?
@ -309,20 +383,11 @@ class Server:
if self.klippy_iostream is not None and \
not self.klippy_iostream.closed():
self.klippy_iostream.close()
self.close_server_sock()
if self.server_running:
self.server_running = False
await self.moonraker_app.close()
self.ioloop.stop()
def close_server_sock(self):
try:
self.remove_server_sock()
self.klippy_server_sock.close()
# XXX - remove server sock file (or use abstract?)
except Exception:
logging.exception("Error Closing Server Socket")
# Basic WebRequest class, easily converted to dict for json encoding
class BaseRequest:
def __init__(self, rpc_method, params):

View File

@ -24,7 +24,7 @@ class FileManager:
self.file_lists = {}
self.gcode_metadata = {}
self.metadata_lock = Lock()
self.mutable_path_args = {}
self.fixed_path_args = {}
# Register file management endpoints
self.server.register_endpoint(
@ -49,40 +49,32 @@ class FileManager:
# Register Klippy Configuration Path
config_path = config.get('config_path', None)
if config_path is not None:
ret = self._register_directory(
ret = self.register_directory(
'config', config_path, can_delete=True)
if not ret:
raise config.error(
"Option 'config_path' is not a valid directory")
def update_mutable_paths(self, paths):
# Update paths from Klippy. The sd_path can potentially change
# location on restart.
if paths == self.mutable_path_args:
# No change in mutable paths
def update_fixed_paths(self, paths):
if paths == self.fixed_path_args:
# No change in fixed paths
return
self.mutable_path_args = dict(paths)
self.fixed_path_args = dict(paths)
str_paths = "\n".join([f"{k}: {v}" for k, v in paths.items()])
logging.debug(f"\nUpdating Mutable Paths:\n{str_paths}")
logging.debug(f"\nUpdating Fixed Paths:\n{str_paths}")
# Register directories
sd = paths.pop('sd_path', None)
self._register_directory("gcodes", sd, can_delete=True)
# Register path for example configs
klipper_path = paths.pop('klipper_path', None)
klipper_path = paths.get('klipper_path', None)
if klipper_path is not None:
example_cfg_path = os.path.join(klipper_path, "config")
self._register_directory("config_examples", example_cfg_path)
paths.pop('klippy_env', None)
paths.pop('printer.cfg', None)
self.register_directory("config_examples", example_cfg_path)
# register remaining static files
for pattern, path in paths.items():
if path is not None:
path = os.path.normpath(os.path.expanduser(path))
self.server.register_static_file_handler(pattern, path)
# Register log path
log_file = paths.get('log_file')
log_path = os.path.normpath(os.path.expanduser(log_file))
self.server.register_static_file_handler("klippy.log", log_path)
def _register_directory(self, base, path, can_delete=False):
def register_directory(self, base, path, can_delete=False):
op_check_cb = None
if base == 'gcodes':
op_check_cb = self._handle_operation_check
@ -93,7 +85,9 @@ class FileManager:
if not os.path.isdir(path) or not path.startswith(home) or \
path == home:
logging.info(
f"Supplied path ({path}) for ({base}) not valid")
f"\nSupplied path ({path}) for ({base}) not valid. Please\n"
"check that the path exists and is a subfolder in the HOME\n"
"directory. Note that the path may not BE the home directory.")
return False
if path != self.file_paths.get(base, ""):
self.file_paths[base] = path
@ -109,8 +103,8 @@ class FileManager:
def get_sd_directory(self):
return self.file_paths.get('gcodes', "")
def get_mutable_path_args(self):
return dict(self.mutable_path_args)
def get_fixed_path_args(self):
return dict(self.fixed_path_args)
async def _handle_filelist_request(self, path, method, args):
root = args.get('root', "gcodes")
@ -167,8 +161,8 @@ class FileManager:
async def _handle_operation_check(self, requested_path):
# Get virtual_sdcard status
result = await self.server.make_request(
"objects/status", {'print_stats': []})
klippy_apis = self.server.lookup_plugin('klippy_apis')
result = await klippy_apis.query_objects({'print_stats': None})
pstats = result.get('print_stats', {})
loaded_file = pstats.get('filename', "")
state = pstats.get('state', "")
@ -369,10 +363,9 @@ class FileManager:
self._write_file(upload)
if start_print:
# Make a Klippy Request to "Start Print"
gcode_apis = self.server.lookup_plugin('gcode_apis')
klippy_apis = self.server.lookup_plugin('klippy_apis')
try:
await gcode_apis.gcode_start_print(
request.path, 'POST', {'filename': upload['filename']})
await klippy_apis.start_print(upload['filename'])
except self.server.error:
# Attempt to start print failed
start_print = False