diff --git a/scripts/ckpt/bench/runner.py b/scripts/ckpt/bench/runner.py index 0a36080e..fbcece80 100644 --- a/scripts/ckpt/bench/runner.py +++ b/scripts/ckpt/bench/runner.py @@ -138,19 +138,26 @@ def optional_saleae(compile_only_warning: str) -> Iterator[Manager | None]: Logs *compile_only_warning* and yields None when no device is detected. The manager is closed on exit. + + The Otii switchboard relays are closed first: with the intermittent-power + rig wired up they carry the ez-FET's SBW and 3V3 lines, and probing for + the device before closing them would find nothing. """ - manager: Manager | None = None - if check_device_available(): - from ..device.saleae import discover_saleae + from ..device.otii import debugger_connection - manager = discover_saleae() - else: - logger.warning(compile_only_warning) - try: - yield manager - finally: - if manager is not None: - manager.close() + with debugger_connection(): + manager: Manager | None = None + if check_device_available(): + from ..device.saleae import discover_saleae + + manager = discover_saleae() + else: + logger.warning(compile_only_warning) + try: + yield manager + finally: + if manager is not None: + manager.close() def measure_execution_time( diff --git a/scripts/ckpt/device/otii.py b/scripts/ckpt/device/otii.py index ae0f6e41..4735ebaa 100644 --- a/scripts/ckpt/device/otii.py +++ b/scripts/ckpt/device/otii.py @@ -6,6 +6,9 @@ from the debugger while it runs on replayed power. Benchmark completion and timing are measured by the Saleae (see device/saleae.py), not by the Otii. +Continuous-power runs (``ckpt bench``, ``ckpt verify``) use the same wiring +but need the opposite relay state; see :func:`debugger_connection`. + Requires the ``otii-tcp-client`` package (``uv sync --extra otii``) and the ``otii_server`` binary (path via the ``OTII_SERVER_BIN`` environment variable). @@ -20,7 +23,7 @@ import threading import time from collections.abc import Iterator -from contextlib import contextmanager +from contextlib import ExitStack, contextmanager from dataclasses import dataclass from typing import Any @@ -153,10 +156,11 @@ def _single_arc(otii: Any, arc_cls: Any) -> Any: @contextmanager -def otii_session() -> Iterator[OtiiSession]: - """Start otii_server, connect, and configure the device for replay runs. +def _otii_device() -> Iterator[OtiiSession]: + """Start otii_server, connect, and configure the device. - On exit: main power off, switchboard relays opened, server stopped. + On exit: main power off, client and server shut down. The switchboard + relays are left as the caller set them. """ otii_client, arc_cls = _import_otii() server = _start_server() @@ -189,20 +193,53 @@ def otii_session() -> Iterator[OtiiSession]: arc.set_main(False) except Exception: logger.exception("Error switching off Otii main power") + if otii is not None: + try: + otii.shutdown() + except Exception: + logger.exception("Error shutting down Otii client") + _stop_server(server) + + +@contextmanager +def otii_session() -> Iterator[OtiiSession]: + """An Otii device session for replay runs; the relays are opened on exit.""" + with _otii_device() as session: + try: + yield session + finally: try: - arc.set_gpo(_RELAY_GPO, False) + session.arc.set_gpo(_RELAY_GPO, False) logger.info( "Switchboard relays left open — the ez-FET is disconnected " "until the next intermittent run closes them" ) except Exception: logger.exception("Error opening switchboard relays") - if otii is not None: - try: - otii.shutdown() - except Exception: - logger.exception("Error shutting down Otii client") - _stop_server(server) + + +@contextmanager +def debugger_connection() -> Iterator[None]: + """Hold the switchboard relays closed for a continuous-power run. + + ``ckpt bench`` and ``ckpt verify`` power the target from the ez-FET's 3V3 + rail, which reaches the board through the same relays an intermittent run + opens. Closing them here means neither command needs the board rewired. + + A no-op when no Otii is reachable: the board is then wired directly to the + ez-FET. The relays stay closed on exit — only an intermittent run opens + them. + """ + with ExitStack() as stack: + try: + connect_debugger(stack.enter_context(_otii_device())) + except DeviceError as exc: + logger.info( + "No Otii switchboard in the loop (%s); assuming a direct " + "ez-FET connection", + exc, + ) + yield def connect_debugger(session: OtiiSession) -> None: diff --git a/scripts/ckpt/verify/common.py b/scripts/ckpt/verify/common.py index cf924432..11d9522f 100644 --- a/scripts/ckpt/verify/common.py +++ b/scripts/ckpt/verify/common.py @@ -11,6 +11,7 @@ import logging import time from collections.abc import Callable +from contextlib import closing from dataclasses import dataclass from enum import Enum from pathlib import Path @@ -29,6 +30,7 @@ compile_uninstrumented, ) from ..device.flash import check_region_violation, raise_if_region_violation, read_nvm +from ..device.otii import debugger_connection from ..device.saleae import discover_saleae, saleae_run from ..env import ProjectEnv from ..errors import CompilationError, ConfigError, DeviceError, RegionViolationError @@ -123,8 +125,10 @@ def verify_algorithms( if not bench_files: raise ConfigError("No benchmarks to verify") - saleae_manager = discover_saleae() - try: + # The relays must close before the Saleae/device probing below: with the + # intermittent-power rig wired up they carry the ez-FET's SBW and 3V3 + # lines to the target. + with debugger_connection(), closing(discover_saleae()) as saleae_manager: capacitors = discover_capacitors(env, algorithms[0].name, caps) results: dict[str, list[BenchResult]] = {spec.name: [] for spec in algorithms} @@ -191,8 +195,6 @@ def verify_algorithms( _print_summary(results[spec.name], spec.name, halt_mode) return results - finally: - saleae_manager.close() def all_ok(results: list[BenchResult]) -> bool: