webhooks: Require a subscription to receive gcode output

Add a new "gcode/subscribe_output" webhook endpoint to subscribe to
gcode output.  Only client connections that subscribe to the gcode
output will receive that output.

This also moves all the gcode webhooks from gcode.py to webhooks.py
and arranges for gcode.py to be initialized prior to webhooks.py.

Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
This commit is contained in:
Kevin O'Connor 2020-08-11 21:21:41 -04:00
parent 568393c941
commit 16a53e6918
3 changed files with 53 additions and 32 deletions

View File

@ -78,16 +78,6 @@ class GCodeParser:
self._handle_disconnect) self._handle_disconnect)
printer.register_event_handler("extruder:activate_extruder", printer.register_event_handler("extruder:activate_extruder",
self._handle_activate_extruder) self._handle_activate_extruder)
# Register webhooks
webhooks = self.printer.lookup_object('webhooks')
webhooks.register_endpoint(
"gcode/help", self._handle_remote_help)
webhooks.register_endpoint(
"gcode/script", self._handle_remote_script)
webhooks.register_endpoint(
"gcode/restart", self._handle_remote_restart)
webhooks.register_endpoint(
"gcode/firmware_restart", self._handle_remote_firmware_restart)
# Command handling # Command handling
self.is_printer_ready = False self.is_printer_ready = False
self.mutex = printer.get_reactor().mutex() self.mutex = printer.get_reactor().mutex()
@ -158,6 +148,8 @@ class GCodeParser:
"mux command %s %s %s already registered (%s)" % ( "mux command %s %s %s already registered (%s)" % (
cmd, key, value, prev_values)) cmd, key, value, prev_values))
prev_values[value] = func prev_values[value] = func
def get_command_help(self):
return dict(self.gcode_help)
def register_output_handler(self, cb): def register_output_handler(self, cb):
self.output_callbacks.append(cb) self.output_callbacks.append(cb)
def set_move_transform(self, transform, force=False): def set_move_transform(self, transform, force=False):
@ -617,15 +609,6 @@ class GCodeParser:
if cmd in self.gcode_help: if cmd in self.gcode_help:
cmdhelp.append("%-10s: %s" % (cmd, self.gcode_help[cmd])) cmdhelp.append("%-10s: %s" % (cmd, self.gcode_help[cmd]))
gcmd.respond_info("\n".join(cmdhelp), log=False) gcmd.respond_info("\n".join(cmdhelp), log=False)
# Webhooks
def _handle_remote_help(self, web_request):
web_request.send(dict(self.gcode_help))
def _handle_remote_restart(self, web_request):
self.run_script('restart')
def _handle_remote_firmware_restart(self, web_request):
self.run_script('firmware_restart')
def _handle_remote_script(self, web_request):
self.run_script(web_request.get('script'))
# Support reading gcode from a pseudo-tty interface # Support reading gcode from a pseudo-tty interface
class GCodeIO: class GCodeIO:

View File

@ -59,7 +59,7 @@ class Printer:
self.event_handlers = {} self.event_handlers = {}
self.objects = collections.OrderedDict() self.objects = collections.OrderedDict()
# Init printer components that must be setup prior to config # Init printer components that must be setup prior to config
for m in [webhooks, gcode]: for m in [gcode, webhooks]:
m.add_early_printer_objects(self) m.add_early_printer_objects(self)
def get_start_args(self): def get_start_args(self):
return self.start_args return self.start_args

View File

@ -45,12 +45,16 @@ class Sentinel:
class WebRequest: class WebRequest:
error = WebRequestError error = WebRequestError
def __init__(self, base_request): def __init__(self, client_conn, base_request):
self.client_conn = client_conn
self.id = base_request['id'] self.id = base_request['id']
self.path = base_request['path'] self.path = base_request['path']
self.args = base_request['args'] self.args = base_request['args']
self.response = None self.response = None
def get_client_connection(self):
return self.client_conn
def get(self, item, default=Sentinel): def get(self, item, default=Sentinel):
if item not in self.args: if item not in self.args:
if default == Sentinel: if default == Sentinel:
@ -170,6 +174,9 @@ class ClientConnection:
pass pass
self.server.pop_client(self.uid) self.server.pop_client(self.uid)
def is_closed(self):
return self.fd_handle is None
def process_received(self, eventtime): def process_received(self, eventtime):
try: try:
data = self.sock.recv(4096) data = self.sock.recv(4096)
@ -191,7 +198,7 @@ class ClientConnection:
logging.debug( logging.debug(
"webhooks: Request received: %s" % (req)) "webhooks: Request received: %s" % (req))
try: try:
web_request = WebRequest(json_loads_byteified(req)) web_request = WebRequest(self, json_loads_byteified(req))
except Exception: except Exception:
logging.exception( logging.exception(
"webhooks: Error decoding Server Request %s" "webhooks: Error decoding Server Request %s"
@ -256,22 +263,12 @@ class WebHooks:
self.sconn = ServerSocket(self, printer) self.sconn = ServerSocket(self, printer)
# Register Events # Register Events
printer.register_event_handler(
"klippy:connect", self._handle_connect)
printer.register_event_handler( printer.register_event_handler(
"klippy:shutdown", self._notify_shutdown) "klippy:shutdown", self._notify_shutdown)
def _handle_connect(self):
gcode = self.printer.lookup_object('gcode')
gcode.register_output_handler(self._process_gcode_response)
def _notify_shutdown(self): def _notify_shutdown(self):
self.call_remote_method("set_klippy_shutdown") self.call_remote_method("set_klippy_shutdown")
def _process_gcode_response(self, gc_response):
self.call_remote_method(
"process_gcode_response", response=gc_response)
def register_endpoint(self, path, callback): def register_endpoint(self, path, callback):
if path in self._endpoints: if path in self._endpoints:
raise WebRequestError("Path already registered to an endpoint") raise WebRequestError("Path already registered to an endpoint")
@ -318,6 +315,46 @@ class WebHooks:
"action_call_remote_method": self._action_call_remote_method "action_call_remote_method": self._action_call_remote_method
} }
class GCodeHelper:
def __init__(self, printer):
self.printer = printer
self.gcode = printer.lookup_object("gcode")
# Output subscription tracking
self.is_output_registered = False
self.clients = {}
# Register webhooks
wh = printer.lookup_object('webhooks')
wh.register_endpoint("gcode/help", self._handle_help)
wh.register_endpoint("gcode/script", self._handle_script)
wh.register_endpoint("gcode/restart", self._handle_restart)
wh.register_endpoint("gcode/firmware_restart",
self._handle_firmware_restart)
wh.register_endpoint("gcode/subscribe_output",
self._handle_subscribe_output)
def _handle_help(self, web_request):
web_request.send(self.gcode.get_command_help())
def _handle_script(self, web_request):
self.gcode.run_script(web_request.get('script'))
def _handle_restart(self, web_request):
self.gcode.run_script('restart')
def _handle_firmware_restart(self, web_request):
self.gcode.run_script('firmware_restart')
def _output_callback(self, msg):
for cconn, template in list(self.clients.items()):
if cconn.is_closed():
del self.clients[cconn]
continue
tmp = dict(template)
tmp['params'] = {'response': msg}
cconn.send(tmp)
def _handle_subscribe_output(self, web_request):
cconn = web_request.get_client_connection()
template = web_request.get('response_template', {})
self.clients[cconn] = template
if not self.is_output_registered:
self.gcode.register_output_handler(self._output_callback)
self.is_output_registered = True
SUBSCRIPTION_REFRESH_TIME = .25 SUBSCRIPTION_REFRESH_TIME = .25
class StatusHandler: class StatusHandler:
@ -443,4 +480,5 @@ class StatusHandler:
def add_early_printer_objects(printer): def add_early_printer_objects(printer):
printer.add_object('webhooks', WebHooks(printer)) printer.add_object('webhooks', WebHooks(printer))
GCodeHelper(printer)
StatusHandler(printer) StatusHandler(printer)