From 2fdd8a420d6e313d6261642c0505126869d017d2 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 21 Jul 2021 12:48:23 -0400 Subject: [PATCH] motion_report: Add support for dumping steps/trapq via API server Signed-off-by: Kevin O'Connor --- docs/API_Server.md | 43 +++++++++++ klippy/extras/motion_report.py | 130 +++++++++++++++++++++++++++++++-- 2 files changed, 168 insertions(+), 5 deletions(-) diff --git a/docs/API_Server.md b/docs/API_Server.md index b8419a8d..77470137 100644 --- a/docs/API_Server.md +++ b/docs/API_Server.md @@ -278,6 +278,49 @@ window" interface. Parsing content from the G-Code terminal output is discouraged. Use the "objects/subscribe" endpoint to obtain updates on Klipper's state. +### motion_report/dump_stepper + +This endpoint is used to subscribe to Klipper's internal stepper +queue_step command stream for a stepper. Obtaining these low-level +motion updates may be useful for diagnostic and debugging +purposes. Using this endpoint may increase Klipper's system load. + +A request may look like: +`{"id": 123, "method":"motion_report/dump_stepper", +"params": {"name": "stepper_x", "response_template": {}}}` +and might return: +`{"id": 123, "result": {"header": ["interval", "count", "add"]}}` +and might later produce asynchronous messages such as: +`{"params": {"first_clock": 179601081, "first_time": 8.98, +"first_position": 0, "last_clock": 219686097, "last_time": 10.984, +"data": [[179601081, 1, 0], [29573, 2, -8685], [16230, 4, -1525], +[10559, 6, -160], [10000, 976, 0], [10000, 1000, 0], [10000, 1000, 0], +[10000, 1000, 0], [9855, 5, 187], [11632, 4, 1534], [20756, 2, 9442]]}}` + +The "header" field in the initial query response is used to describe +the fields found in later "data" responses. + +### motion_report/dump_trapq + +This endpoint is used to subscribe to Klipper's internal "trapezoid +motion queue". Obtaining these low-level motion updates may be useful +for diagnostic and debugging purposes. Using this endpoint may +increase Klipper's system load. + +A request may look like: +`{"id": 123, "method": "motion_report/dump_trapq", "params": +{"name": "toolhead", "response_template":{}}}` +and might return: +`{"id": 1, "result": {"header": ["time", "duration", +"start_velocity", "acceleration", "start_position", "direction"]}}` +and might later produce asynchronous messages such as: +`{"params": {"data": [[4.05, 1.0, 0.0, 0.0, [300.0, 0.0, 0.0], +[0.0, 0.0, 0.0]], [5.054, 0.001, 0.0, 3000.0, [300.0, 0.0, 0.0], +[-1.0, 0.0, 0.0]]]}}` + +The "header" field in the initial query response is used to describe +the fields found in later "data" responses. + ### pause_resume/cancel This endpoint is similar to running the "PRINT_CANCEL" G-Code command. diff --git a/klippy/extras/motion_report.py b/klippy/extras/motion_report.py index ce7486e4..439c30c6 100644 --- a/klippy/extras/motion_report.py +++ b/klippy/extras/motion_report.py @@ -6,11 +6,79 @@ import logging import chelper +API_UPDATE_INTERVAL = 0.500 + +# Helper to periodically transmit data to a set of API clients +class APIDumpHelper: + def __init__(self, printer, data_cb, startstop_cb=None, + update_interval=API_UPDATE_INTERVAL): + self.printer = printer + self.data_cb = data_cb + if startstop_cb is None: + startstop_cb = (lambda is_start: None) + self.startstop_cb = startstop_cb + self.update_interval = update_interval + self.update_timer = None + self.clients = {} + def _stop(self): + self.clients.clear() + if self.update_timer is None: + return + reactor = self.printer.get_reactor() + reactor.unregister_timer(self.update_timer) + self.update_timer = None + try: + self.startstop_cb(False) + except self.printer.command_error as e: + logging.exception("API Dump Helper stop callback error") + return reactor.NEVER + def _start(self): + if self.update_timer is not None: + return + try: + self.startstop_cb(True) + except self.printer.command_error as e: + logging.exception("API Dump Helper start callback error") + self._stop() + return + reactor = self.printer.get_reactor() + systime = reactor.monotonic() + waketime = systime + self.update_interval + self.update_timer = reactor.register_timer(self._update, waketime) + def add_client(self, web_request): + cconn = web_request.get_client_connection() + template = web_request.get_dict('response_template', {}) + self.clients[cconn] = template + self._start() + def _update(self, eventtime): + try: + msg = self.data_cb(eventtime) + except self.printer.command_error as e: + logging.exception("API Dump Helper data callback error") + return self._stop() + if not msg: + return eventtime + self.update_interval + for cconn, template in list(self.clients.items()): + if cconn.is_closed(): + del self.clients[cconn] + if not self.clients: + return self._stop() + continue + tmp = dict(template) + tmp['params'] = msg + cconn.send(tmp) + return eventtime + self.update_interval + # Extract stepper queue_step messages class DumpStepper: def __init__(self, printer, mcu_stepper): self.printer = printer self.mcu_stepper = mcu_stepper + self.last_api_clock = 0 + self.api_dump = APIDumpHelper(printer, self._api_update) + wh = self.printer.lookup_object('webhooks') + wh.register_mux_endpoint("motion_report/dump_stepper", "name", + mcu_stepper.get_name(), self._add_api_client) def get_step_queue(self, start_clock, end_clock): mcu_stepper = self.mcu_stepper res = [] @@ -36,6 +104,30 @@ class DumpStepper: % (i, s.first_clock, s.start_position, s.interval, s.step_count, s.add)) logging.info('\n'.join(out)) + def _api_update(self, eventtime): + data, cdata = self.get_step_queue(self.last_api_clock, 1<<63) + if not data: + return {} + clock_to_print_time = self.mcu_stepper.get_mcu().clock_to_print_time + first = data[0] + first_clock = first.first_clock + first_time = clock_to_print_time(first_clock) + self.last_api_clock = last_clock = data[-1].last_clock + last_time = clock_to_print_time(last_clock) + mcu_pos = first.start_position + start_position = self.mcu_stepper.mcu_to_commanded_position(mcu_pos) + step_dist = self.mcu_stepper.get_step_dist() + if self.mcu_stepper.is_dir_inverted(): + step_dist = -step_dist + d = [(s.interval, s.step_count, s.add) for s in data] + return {"data": d, "start_position": start_position, + "start_mcu_position": mcu_pos, "step_distance": step_dist, + "first_clock": first_clock, "first_step_time": first_time, + "last_clock": last_clock, "last_step_time": last_time} + def _add_api_client(self, web_request): + self.api_dump.add_client(web_request) + hdr = ('interval', 'count', 'add') + web_request.send({'header': hdr}) NEVER_TIME = 9999999999999999. @@ -45,7 +137,12 @@ class DumpTrapQ: self.printer = printer self.name = name self.trapq = trapq - def get_trapq(self, start_time, end_time): + self.last_api_msg = (0., 0.) + self.api_dump = APIDumpHelper(printer, self._api_update) + wh = self.printer.lookup_object('webhooks') + wh.register_mux_endpoint("motion_report/dump_trapq", "name", name, + self._add_api_client) + def extract_trapq(self, start_time, end_time): ffi_main, ffi_lib = chelper.get_ffi() res = [] while 1: @@ -83,6 +180,23 @@ class DumpTrapQ: move.start_z + move.z_r * dist) velocity = move.start_v + move.accel * move_time return pos, velocity + def _api_update(self, eventtime): + qtime = self.last_api_msg[0] + min(self.last_api_msg[1], 0.100) + data, cdata = self.extract_trapq(qtime, NEVER_TIME) + d = [(m.print_time, m.move_t, m.start_v, m.accel, + (m.start_x, m.start_y, m.start_z), (m.x_r, m.y_r, m.z_r)) + for m in data] + if d and d[0] == self.last_api_msg: + d.pop(0) + if not d: + return {} + self.last_api_msg = d[-1] + return {"data": d} + def _add_api_client(self, web_request): + self.api_dump.add_client(web_request) + hdr = ('time', 'duration', 'start_velocity', 'acceleration', + 'start_position', 'direction') + web_request.send({'header': hdr}) STATUS_REFRESH_TIME = 0.250 @@ -94,8 +208,11 @@ class PrinterMotionReport: # get_status information self.next_status_time = 0. gcode = self.printer.lookup_object('gcode') - self.last_status = {'live_position': gcode.Coord(0., 0., 0., 0.), - 'live_velocity': 0., 'live_extruder_velocity': 0.} + self.last_status = { + 'live_position': gcode.Coord(0., 0., 0., 0.), + 'live_velocity': 0., 'live_extruder_velocity': 0., + 'steppers': [], 'trapq': [], + } # Register handlers self.printer.register_event_handler("klippy:connect", self._connect) self.printer.register_event_handler("klippy:shutdown", self._shutdown) @@ -117,6 +234,9 @@ class PrinterMotionReport: break etrapq = extruder.get_trapq() self.trapqs[ename] = DumpTrapQ(self.printer, ename, etrapq) + # Populate 'trapq' and 'steppers' in get_status result + self.last_status['steppers'] = list(sorted(self.steppers.keys())) + self.last_status['trapq'] = list(sorted(self.trapqs.keys())) # Shutdown handling def _dump_shutdown(self, eventtime): # Log stepper queue_steps on mcu that started shutdown (if any) @@ -136,8 +256,8 @@ class PrinterMotionReport: return # Log trapqs around time of shutdown for dtrapq in self.trapqs.values(): - data, cdata = dtrapq.get_trapq(shutdown_time - .100, - shutdown_time + .100) + data, cdata = dtrapq.extract_trapq(shutdown_time - .100, + shutdown_time + .100) dtrapq.log_trapq(data) # Log estimated toolhead position at time of shutdown dtrapq = self.trapqs.get('toolhead')