diff --git a/docs/primer_status_log.md b/docs/primer_status_log.md index 19ca632..3dd9c79 100644 --- a/docs/primer_status_log.md +++ b/docs/primer_status_log.md @@ -87,3 +87,21 @@ To import and view a saved ZIP run, showing only the lift and base joints: ```bash stretch_status_viz --import ~/Desktop/stretch_status_2026-05-31.zip --fields robot.lift robot.omnibase ``` + +## Firmware Event Diagnostics + +Power-peripheral firmware has a separate RAM-only event log for bench debugging. This is not part of the normal Stretch Body status logger and is off by default except for rare critical events. To enable noisy diagnostic firmware events for the firmware's fixed auto-expiring window: + +```python +robot.power_periph.enable_event_diagnostics() +robot.push_command() +``` + +To disable the diagnostic window immediately: + +```python +robot.power_periph.disable_event_diagnostics() +robot.push_command() +``` + +Use this only while reproducing a firmware issue. It is intended for BMS, charger, rail, ESP pin, and shutdown-charge debugging and should not be left enabled during timing-sensitive control-loop tests. diff --git a/stretch4_body/robot/robot_client.py b/stretch4_body/robot/robot_client.py index c2d7eaf..e37bdc4 100644 --- a/stretch4_body/robot/robot_client.py +++ b/stretch4_body/robot/robot_client.py @@ -459,6 +459,12 @@ def clear_runstop(self): def trigger_runstop(self): self._queue_command(subsystem="power_periph", command="trigger_runstop") + def enable_event_diagnostics(self): + self._queue_command(subsystem="power_periph", command="enable_event_diagnostics") + + def disable_event_diagnostics(self): + self._queue_command(subsystem="power_periph", command="disable_event_diagnostics") + def set_fan_on(self): """ Turn on the cooling fan. @@ -1439,4 +1445,4 @@ def __init__(self,parent=None): # status=s.pull_status() # print(status) # time.sleep(.01) - # s.stop() \ No newline at end of file + # s.stop() diff --git a/stretch4_body/subsystem/power_periph.py b/stretch4_body/subsystem/power_periph.py index 6b5216e..0a1988c 100644 --- a/stretch4_body/subsystem/power_periph.py +++ b/stretch4_body/subsystem/power_periph.py @@ -324,6 +324,8 @@ def __init__(self): TRIGGER_CPU_PWR_CYCLE = (1 << 21) TRIGGER_ESP32_STATUS_PRINT = (1 << 22) TRIGGER_SLEEP = (1 << 23) + TRIGGER_EVENT_DIAGNOSTICS_ENABLE = (1 << 24) + TRIGGER_EVENT_DIAGNOSTICS_DISABLE = (1 << 25) TRACE_TYPE_STATUS = 0 TRACE_TYPE_DEBUG = 1 @@ -621,6 +623,16 @@ def enable_firmware_trace(self): def disable_firmware_trace(self): self._trigger = self._trigger | self.TRIGGER_DISABLE_TRACE self._dirty_trigger = True + + def enable_event_diagnostics(self): + """Enable firmware event diagnostics for the firmware's fixed auto-expiring window.""" + self._trigger = self._trigger | self.TRIGGER_EVENT_DIAGNOSTICS_ENABLE + self._dirty_trigger = True + + def disable_event_diagnostics(self): + """Disable firmware event diagnostics immediately.""" + self._trigger = self._trigger | self.TRIGGER_EVENT_DIAGNOSTICS_DISABLE + self._dirty_trigger = True def read_firmware_trace(self): self.trace_buf = [] @@ -1414,4 +1426,4 @@ class PowerPeriphStatus(TypedDict): connected_to_firebase: bool adapter_fault: bool adapter_connected: bool - us_loop_time: float \ No newline at end of file + us_loop_time: float diff --git a/test/test_power_periph_event_diagnostics.py b/test/test_power_periph_event_diagnostics.py new file mode 100644 index 0000000..e27f4c5 --- /dev/null +++ b/test/test_power_periph_event_diagnostics.py @@ -0,0 +1,65 @@ +import ast +import pathlib +import unittest + + +ROOT_DIR = pathlib.Path(__file__).resolve().parents[1] +POWER_PERIPH_PATH = ROOT_DIR / "stretch4_body" / "subsystem" / "power_periph.py" +ROBOT_CLIENT_PATH = ROOT_DIR / "stretch4_body" / "robot" / "robot_client.py" + + +def parse(path): + return ast.parse(path.read_text(), filename=str(path)) + + +def get_class(tree, name): + for node in tree.body: + if isinstance(node, ast.ClassDef) and node.name == name: + return node + raise AssertionError(f"missing class {name}") + + +def get_method(class_node, name): + for node in class_node.body: + if isinstance(node, ast.FunctionDef) and node.name == name: + return node + raise AssertionError(f"missing method {class_node.name}.{name}") + + +class TestPowerPeriphEventDiagnostics(unittest.TestCase): + def test_trigger_bits_are_defined(self): + power_periph = get_class(parse(POWER_PERIPH_PATH), "PowerPeriphDefn") + assignments = { + node.targets[0].id: ast.unparse(node.value) + for node in power_periph.body + if isinstance(node, ast.Assign) + and len(node.targets) == 1 + and isinstance(node.targets[0], ast.Name) + } + + self.assertEqual(assignments["TRIGGER_EVENT_DIAGNOSTICS_ENABLE"], "1 << 24") + self.assertEqual(assignments["TRIGGER_EVENT_DIAGNOSTICS_DISABLE"], "1 << 25") + + def test_direct_api_sets_diagnostic_trigger_bits(self): + trace = get_class(parse(POWER_PERIPH_PATH), "PowerPeriphTrace") + enable_source = ast.unparse(get_method(trace, "enable_event_diagnostics")) + disable_source = ast.unparse(get_method(trace, "disable_event_diagnostics")) + + self.assertIn("TRIGGER_EVENT_DIAGNOSTICS_ENABLE", enable_source) + self.assertIn("_dirty_trigger = True", enable_source) + self.assertIn("TRIGGER_EVENT_DIAGNOSTICS_DISABLE", disable_source) + self.assertIn("_dirty_trigger = True", disable_source) + + def test_robot_client_queues_diagnostic_commands(self): + client = get_class(parse(ROBOT_CLIENT_PATH), "PowerPeriphClient") + enable_source = ast.unparse(get_method(client, "enable_event_diagnostics")) + disable_source = ast.unparse(get_method(client, "disable_event_diagnostics")) + + self.assertIn("power_periph", enable_source) + self.assertIn("enable_event_diagnostics", enable_source) + self.assertIn("power_periph", disable_source) + self.assertIn("disable_event_diagnostics", disable_source) + + +if __name__ == "__main__": + unittest.main()