From 01e39722d380f4a85f09d16ef5d1f715a9e2953c Mon Sep 17 00:00:00 2001 From: Henrik Muehe Date: Sun, 3 May 2026 17:44:36 +0200 Subject: [PATCH] Jog: detailed event/state logging + dry-run env var Adds visibility into the gamepad event path so future regressions can be diagnosed without the gantry attached. AJOG EV logs every incoming KEY event and any ABS event matching the trigger codes; AJOG STATE logs every transition; the would-be JOG / JOGSTOP is also logged. BBCTRL_AJOG_DRYRUN=1 in the bbctrl env disables actuation while keeping the logging, so the host-side state machine can be tested without driving the ESP. Default is live actuation (dry-run off). Used this to prove the host side was correct on hardware where the firmware bug was hiding -- pendant taps produced perfect press/release pairs at ~200 ms while the ESP was the one ignoring JOGSTOP. --- src/py/bbctrl/Jog.py | 59 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/py/bbctrl/Jog.py b/src/py/bbctrl/Jog.py index 7d7c757..8095255 100644 --- a/src/py/bbctrl/Jog.py +++ b/src/py/bbctrl/Jog.py @@ -25,12 +25,21 @@ # # ################################################################################ +import os import threading +import time import inevent from inevent.Constants import * +# Set to True (or BBCTRL_AJOG_DRYRUN=1 in env) to log press/release +# events and would-be ESP commands without actually sending JOG / +# JOGSTOP. Useful for debugging the gamepad event path without +# touching the gantry. Defaults to live actuation. +A_DRY_RUN = os.environ.get('BBCTRL_AJOG_DRYRUN', '') == '1' + + # Listen for input events class Jog(inevent.JogHandler): def __init__(self, ctrl): @@ -94,6 +103,12 @@ class Jog(inevent.JogHandler): def _a_stop(self): ext = getattr(self.ctrl, 'ext_axis', None) + ext_state = ('present' if (ext is not None and ext.enabled) + else 'unavailable') + if A_DRY_RUN: + self.log.info('AJOG DRYRUN _a_stop ext=%s (would send JOGSTOP)', + ext_state) + return if ext is None or not ext.enabled: return try: @@ -103,6 +118,22 @@ class Jog(inevent.JogHandler): def _a_start(self, direction): ext = getattr(self.ctrl, 'ext_axis', None) + ext_state = ('present' if (ext is not None and ext.enabled) + else 'unavailable') + if A_DRY_RUN: + scale = self._a_speed_scale() + try: + step_max = (int(ext.aux._cfg['step_max_sps']) + if ext is not None and ext.enabled else -1) + accel = (int(ext.aux._cfg['step_accel_sps2']) + if ext is not None and ext.enabled else -1) + except Exception: + step_max, accel = -1, -1 + self.log.info( + 'AJOG DRYRUN _a_start dir=%+d ext=%s speed=%d scale=%.4f ' + 'step_max=%d accel=%d (would send JOG)', + direction, ext_state, self.speed, scale, step_max, accel) + return if ext is None or not ext.enabled or direction == 0: return scale = self._a_speed_scale() @@ -154,6 +185,28 @@ class Jog(inevent.JogHandler): cfg = self.get_config(dev_name) old = self.a_button + # DEBUG: log EVERY incoming gamepad event so we can see + # exactly what the pendant is producing on press/release. + # Skip noisy stick / report-syn events to keep the journal + # readable but log all KEY events and any ABS event whose + # code matches one we care about. + try: + tname = ev_type_name.get(event.type, '?') + except Exception: + tname = '?' + if event.type == EV_KEY: + self.log.info( + 'AJOG EV dev=%r type=%s(%d) code=0x%x val=%d ' + 'cfg.a_pos_btn=0x%x cfg.a_neg_btn=0x%x', + dev_name, tname, event.type, event.code, event.value, + cfg.get('a_pos_btn', 0), cfg.get('a_neg_btn', 0)) + elif event.type == EV_ABS and event.code in ( + cfg.get('a_neg_abs', -1), + cfg.get('a_pos_abs', -1)): + self.log.info( + 'AJOG EV dev=%r type=%s(%d) code=0x%x val=%d (trigger ABS)', + dev_name, tname, event.type, event.code, event.value) + if event.type == EV_KEY: if event.code == cfg.get('a_pos_btn'): if event.value: self.a_button = 1 @@ -169,13 +222,15 @@ class Jog(inevent.JogHandler): elif self.a_button == -1: self.a_button = 0 if self.a_button != old: - self.log.info('A-axis trigger -> %s', self.a_button) + self.log.info( + 'AJOG STATE %+d -> %+d (t=%.3f dry_run=%s)', + old, self.a_button, time.monotonic(), A_DRY_RUN) self._a_apply(self.a_button, old) # On every release pull a fresh position mirror in case # the user does a gplan-driven A move next. The terminal # [jog] done line itself already updates aux._pos_steps; # this propagates that into ExternalAxis._pos_mm. - if self.a_button == 0: + if self.a_button == 0 and not A_DRY_RUN: # Wait briefly so the [jog] done line has time to # arrive before we read aux.position_mm. self.ctrl.ioloop.call_later(0.2, self._a_resync_pos)