Skip to content

feat(trigger): add TrainCount + trigger_status for polled progress - #39

Merged
peterhollender merged 1 commit into
feature/fda_versionfrom
feature/trigger-status-and-train-count
Oct 1, 2026
Merged

peterhollender merged 1 commit into
feature/fda_versionfrom
feature/trigger-status-and-train-count

Conversation

@openlifu-agent

Copy link
Copy Markdown
Contributor

Companion to OpenwaterHealth/openlifu-operator-interface#78 (PROJ-08 Task 1: Running-state ProgressBar + firmware-driven Running→Finished transition).

The walking-skeleton OI deliberately dropped async telemetry / STATUS-frame subscriptions in openlifu-operator-interface#92; reintroducing one for sonication progress would undo that work. A polled model fits the sync-probe idiom the OI state machine already uses for connect state and firmware compat.

What changes

Sim SimulatedTxDevice:

  • __init__ adds self._train_curr = 0 — the field a polled host reads.
  • get_trigger_json() adds "TrainCount": self._train_curr.
  • get_trigger() is new — mirrors the shape of LIFUTXDevice.TxDevice.get_trigger() so OI code calling interface.txdevice.get_trigger() gets identical keys against sim and real. Adds two polling fields: train_count and trigger_status.

Sim engine (_SimulatedRunEngine):

  • start() resets self._tx._train_curr = 0 so progress starts from 0 on every fresh sonication.
  • _on_train_tick mirrors self._train_curr back to self._tx._train_curr after each increment.
  • _on_train_tick natural-completion branch now calls self._tx.stop_trigger(). Bug fix: pre-2026-10-01 only an explicit stop_sonication flipped _trigger_running to False, so a polled host saw TriggerStatus=RUNNING after the sim finished naturally. The emitted STATUS frame already carried status=STOPPED so OWSignal subscribers weren't affected; polled callers (new) were.

Real LIFUTXDevice.get_trigger():

  • Extends the returned dict with:
    • "train_count": trigger_json.get("TrainCount", 0)
    • "trigger_status": trigger_json.get("TriggerStatus", "UNKNOWN")
  • Both use .get(default) so pre-PROJ-04 firmware (which doesn't yet populate TrainCount) returns 0 cleanly. The firmware-side OW_CTRL_GET_SWTRIG response-payload extension to actually populate the field is a PROJ-04 companion on openlifu-transmitter-fw — not blocking the OI walking skeleton since OI runs against the sim pre-PROJ-04.

Tests

New: unit-test/test_simulated_trigger_progress.py — 7 tests:

  • get_trigger_json / get_trigger initial state (defaults to TrainCount=0, TriggerStatus=STOPPED)
  • get_trigger snake-case shape (every expected key present)
  • train_count monotonically advances during a short synthetic sequence (5 trains × 20 ms), reaches pulse_train_count on natural completion
  • Natural completion flips trigger_status to STOPPED (regression test for the pre-fix bug)
  • Operator-initiated stop_sonication midway: trigger_status flips to STOPPED, train_count retained at partial progress
  • Restart resets train_count to 0 at engine start()

Full SDK suite (excluding pre-existing test_tx_device.py collection failure on unrelated HW_ID_DATA_LENGTH import): 107 passed.

Refs OpenwaterHealth/openlifu-operator-interface#78

Self-review

  • Assumption I'm making: firmware-side TrainCount population is a safe PROJ-04 follow-up. The .get("TrainCount", 0) default on the real wrapper means OI code gets a safe (0) value against pre-PROJ-04 firmware; nothing breaks, the ProgressBar just doesn't fill against real hardware until PROJ-04 lands. If that's not the right default behavior, I'd rather raise explicitly at the SDK boundary than silently return 0 — ping if preferred.
  • Alternative considered: have the engine always keep _trigger_running in sync (write it alongside _train_curr on every tick) instead of calling stop_trigger() on natural completion. Rejected — stop_trigger() is the semantic reset and other code paths rely on it being the sole writer; calling it from the engine mirrors the semantics of the operator-initiated stop path, keeping the two edges identical from the TxDevice's point of view.
  • New dependency introduced: none.
  • Test coverage: SR-N/A — this is an SDK plumbing PR. Coverage is behavioral (progress monotonic, status flips on both edges, restart resets). Not covered: real-hardware polling against actual firmware (deferred to PROJ-04 companion).
  • Breaking change: non-breaking additive. Existing callers of get_trigger_json() get an extra TrainCount key; existing callers of get_trigger() get two extra snake_case keys. Existing OWSignal-based callers unaffected. The natural-completion trigger_status flip is a bug fix observable only by polled callers (previously there were none).
  • Tech-debt filed: none. The firmware-side TrainCount population is explicit PROJ-04 work, not debt.

Lets the operator interface poll sonication progress against
``TxDevice.get_trigger()`` instead of subscribing to the async
``signal_data_received`` STATUS-frame OWSignal. Companion to
openlifu-operator-interface#78 (PROJ-08 Task 1: Running-state
ProgressBar + firmware-driven Running->Finished transition).

The walking-skeleton OI deliberately dropped async telemetry /
status-frame subscriptions in openlifu-operator-interface#92;
reintroducing one for sonication progress would undo that work.
A polled model fits the sync-probe idiom the OI state machine
already uses for connect state and firmware compat.

## Changes

### Simulated TxDevice

* ``SimulatedTxDevice.__init__`` adds ``self._train_curr = 0``
  so a polled host can read progress without subscribing to the
  OWSignal. Written by the engine on each tick.
* ``SimulatedTxDevice.get_trigger_json()`` adds ``"TrainCount":
  self._train_curr`` to its returned dict.
* ``SimulatedTxDevice.get_trigger()`` is new -- mirrors the shape
  of the real ``LIFUTXDevice.TxDevice.get_trigger()`` so OI code
  calling ``interface.txdevice.get_trigger()`` gets the same keys
  against sim and real hardware. Adds two walking-skeleton
  polling fields: ``train_count`` (current running counter) and
  ``trigger_status`` (``"RUNNING"`` / ``"STOPPED"``).

### Sim engine

* ``_SimulatedRunEngine.start()`` resets ``self._tx._train_curr =
  0`` so a polled host sees progress starting from 0 on every
  fresh sonication (previously the counter carried over from a
  prior run if the host polled before the first tick).
* ``_SimulatedRunEngine._on_train_tick`` mirrors
  ``self._train_curr`` back to ``self._tx._train_curr`` after
  each increment -- the field the polled host reads.
* ``_SimulatedRunEngine._on_train_tick`` natural-completion
  branch now calls ``self._tx.stop_trigger()`` (previously only
  an explicit ``stop_sonication`` flipped ``_trigger_running``
  to False, so a polled host saw ``TriggerStatus=RUNNING`` after
  the sim finished naturally). The emitted STATUS frame already
  carried ``status=STOPPED`` so callers that subscribed to the
  OWSignal weren't affected by the bug; polled callers were.

### Real TxDevice

* ``LIFUTXDevice.get_trigger()`` extends its return dict with
  the same two fields:
  * ``"train_count": trigger_json.get("TrainCount", 0)``
  * ``"trigger_status": trigger_json.get("TriggerStatus",
    "UNKNOWN")``
  Both use ``.get(default)`` so pre-PROJ-04 firmware (which
  doesn't yet populate ``TrainCount``) returns 0 cleanly. The
  firmware-side ``OW_CTRL_GET_SWTRIG`` response-payload
  extension to actually populate the field is tracked
  separately as a PROJ-04 companion on
  openlifu-transmitter-fw -- not blocking the OI walking
  skeleton since OI runs against the sim pre-PROJ-04.

## Tests

New: ``unit-test/test_simulated_trigger_progress.py`` -- 7 tests
exercising the sim-side changes end-to-end:

* ``get_trigger_json`` / ``get_trigger`` initial state (both
  ``TrainCount`` / ``train_count`` default to 0, ``TriggerStatus``
  / ``trigger_status`` default to ``STOPPED``).
* ``get_trigger`` snake-case shape check (every expected key
  present).
* ``train_count`` advances monotonically during a short synthetic
  sequence (5 pulse trains * 20 ms), reaches ``pulse_train_count``
  on natural completion.
* Natural completion flips ``trigger_status`` to ``STOPPED`` --
  regression test for the pre-2026-10-01 bug where
  ``_trigger_running`` wasn't flipped on natural end.
* Operator-initiated ``stop_sonication`` midway: ``trigger_status``
  flips to ``STOPPED``, ``train_count`` retained at partial
  progress (not reset to 0, not advanced to total).
* Restart resets ``train_count`` to 0 at engine ``start()``.

Full suite (excluding pre-existing ``test_tx_device.py`` collection
failure on an unrelated ``HW_ID_DATA_LENGTH`` import): 107 passed.

Refs OpenwaterHealth/openlifu-operator-interface#78

Signed-off-by: Peter Hollender <peterhollender@gmail.com>
@peterhollender
peterhollender force-pushed the feature/trigger-status-and-train-count branch from 683b5f2 to 4701082 Compare October 1, 2026 15:49
@peterhollender
peterhollender merged commit 6f27280 into feature/fda_version Oct 1, 2026
2 checks passed
@peterhollender
peterhollender deleted the feature/trigger-status-and-train-count branch October 1, 2026 15:50
peterhollender added a commit that referenced this pull request Oct 9, 2026
Lets the operator interface poll sonication progress against
``TxDevice.get_trigger()`` instead of subscribing to the async
``signal_data_received`` STATUS-frame OWSignal. Companion to
openlifu-operator-interface#78 (PROJ-08 Task 1: Running-state
ProgressBar + firmware-driven Running->Finished transition).

The walking-skeleton OI deliberately dropped async telemetry /
status-frame subscriptions in openlifu-operator-interface#92;
reintroducing one for sonication progress would undo that work.
A polled model fits the sync-probe idiom the OI state machine
already uses for connect state and firmware compat.

## Changes

### Simulated TxDevice

* ``SimulatedTxDevice.__init__`` adds ``self._train_curr = 0``
  so a polled host can read progress without subscribing to the
  OWSignal. Written by the engine on each tick.
* ``SimulatedTxDevice.get_trigger_json()`` adds ``"TrainCount":
  self._train_curr`` to its returned dict.
* ``SimulatedTxDevice.get_trigger()`` is new -- mirrors the shape
  of the real ``LIFUTXDevice.TxDevice.get_trigger()`` so OI code
  calling ``interface.txdevice.get_trigger()`` gets the same keys
  against sim and real hardware. Adds two walking-skeleton
  polling fields: ``train_count`` (current running counter) and
  ``trigger_status`` (``"RUNNING"`` / ``"STOPPED"``).

### Sim engine

* ``_SimulatedRunEngine.start()`` resets ``self._tx._train_curr =
  0`` so a polled host sees progress starting from 0 on every
  fresh sonication (previously the counter carried over from a
  prior run if the host polled before the first tick).
* ``_SimulatedRunEngine._on_train_tick`` mirrors
  ``self._train_curr`` back to ``self._tx._train_curr`` after
  each increment -- the field the polled host reads.
* ``_SimulatedRunEngine._on_train_tick`` natural-completion
  branch now calls ``self._tx.stop_trigger()`` (previously only
  an explicit ``stop_sonication`` flipped ``_trigger_running``
  to False, so a polled host saw ``TriggerStatus=RUNNING`` after
  the sim finished naturally). The emitted STATUS frame already
  carried ``status=STOPPED`` so callers that subscribed to the
  OWSignal weren't affected by the bug; polled callers were.

### Real TxDevice

* ``LIFUTXDevice.get_trigger()`` extends its return dict with
  the same two fields:
  * ``"train_count": trigger_json.get("TrainCount", 0)``
  * ``"trigger_status": trigger_json.get("TriggerStatus",
    "UNKNOWN")``
  Both use ``.get(default)`` so pre-PROJ-04 firmware (which
  doesn't yet populate ``TrainCount``) returns 0 cleanly. The
  firmware-side ``OW_CTRL_GET_SWTRIG`` response-payload
  extension to actually populate the field is tracked
  separately as a PROJ-04 companion on
  openlifu-transmitter-fw -- not blocking the OI walking
  skeleton since OI runs against the sim pre-PROJ-04.

## Tests

New: ``unit-test/test_simulated_trigger_progress.py`` -- 7 tests
exercising the sim-side changes end-to-end:

* ``get_trigger_json`` / ``get_trigger`` initial state (both
  ``TrainCount`` / ``train_count`` default to 0, ``TriggerStatus``
  / ``trigger_status`` default to ``STOPPED``).
* ``get_trigger`` snake-case shape check (every expected key
  present).
* ``train_count`` advances monotonically during a short synthetic
  sequence (5 pulse trains * 20 ms), reaches ``pulse_train_count``
  on natural completion.
* Natural completion flips ``trigger_status`` to ``STOPPED`` --
  regression test for the pre-2026-10-01 bug where
  ``_trigger_running`` wasn't flipped on natural end.
* Operator-initiated ``stop_sonication`` midway: ``trigger_status``
  flips to ``STOPPED``, ``train_count`` retained at partial
  progress (not reset to 0, not advanced to total).
* Restart resets ``train_count`` to 0 at engine ``start()``.

Full suite (excluding pre-existing ``test_tx_device.py`` collection
failure on an unrelated ``HW_ID_DATA_LENGTH`` import): 107 passed.

Refs OpenwaterHealth/openlifu-operator-interface#78

Signed-off-by: Peter Hollender <peterhollender@gmail.com>
Co-authored-by: Peter Hollender <peterhollender@gmail.com>
Signed-off-by: Peter (Agent) <phollender@openwater.cc>
peterhollender added a commit that referenced this pull request Oct 12, 2026
…erge fda_version (#45)

* feat(preset): add simulated preset-integrity handshake endpoints (#38)

* feat(preset): add simulated preset-integrity handshake endpoints

Adds the SDK side of the SR-002 / SR-003 host<->firmware preset-
integrity handshake described in the operator-interface repo's
``preset_pseudo_code.md``.

**Simulated side (implemented):**

* ``SimulatedTxDevice`` now takes an optional ``preset_flash:
  list[tuple[str, int]]`` in its constructor and exposes
  ``get_preset(preset_index) -> (id, settings_crc)``,
  ``set_preset(preset_index, sequence_duration_index, expected_crc)``,
  ``set_preset_flash(flash)``, and ``get_loaded_preset() ->
  (preset_index | None, duration_index | None)`` for test
  round-tripping.

  ``set_preset`` verifies the caller's ``expected_crc`` against the
  flash-baked ``settings_crc``, matching what real firmware will do
  once PROJ-04 lands. Mismatch raises ``ValueError`` -- this is the
  CRC-mismatch fault path the operator UI must annunciate per
  SR-003.

* ``SimulatedLIFUInterface.__init__`` gains a ``preset_flash``
  keyword that is forwarded to the underlying
  ``SimulatedTxDevice``.

**Real side (stubbed):**

* ``TxDevice.get_preset`` / ``TxDevice.set_preset`` are added as
  documented ``NotImplementedError`` stubs pointing at PROJ-04 for
  firmware work. Their docstrings direct testers to
  ``SimulatedLIFUInterface`` in the meantime.

**Tests (new, ``unit-test/test_simulated_preset_flash.py``):**

* Happy-path: ``get_preset`` returns flash entries; ``set_preset``
  records selection on matching CRC; ``get_loaded_preset``
  round-trips.
* Rejection: ``set_preset`` on CRC mismatch / out-of-range index /
  negative duration index; ``get_preset`` out-of-range.
* Flash reseed clears loaded selection.
* ``SimulatedLIFUInterface.__init__`` forwards ``preset_flash`` to
  its txdevice.
* ``TxDevice.get_preset`` / ``set_preset`` raise
  ``NotImplementedError`` mentioning PROJ-04.

All 14 new tests pass. Full ``unit-test/`` run (excluding the
pre-existing ``test_tx_device.py`` collection failure that is
unrelated to this change): 92 passed.

No firmware changes in this PR. The corresponding operator-
interface work that consumes this API lands in
OpenwaterHealth/openlifu-operator-interface as a follow-up (SR-002
/ SR-003 UI wiring; part of PROJ-03 close-out).

Signed-off-by: Peter Hollender <peterhollender@gmail.com>

* feat(sim): add version mutators for firmware-compat simulation

SimulatedTxDevice.set_version(version, module=0) and SimulatedHVController.set_version(version) let a debug UI or test flip the reported firmware version so the operator connector's check_firmware_compat can be driven down the incompatible-firmware path without shipping a separate `firmware_compatible` toggle. Single string per device today; can extend to per-module later.

3 new tests, 17 total in test_simulated_preset_flash.py.

Signed-off-by: Peter Hollender <peterhollender@gmail.com>

* refactor(sim): preset_flash holds machine_config dicts; implement update_firmware

Two related SDK-side changes that support the SR-003 preset-integrity
handshake per the operator-interface repo's ``preset_pseudo_code.md``:

**1. preset_flash schema change (breaking)**

The simulated flash-baked preset table now holds full
``machine_config`` dicts, not just ``(id, settings_crc)`` tuples.
Each entry is ``(machine_config_dict, machine_config_crc)`` where:

* ``machine_config_dict`` mirrors what the real firmware image
  would carry after PROJ-04 codegen -- specifically, includes an
  ``id`` and a ``settings_crc`` field the ``get_preset`` method
  reads.
* ``machine_config_crc`` is what the host sends to ``set_preset``
  at runtime -- the CRC of the full machine_config, not the CRC
  of the underlying preset dict.

New method: ``SimulatedTxDevice.get_machine_config(preset_index)``
returns ``(machine_config_dict, machine_config_crc)`` for
debugging + tests to inspect the flash-side state.

``set_preset``'s CRC-mismatch error message now names
``machine_config_crc`` explicitly so callers understand which
CRC was checked (the machine_config's, not the preset's).

**2. Simulator's update_firmware is implemented**

``SimulatedTxDevice.update_firmware`` and
``SimulatedHVController.update_firmware`` now flip the reported
firmware version to a ``target_version`` kwarg (default:
``"sim-1.0.7"``). Real DFU is out of scope for the simulator; the
purpose here is to let an app-side firmware-update flow (e.g. the
operator-interface's "Firmware update required" modal ->
``connector.update_firmware()`` -> re-probe) run end-to-end in
sim mode.

The signatures mirror the shape of the real
``TxDevice.update_firmware`` / HV equivalent so app-side code can
call the same method against either backend.

Tests: 22 total (was 17). New coverage:

* ``machine_config_dict`` -> ``(id, settings_crc)`` extraction
* ``get_machine_config`` round-trip
* ``set_preset`` CRC-mismatch error message names
  ``machine_config_crc``
* ``update_firmware`` on both TX + HV flips ``get_version`` output

No firmware changes in this PR.

Signed-off-by: Peter Hollender <peterhollender@gmail.com>

---------

Signed-off-by: Peter Hollender <peterhollender@gmail.com>
Co-authored-by: Peter Hollender <peterhollender@gmail.com>
Signed-off-by: Peter (Agent) <phollender@openwater.cc>

* feat(trigger): add TrainCount + trigger_status for polled progress (#39)

Lets the operator interface poll sonication progress against
``TxDevice.get_trigger()`` instead of subscribing to the async
``signal_data_received`` STATUS-frame OWSignal. Companion to
openlifu-operator-interface#78 (PROJ-08 Task 1: Running-state
ProgressBar + firmware-driven Running->Finished transition).

The walking-skeleton OI deliberately dropped async telemetry /
status-frame subscriptions in openlifu-operator-interface#92;
reintroducing one for sonication progress would undo that work.
A polled model fits the sync-probe idiom the OI state machine
already uses for connect state and firmware compat.

## Changes

### Simulated TxDevice

* ``SimulatedTxDevice.__init__`` adds ``self._train_curr = 0``
  so a polled host can read progress without subscribing to the
  OWSignal. Written by the engine on each tick.
* ``SimulatedTxDevice.get_trigger_json()`` adds ``"TrainCount":
  self._train_curr`` to its returned dict.
* ``SimulatedTxDevice.get_trigger()`` is new -- mirrors the shape
  of the real ``LIFUTXDevice.TxDevice.get_trigger()`` so OI code
  calling ``interface.txdevice.get_trigger()`` gets the same keys
  against sim and real hardware. Adds two walking-skeleton
  polling fields: ``train_count`` (current running counter) and
  ``trigger_status`` (``"RUNNING"`` / ``"STOPPED"``).

### Sim engine

* ``_SimulatedRunEngine.start()`` resets ``self._tx._train_curr =
  0`` so a polled host sees progress starting from 0 on every
  fresh sonication (previously the counter carried over from a
  prior run if the host polled before the first tick).
* ``_SimulatedRunEngine._on_train_tick`` mirrors
  ``self._train_curr`` back to ``self._tx._train_curr`` after
  each increment -- the field the polled host reads.
* ``_SimulatedRunEngine._on_train_tick`` natural-completion
  branch now calls ``self._tx.stop_trigger()`` (previously only
  an explicit ``stop_sonication`` flipped ``_trigger_running``
  to False, so a polled host saw ``TriggerStatus=RUNNING`` after
  the sim finished naturally). The emitted STATUS frame already
  carried ``status=STOPPED`` so callers that subscribed to the
  OWSignal weren't affected by the bug; polled callers were.

### Real TxDevice

* ``LIFUTXDevice.get_trigger()`` extends its return dict with
  the same two fields:
  * ``"train_count": trigger_json.get("TrainCount", 0)``
  * ``"trigger_status": trigger_json.get("TriggerStatus",
    "UNKNOWN")``
  Both use ``.get(default)`` so pre-PROJ-04 firmware (which
  doesn't yet populate ``TrainCount``) returns 0 cleanly. The
  firmware-side ``OW_CTRL_GET_SWTRIG`` response-payload
  extension to actually populate the field is tracked
  separately as a PROJ-04 companion on
  openlifu-transmitter-fw -- not blocking the OI walking
  skeleton since OI runs against the sim pre-PROJ-04.

## Tests

New: ``unit-test/test_simulated_trigger_progress.py`` -- 7 tests
exercising the sim-side changes end-to-end:

* ``get_trigger_json`` / ``get_trigger`` initial state (both
  ``TrainCount`` / ``train_count`` default to 0, ``TriggerStatus``
  / ``trigger_status`` default to ``STOPPED``).
* ``get_trigger`` snake-case shape check (every expected key
  present).
* ``train_count`` advances monotonically during a short synthetic
  sequence (5 pulse trains * 20 ms), reaches ``pulse_train_count``
  on natural completion.
* Natural completion flips ``trigger_status`` to ``STOPPED`` --
  regression test for the pre-2026-10-01 bug where
  ``_trigger_running`` wasn't flipped on natural end.
* Operator-initiated ``stop_sonication`` midway: ``trigger_status``
  flips to ``STOPPED``, ``train_count`` retained at partial
  progress (not reset to 0, not advanced to total).
* Restart resets ``train_count`` to 0 at engine ``start()``.

Full suite (excluding pre-existing ``test_tx_device.py`` collection
failure on an unrelated ``HW_ID_DATA_LENGTH`` import): 107 passed.

Refs OpenwaterHealth/openlifu-operator-interface#78

Signed-off-by: Peter Hollender <peterhollender@gmail.com>
Co-authored-by: Peter Hollender <peterhollender@gmail.com>
Signed-off-by: Peter (Agent) <phollender@openwater.cc>

* feat(preset): apply trigger config in SimulatedTxDevice.set_preset (#40)

Companion to the OI #78 smoke-test regression. The old
``SimulatedTxDevice.set_preset`` verified the CRC and recorded
the loaded selection but did not update ``_sequence`` /
``_pulse`` with the loaded preset's parameters, so an immediate
``SimulatedLIFUInterface.start_sonication`` ran the default
1-train sequence instead of the operator's selected duration.
That path reaches the UI as "Progress bar never advances; sim
reports STOPPED after one tick."

Real firmware does not have this problem: ``set_preset`` on the
device loads the machine_config and applies its registers /
profiles, and no separate ``set_trigger`` call is needed before
``start_trigger``. The sim now mirrors that model.

## Change

``SimulatedTxDevice.set_preset`` now reads the following fields
from the flash-baked machine_config and applies them to the TX
device state before returning:

* ``pulse_interval_ms`` (ms -> seconds -> ``_sequence["pulse_interval"]``)
* ``pulse_count`` -> ``_sequence["pulse_count"]``
* ``pulse_train_interval_s`` -> ``_sequence["pulse_train_interval"]``
* ``pulse_train_count_selections[sequence_duration_index]`` ->
  ``_sequence["pulse_train_count"]``
* ``pulse_length_us`` (us -> seconds -> ``_pulse["duration"]``)

Also adds a bounds check that raises ``ValueError`` when
``sequence_duration_index`` is beyond the preset's own
``pulse_train_count_selections`` length (previously only the
negative-index case raised; an out-of-range positive index
silently stored a nonsense selection).

The CRC-mismatch error path is unchanged -- no partial state
leaks into ``_sequence`` / ``_pulse`` on a failed CRC check.

## Tests

New: ``unit-test/test_simulated_set_preset_applies_trigger.py``
(7 tests):

* ``set_preset`` writes pulse_interval / pulse_count /
  pulse_train_interval / pulse_train_count into ``_sequence``.
* ``set_preset`` writes pulse_length_us (converted to seconds)
  into ``_pulse["duration"]``.
* The applied values are visible through ``get_trigger()`` --
  wrapper round-trip test.
* Out-of-range ``sequence_duration_index`` raises ``ValueError``
  mentioning "out of range".
* Negative ``sequence_duration_index`` still raises (regression).
* CRC mismatch leaves ``_sequence`` and ``_pulse`` unchanged
  (no partial state on fault path).
* **Integration**: load preset -> start_sonication -> engine ticks
  run the selected ``pulse_train_count``, reach ``trigger_status
  == STOPPED`` at ``train_count == pulse_train_count``.

Updated ``_mc()`` helper in
``unit-test/test_simulated_preset_flash.py`` to include the
required trigger-config fields plus a default
``pulse_train_count_selections=[1, 2, 3, 4, 5]`` so pre-existing
tests that call ``set_preset(duration_index=0..4)`` don't need
per-site overrides.

Full suite (excluding pre-existing ``test_tx_device.py``
collection failure on unrelated ``HW_ID_DATA_LENGTH`` import):
114 passed (was 107 pre-change).

Refs OpenwaterHealth/openlifu-operator-interface#78

Signed-off-by: Peter Hollender <peterhollender@gmail.com>
Co-authored-by: Peter Hollender <peterhollender@gmail.com>
Signed-off-by: Peter (Agent) <phollender@openwater.cc>

* refactor(io): split DeviceInterface (FDA) from LIFUInterface (RUO)

DeviceInterface is now the FDA-facing base class: connection,
monitoring, PID lock, load_preset (TX load + HV select with
frequency-specific sensitivity), start/stop sonication, status, close.
LIFUInterface extends it with the research-only endpoints
(set_solution, check_solution, set_module_invert, voltage tables,
sequence duty/duration) and the RUO constructor options
(ext_power_supply, module_invert, voltage_table_selection) via a
_create_devices hook. Existing LIFUInterface callers are unchanged.

Sensitivity lookup is a single sensitivity_at_frequency() in
LIFUUserConfig (np.interp per module, clamped, mean across modules);
TxDevice and SimulatedTxDevice both delegate to it.

HVController.select_preset sends <I (CRC only) or <Iff (CRC,
sensitivity, frequency_hz); a half-specified call raises ValueError.
Removed the unused HVController.load_preset alias.

Simulator mirrors the split: SimulatedDeviceInterface exposes no RUO
methods; SimulatedLIFUInterface adds them. Simulated preset paths raise
LIFUDeviceError (OW_BAD_CRC / OW_HV_PRESET_CRC) and LIFUSolutionError
like the hardware path.

Refs SR-002
Refs OpenwaterHealth/openlifu-diathermy-application#16

Signed-off-by: Peter Hollender <peterhollender@gmail.com>

* refactor(io): split FDA TxDevice/HVController from RUO subclasses (#47)

* refactor(io): split FDA TxDevice/HVController from RUO subclasses

DeviceInterface (the FDA entry point) handed out TxDevice and
HVController objects that still carried every RUO command (register
writes, set_voltage, set_trigger, DFU, config writes, async mode).
Restricting the interface alone left them one attribute away.

- TxDevice / HVController are now the FDA components. They expose only
  identity, telemetry, trigger read/start/stop and stored-preset
  methods, and carry a SUPPORTED_COMMANDS whitelist
  (TX_FDA_COMMANDS / HV_FDA_COMMANDS) so any other opcode raises
  ValueError before it reaches the UART.
- LIFUTxDevice / LIFUHVController subclass them and add the RUO
  methods; shared RUO globals (start/stop, send_async, echo, LED,
  reset, DFU, write_config) move to LIFUComponentMixin.
- Only LIFUInterface creates the LIFU classes (_create_devices hook).
- Async mode is RUO-only: DeviceInterface drops run_async,
  start_monitoring/stop_monitoring and the async_mode calls in
  start/stop_sonication; LIFUInterface keeps them.
- The simulator mirrors the split (SimulatedLIFUTxDevice,
  SimulatedLIFUHVController; monitoring and async_mode on
  SimulatedLIFUInterface).
- RUO unit tests now use the LIFU classes; new tests cover the class
  surfaces, the opcode whitelist and which classes each interface
  builds. test_tx_device.py imports HW_ID_DATA_LENGTH from LIFUConfig
  so it collects again (three stale set_trigger tests remain, #44).

Refs SR-002
Closes #43
Refs #44

Signed-off-by: Peter Hollender <peterhollender@gmail.com>

* test(tx): update set_trigger train-interval tests to back-to-back contract

test_23b/23c/23d asserted the 2.0.14 behaviour (auto-filling a zero
train interval and bumping it past the pulse period for firmware
<= 2.0.3). Since 8b3eaf2 set_trigger sends 0 for back-to-back trains,
and firmware <= 2.0.3 no longer needs support.

Replace them with tests of the current contract: zero or zero-gap
intervals are sent as 0; longer intervals pass through; a short
interval is sent as 0 for a single train and raises ValueError (with
nothing sent) for multiple trains.

Refs SR-002
Closes #44

Signed-off-by: Peter Hollender <peterhollender@gmail.com>

---------

Signed-off-by: Peter Hollender <peterhollender@gmail.com>
Co-authored-by: Peter Hollender <peterhollender@gmail.com>
Signed-off-by: Peter (Agent) <phollender@openwater.cc>

---------

Signed-off-by: Peter Hollender <peterhollender@gmail.com>
Signed-off-by: Peter (Agent) <phollender@openwater.cc>
Co-authored-by: Peter Hollender <peterhollender@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants