Skip to content

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

Merged
peterhollender merged 2 commits into
feature/SR-002-device-interfacefrom
techdebt/SR-002-fda-component-split
Oct 9, 2026
Merged

peterhollender merged 2 commits into
feature/SR-002-device-interfacefrom
techdebt/SR-002-fda-component-split

Conversation

@openlifu-agent

@openlifu-agent openlifu-agent commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Closes #43
Closes #44
Refs OpenwaterHealth/openlifu-diathermy-application#16 (SR-002)

Stacked on #45 (base feature/SR-002-device-interface). Merge #45 first.

What

DeviceInterface (the FDA entry point) handed out TxDevice and
HVController objects that still had every RUO command on them.
Restricting the interface alone left those commands one attribute away.
This PR moves the split down to the component level.

FDA (DeviceInterface) RUO (LIFUInterface)
TxDevice: identity, temperatures, get_trigger/start_trigger/stop_trigger, get_preset/load_preset, metadata LIFUTxDevice(LIFUComponentMixin, TxDevice): set_trigger*, profiles/set_solution, registers, async_mode, module invert, firmware update
HVController: identity, HV on/off/status, get_preset/select_preset, telemetry reads LIFUHVController(LIFUComponentMixin, HVController): set_voltage, DACs, 12 V, fan, RGB, hv_enable
OWComponent: connect/close, send_checked, ping/version/HWID/read_config LIFUComponentMixin: uart, start/stop, send_async, echo, LED, reset, DFU, write_config*
  • Opcode whitelist: each FDA class has SUPPORTED_COMMANDS
    (TX_FDA_COMMANDS / HV_FDA_COMMANDS in LIFUConfig).
    send/send_checked raise ValueError for any other opcode
    before the UART is touched, so a raw send_checked(OW_TX7332_WREG)
    on an FDA object is refused.
  • Async is RUO-only: DeviceInterface no longer has run_async,
    start_monitoring/stop_monitoring, or async_mode calls in
    start_sonication/stop_sonication. LIFUInterface keeps all
    of these.
  • Hooks: _create_devices (LIFUInterface builds the LIFU
    classes) and _connect_devices (LIFUInterface skips the sync
    connect in async mode).
  • Simulator: mirrors the split (SimulatedLIFUTxDevice,
    SimulatedLIFUHVController; monitoring and async_mode are on
    SimulatedLIFUInterface).

Behaviour changes to note

  • LIFUInterface.start_sonication now sets async_mode before
    turning HV on (previously after). Both happen before the trigger
    starts, so the sonication sequence is unchanged.
  • The FDA DeviceInterface has no monitor thread, so there is no
    USB hot-plug auto-reconnect
    on the FDA path. Ports are opened once
    in __init__.
  • TxDevice.__init__ no longer takes module_invert (it is now on
    LIFUTxDevice).

Residual gaps (not fixed here)

  • OW_CMD_USR_CFG covers both read and write (reserved=1). The
    whitelist checks only the opcode, so it cannot block a hand-built
    config write. write_config itself is no longer on the FDA classes.
  • _uart is private by convention only.
  • The whitelist ignores packet type. This is safe today because command
    values are unique across packet types.
  • The operator interface's firmware-update flow has no real-hardware
    path on DeviceInterface. The sim keeps an update_firmware
    placeholder. Tracked in Add a service interface for firmware update / config writes outside the FDA DeviceInterface #46 (service interface).

Test plan (executed)

  • SDK: pytest unit-test gives 223 passed, 113 subtests, with no deselections. The baseline before this change was 147 passed with test_tx_device.py ignored entirely.
  • New tests in test_fda_presets.py:
    • TestComponentSurface: the FDA classes lack every RUO member, the
      LIFU classes have them, and the simulator classes match.
    • TestCommandWhitelist: FDA TX rejects OW_TX7332_WREG and
      OW_CMD_ECHO, FDA HV rejects OW_POWER_SET_HV, and none of
      these calls reaches send_packet. The LIFU classes allow them.
    • TestInterfaceSurface: DeviceInterface._create_devices builds
      exactly TxDevice/HVController, and
      LIFUInterface._create_devices builds the LIFU classes. Monitoring
      is now in RUO_ENDPOINTS.
  • Operator interface (feature/SR-002-device-interface + this SDK):
    all 12 test files pass. test_sonication_progress.py needed a
    test-only change because it used set_trigger on the FDA sim; that
    change is on OI PR #124 (commit c4762c3).

#44

test_tx_device.py exercises the class being split, so this PR fixes
its HW_ID_DATA_LENGTH import (now from LIFUConfig) and points it
at LIFUTxDevice. Once the file collected, three tests
(test_23b/23c/23d) failed. They asserted the 2.0.14
train-interval auto-fill and the bump for firmware 2.0.3 and older.
set_trigger has sent 0 for back-to-back trains since 8b3eaf2,
and firmware 2.0.3 and older no longer needs support. A separate commit
replaces those three tests with tests of the current contract (23b-23f):

  • Zero or zero-gap intervals are sent as 0.
  • Longer intervals pass through unchanged.
  • A short interval is sent as 0 for a single train, and raises
    ValueError with nothing sent for multiple trains.

Self-review

  • Assumption I'm making: the FDA app needs only the opcodes in
    TX_FDA_COMMANDS/HV_FDA_COMMANDS. That set is derived from
    every method the operator interface calls on txdevice/hvcontroller
    (verified by its test suite).
  • Alternative considered: putting the RUO classes in separate
    modules. I kept them next to their FDA bases to keep the diff
    reviewable and preserve import paths. The FDA classes still do not
    define any RUO method.
  • New dependency introduced: none
  • Test coverage: SR-002 component surface, opcode whitelist, and
    interface factories. Not covered: real-hardware async/monitor path
    (unchanged code, moved), and the FDA no-hot-plug behaviour.
  • Breaking change: code that called RUO methods on a
    DeviceInterface's components, or passed run_async /
    async_mode to DeviceInterface, must use LIFUInterface. The
    only in-tree caller of that kind was an OI test (fixed).
    LIFUInterface users are unaffected.
  • Tech-debt filed: Add a service interface for firmware update / config writes outside the FDA DeviceInterface #46 (service interface). Closes test_tx_device.py fails to import HW_ID_DATA_LENGTH #44.

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>
…tract

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>
@peterhollender
peterhollender merged commit 7051a09 into feature/SR-002-device-interface Oct 9, 2026
2 checks passed
@peterhollender
peterhollender deleted the techdebt/SR-002-fda-component-split branch October 9, 2026 18:42
peterhollender added a commit that referenced this pull request Oct 9, 2026
* 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>
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