Repository navigation
chore(git): record feature/fda_version as merged (no content change) - #48
Merged
peterhollender merged 4 commits intoOct 12, 2026
Conversation
* 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>
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>
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>
#45 was squash-merged into feature/fda-preset-generation, so the #38-#40 commits it carried are not ancestors of this branch, and a merge into feature/fda_version produced 17 spurious conflict hunks even though the content is already here. feature/fda_version was first reset to the DCO-signed copies of #38-#40 (identical trees, same parent). This merge uses the "ours" strategy to add that tip as a parent without changing any file, so the follow-up PR into feature/fda_version is conflict-free. Refs SR-002 Refs #45 Signed-off-by: Peter (Agent) <phollender@openwater.cc>
peterhollender
force-pushed
the
techdebt/SR-002-reconcile-fda-version
branch
from
October 12, 2026 00:55
709a72d to
c35e58a
Compare
2 tasks
peterhollender
merged commit Oct 12, 2026
843c9a2
into
feature/fda-preset-generation
2 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs SR-002 (OpenwaterHealth/openlifu-diathermy-application#16)
Refs #45
Merge-only, no file changes.
git diff feature/fda-preset-generation HEADis empty.Why
#45 was squash-merged into
feature/fda-preset-generation, so the #38-#40 commits it carried are not ancestors of that branch. A trial merge intofeature/fda_versionproduced 17 spurious conflict hunks inLIFUTXDevice.pyandsimulated_interface.py, even though the content is already present.What was done
feature/fda_versionwas force-pushed from093185ftoc7a9ded, with owner approval. That swaps the original feat(preset): add simulated preset-integrity handshake endpoints #38-feat(preset): apply trigger config in SimulatedTxDevice.set_preset #40 commits for their DCO-signed copies from refactor(io): split DeviceInterface (FDA) from LIFUInterface (RUO); merge fda_version #45:c14622c(feat(preset): add simulated preset-integrity handshake endpoints #38)a35162d81aaf08(same)6f27280(feat(trigger): add TrainCount + trigger_status for polled progress #39)0201523093185f(feat(preset): apply trigger config in SimulatedTxDevice.set_preset #40)c7a9dedThis PR adds one
-s oursmerge commit (c35e58a). It makes the newfeature/fda_versiontip a parent without touching any file. After it merges, feat(io): integrate FDA preset generation and DeviceInterface split #49 is conflict-free, and none of the agent-authored commits it carries fails DCO.Merge with a merge commit, not squash. A squash drops the second parent and undoes the fix.
Test plan (executed)
git diff origin/feature/fda-preset-generation HEAD: empty.git merge-base --is-ancestor origin/feature/fda_version HEAD: true.Signed-off-bythat matches its author.pytest unit-test: 223 passed, 113 subtests.Self-review
feature/fda_version. The owner confirmed this, and only this branch contained the old commits.main..github/dco.ymlpolicy change.feature/fda_versionshould rungit fetch; git reset --hard origin/feature/fda_version. The file contents are identical.