Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/primer_status_log.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 7 additions & 1 deletion stretch4_body/robot/robot_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -1439,4 +1445,4 @@ def __init__(self,parent=None):
# status=s.pull_status()
# print(status)
# time.sleep(.01)
# s.stop()
# s.stop()
14 changes: 13 additions & 1 deletion stretch4_body/subsystem/power_periph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -1414,4 +1426,4 @@ class PowerPeriphStatus(TypedDict):
connected_to_firebase: bool
adapter_fault: bool
adapter_connected: bool
us_loop_time: float
us_loop_time: float
65 changes: 65 additions & 0 deletions test/test_power_periph_event_diagnostics.py
Original file line number Diff line number Diff line change
@@ -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()