Skip to content
Merged

Next #29

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
11e59ba
Add 2.0.3 triggering compatibility
peterhollender Jun 17, 2026
d1316c3
update dfu script
peterhollender Jun 17, 2026
06368ea
remove print
peterhollender Jun 17, 2026
94d3897
Add 2.0.3 triggering compatibility
peterhollender Jun 17, 2026
a77ae4e
udpate getversion
georgevigelette Jul 12, 2026
bfcfea8
check module mode dfu or app
georgevigelette Jul 12, 2026
c30e332
stop assuming i2c address
georgevigelette Jul 14, 2026
1d179ac
Merge branch 'next' of github.com:OpenwaterHealth/openlifu-sdk into next
georgevigelette Jul 14, 2026
6683065
sbom added and pinned repos
georgevigelette Jul 16, 2026
4dfba6f
static analysis
georgevigelette Jul 16, 2026
911ff12
fix version
georgevigelette Jul 16, 2026
0640d79
added rgb effects
georgevigelette Jul 20, 2026
1ba01f9
signing capabilities added to the sdk
georgevigelette Jul 20, 2026
0629da7
added bootloader version printout
georgevigelette Jul 20, 2026
41c2581
update migration from previous console fw versions
georgevigelette Jul 20, 2026
d65b0b4
update
georgevigelette Jul 20, 2026
b1b1613
update documentation
georgevigelette Jul 21, 2026
eafac63
all console versions can be updated with the fwupdater module
georgevigelette Jul 21, 2026
37ebec7
update to support transmitter firmware update
georgevigelette Jul 21, 2026
406f4e3
added bl version and fix static analysis
georgevigelette Jul 21, 2026
6677059
udpate for static analysis
georgevigelette Jul 21, 2026
cfe5d41
Pattern and delay profile selection seems to be working, need to remo…
May 13, 2026
72c4dab
Add logic for multiple profiles
May 14, 2026
4fa5c0e
Modify QA script and profile parameters to pass over to firmware
May 15, 2026
de8ef8e
QA script for multiple profiles seems to be working, switched to havi…
Jun 12, 2026
63b1a60
Move set_trigger to outside of loop
Jun 12, 2026
441c342
write_block moved to before the loop
Jun 15, 2026
a4d6ff7
Remove hard code for just tx chip 0, add tx chip 1 so all elements pr…
Jun 18, 2026
8d5503a
Checkpoint - apodization switching working
Jun 19, 2026
2fb14bf
Loop sequence 10 times
Jun 22, 2026
0ee61a3
Loop through both chips on module
Jun 23, 2026
369ff69
updated api working for get/set delay and profile registers
Jun 25, 2026
75608a6
Checkpoint - need to clean up, setting profiles working again
Jun 26, 2026
fdc3372
get delay profile working again
Jun 26, 2026
0e3ddd8
Delay profile set and get working correctly through OW commands sent …
Jun 29, 2026
e32aec6
Re-add apodizations alongside delays
Jun 30, 2026
6337714
Modularize test script a bit more by separating read delay registers …
Jul 2, 2026
227f9ef
Separate pattern and delay profiles so that you can have one pattern …
Jul 7, 2026
9a7dee7
Re-organize QA script
Jul 8, 2026
67c0fca
Revert apodization register mapping compute to sdk rather than firmwa…
Jul 8, 2026
88aa59c
add pulse level profile switching interlock
Jul 14, 2026
15db90e
Bump safety margin to 1ms
Jul 14, 2026
177f3cd
Add test case for specific execution order
Jul 16, 2026
1046d51
cleanup squash
Jul 17, 2026
b70a95d
update tx old and new versions that are on master
georgevigelette Jul 22, 2026
3629261
Remove unused imports
Jul 22, 2026
85dcadf
added stm32dfu support and hardening of the upgrade over dfu
georgevigelette Jul 22, 2026
c373e68
added force production update
georgevigelette Jul 22, 2026
c62d2b8
update tx over i2c
georgevigelette Jul 22, 2026
400839f
Add execution length in set_solution() of LIFUInterface (was only in …
Jul 22, 2026
8f1f5ae
Remove duplicate global variable
Jul 22, 2026
ec3e116
legacy bootloader update
georgevigelette Jul 23, 2026
e3c626d
update naming of console legacy updater
georgevigelette Jul 23, 2026
553c7c3
working slave legacy udpates for tx
georgevigelette Jul 23, 2026
1771334
Readjust pulse train interval to add min profile switch interval
Jul 24, 2026
315cc80
update pre-release fw bins
georgevigelette Jul 24, 2026
40bfa57
Remove unused imports
Jul 24, 2026
d76a433
Merge pull request #27 from OpenwaterHealth/feature/rastered_focusing
georgevigelette Jul 24, 2026
77cdd1e
pin ruff in ci/cd
georgevigelette Jul 24, 2026
3937663
updated static scan errors
georgevigelette Jul 24, 2026
8b3eaf2
Fix initial pulse truncation and pulse train interval timing setting
alkagan Aug 5, 2026
3f9b488
Comment cleanup
alkagan Aug 7, 2026
645408c
update from bootloader
georgevigelette Aug 11, 2026
21b3849
fix static analysis
georgevigelette Aug 11, 2026
a5ada44
console updates validated
georgevigelette Aug 11, 2026
c0f919a
cleanup
georgevigelette Aug 11, 2026
dafdc50
Add multiple module rastering in test sequence and setting pattern pr…
alkagan Aug 13, 2026
2396bfb
Add mirroring for N modules and skip readback command line args in te…
alkagan Aug 19, 2026
fa210ab
Remove unnecessary functions
alkagan Aug 20, 2026
18fc75c
Merge pull request #28 from OpenwaterHealth/bugfix/trigger_timing
georgevigelette Aug 25, 2026
edc6f3d
Remove incorrect continuous trigger mode ValueError
alkagan Aug 27, 2026
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
57 changes: 57 additions & 0 deletions .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,63 @@ jobs:
- name: Install build tooling
run: python -m pip install --upgrade build setuptools-scm

# ── Bundled firmware from the firmware repos' releases ───────
# rc tags and next builds take each repo's latest pre-release
# (when it is at least as new as the latest stable); everything
# else -- and rc/next builds with no usable pre-release -- takes
# the latest stable release. The dfu-stub and legacy-updater
# images have no publishing CI and stay as committed.
- name: Resolve firmware releases
id: fw
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
resolve() {
local repo="$1" REL PRE TAG KIND
REL=$(gh release list --repo "$repo" --json tagName,isPrerelease \
--jq 'map(select(.isPrerelease | not)) | .[0].tagName // empty')
PRE=$(gh release list --repo "$repo" --json tagName,isPrerelease \
--jq 'map(select(.isPrerelease)) | .[0].tagName // empty')
TAG="$REL"
KIND="release"
if [[ "${GITHUB_REF_NAME}" == "next" || "${GITHUB_REF_NAME}" =~ -rc\.[0-9]+$ ]]; then
# semver compare: '-rc.' -> '~rc.' makes sort -V rank a pre-release
# below its own stable release but above older ones.
if [ -n "$PRE" ] && { [ -z "$REL" ] || \
[ "$(printf '%s\n' "${PRE/-rc./~rc.}" "$REL" | sort -V | tail -n1)" = "${PRE/-rc./~rc.}" ]; }; then
TAG="$PRE"
KIND="pre-release"
fi
fi
if [ -z "$TAG" ]; then
echo "::error::No release found in ${repo}." >&2
return 1
fi
echo "${repo}: $KIND @ $TAG (stable='${REL:-none}' pre-release='${PRE:-none}')" >&2
printf '%s' "$TAG"
}
CONSOLE_TAG=$(resolve OpenwaterHealth/openlifu-console-fw)
TRANSMITTER_TAG=$(resolve OpenwaterHealth/openlifu-transmitter-fw)
echo "console=$CONSOLE_TAG" >> "$GITHUB_OUTPUT"
echo "transmitter=$TRANSMITTER_TAG" >> "$GITHUB_OUTPUT"

- name: Pull firmware binaries
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
FW_DIR=src/openlifu_sdk/firmware
gh release download "${{ steps.fw.outputs.console }}" \
--repo OpenwaterHealth/openlifu-console-fw --dir "$FW_DIR" --clobber \
--pattern 'openlifu-console-fw-signed.bin' \
--pattern 'openlifu-console-fw-production.bin'
gh release download "${{ steps.fw.outputs.transmitter }}" \
--repo OpenwaterHealth/openlifu-transmitter-fw --dir "$FW_DIR" --clobber \
--pattern 'openlifu-transmitter-fw-signed.bin' \
--pattern 'openlifu-transmitter-fw-production.bin'
ls -l "$FW_DIR"

- name: Build sdist & wheel
run: python -m build

Expand Down
85 changes: 85 additions & 0 deletions .github/workflows/safety-security-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: "Medical Software Safety & Security Scan"

on:
push:
branches: [ main, next ]
pull_request:
branches: [ main ]

jobs:
static-analysis:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install Analysis Tools
run: pip install ruff==0.15.0 bandit lizard

# 1. Ruff (correctness / dead code / import hygiene)
# Python counterpart of the firmware repos' cppcheck gate; any finding
# fails the build. Suppressions are inline (# noqa: <rule> -- reason).
- name: Run Ruff (Correctness)
run: ruff check src

# 2. Bandit (security static analysis)
# Gates on MEDIUM and higher severity. LOW findings (e.g. B110
# try/except/pass in device-cleanup paths) are reported in the log but
# do not fail the build.
- name: Run Bandit (Security)
run: bandit -r src --severity-level medium

# 3. Complexity Analysis (Lizard)
# IEC 62304 recommends low cyclomatic complexity for testability.
# Same thresholds as the firmware repos (-C 15 -L 100); functions that
# are structurally complex by design are whitelisted in
# lizard_whitelist.csv.
- name: Check Code Complexity (IEC 62304 Requirement)
run: |
lizard src \
-C 15 -L 100 -w \
--whitelist lizard_whitelist.csv

# 4. Vulnerability Scan against the committed SBOM
# grype consumes the CycloneDX SBOM and checks each component against
# the Grype vulnerability database (NVD, OSV, GHSA, and others).
# Dependency versions are pinned in pyproject.toml and mirrored in the
# SBOM, so PyPI purl matching is exact.
# Fails the build on any HIGH or CRITICAL finding.
- name: Scan SBOM for Vulnerabilities (grype)
id: grype
uses: anchore/scan-action@v6
with:
sbom: "sbom.cdx.json"
fail-build: true
severity-cutoff: high
output-format: sarif

# 5. Upload SARIF results to GitHub Security tab
# Makes vulnerability findings visible in the repository's Security view.
# Code Scanning (Security tab) requires GitHub Advanced Security, which is
# off by default on private repos. Don't fail the scan if it isn't enabled:
# grype already gates the build on HIGH/CRITICAL CVEs, and the SARIF is kept
# as a build artifact below. Remove continue-on-error once GHAS is enabled.
- name: Upload SARIF to GitHub Security
if: always() && steps.grype.conclusion != 'skipped'
continue-on-error: true
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: ${{ steps.grype.outputs.sarif }}

# 6. Upload Audit Artifacts
# Keeps a record of the scan results for regulatory submission.
- name: Archive Regulatory Evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: sdk-compliance-report
path: |
sbom.cdx.json
${{ steps.grype.outputs.sarif }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ htmlcov/

#Downloaded Firmware
src/openlifu_sdk/firmware/downloads
src/openlifu_sdk/_version.py
6 changes: 4 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ Core package (entry points):
I/O modules (in `openlifu_sdk.io`):

- `LIFUUart` — low-level USB/serial transport wrapper and monitor.
- `LIFUHVController` — HV/console interface (power, voltage, telemetry). See [LIFUHVController API](api/LIFUHVController.md).
- `LIFUDFU` — firmware DFU helpers and managers (USB DFU, I2C DFU). See [LIFUDFU API](api/LIFUDFU.md).
- `LIFUHVController` — HV/console interface (power, voltage, telemetry, RGB LED and effects). See [LIFUHVController API](api/LIFUHVController.md).
- `LIFUFirmwareUpdate` — one-call, auto-detecting console firmware update covering all three unit states (no-bootloader / legacy / secure); keyless, uses bundled images. See [LIFUFirmwareUpdate API](api/LIFUFirmwareUpdate.md).
- `LIFUDFU` — lower-level firmware DFU helpers and managers (USB/I2C DFU, per-scenario console update and bootloader migration methods). See [LIFUDFU API](api/LIFUDFU.md).
- `LIFUCrypto` — SBSFU firmware image signing, validation and inspection; owns the FwVersion encoding. See [LIFUCrypto API](api/LIFUCrypto.md).
- `LIFUConfig`, `LIFUUserConfig` — configuration helpers for device registers and user settings.

Utility modules (in `openlifu_sdk.util`):
Expand Down
110 changes: 110 additions & 0 deletions docs/api/LIFUCrypto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# `LIFUCrypto` API

SBSFU firmware image signing, validation and inspection. This module is the
**single source of truth** for the LIFU secure-bootloader image format and the
`FwVersion` encoding — the SDK's update/migration tooling, the standalone CLI,
and the firmware-build CI all sign and verify through it.

Image format (`SECBOOT_ECCDSA_WITH_AES128_CBC_SHA256`, NO_LOADER config):

```
[320 B header] [0xFF pad to 0x400] [firmware body, stored in clear]
```

The 128-byte authenticated header region carries the metadata and the SHA-256
of the firmware body (`FwTag`), signed with ECDSA-P256/SHA-256. At boot the
bootloader verifies the header signature and the body hash; the AES fields
exist only to satisfy the header format and are unused in this configuration.

Installation
- `pip install "openlifu-sdk[crypto]"` — signing/verification needs the
optional `cryptography` dependency. Parsing and hash checks work without it.
- The module also runs **standalone** (guarded imports): executing the file
directly requires only `cryptography`, not the rest of the SDK.

FwVersion encoding (16-bit bitfield)
- `encode_fw_version(version: int | str) -> int` — accepts an already-encoded
int (1–65535) or a semver string; git-describe/pre-release suffixes are
dropped (`"1.2.6-rc.1-3-gabc"` encodes as `1.2.6`).
- `decode_fw_version(value: int) -> str` — back to `"major.minor.patch"`.
- Layout: `major[15:11] . minor[10:5] . patch[4:0]` → ranges **major 0–31,
minor 0–63, patch 0–31** (max `31.63.31` = 0xFFFF; `0.0.0` is invalid).
Strictly monotonic with semver, so the bootloader's anti-rollback integer
compare needs no knowledge of the scheme.
- **Migration note:** this replaces the earlier decimal `MMmmpp` convention
(`1.2.6` was `10206`, is now `2118`). Units with a floor latched under the
old scheme reject new-scheme images until the floor is reset (full-chip
erase / migration).
- `read_fw_version_header(header_path) -> dict` — parse a CMake-generated
`version.h` (`FW_VERSION`, `FW_SHA`, `FW_BUILD_TIME`) so builds can be
signed from their own version metadata.

Keys
- `SigningKeys.from_directory(keys_dir, require_private=False) -> SigningKeys`
— loads from a keys directory: `ecdsa_private.pem` + `aes128.bin` (signing),
`ecdsa_public.pem` (validation; derived from the private key when absent).
Key material is never bundled with the SDK — every operation takes an
explicit keys directory.

Signing
- `sign_firmware_file(firmware, keys_dir, output, version) -> FirmwareHeader`
— sign a raw application `.bin` (linked for slot base + 0x400) and write the
signed image; returns the parsed header.
- `sign_firmware_bytes(firmware: bytes, keys: SigningKeys, version) -> bytes`
— the in-memory core.

Parsing and inspection
- `parse_signed_image(path | bytes) -> FirmwareHeader` — parse the 320-byte
header of a signed image (or a dump of the active slot).
- `FirmwareHeader` — dataclass with `magic`, `protocol_version`, `fw_version`
(+ `fw_version_str`), `fw_size`, `fw_tag`, `signature`, `image_state`
(+ `image_state_str`), `prev_fingerprint`; `describe()` returns a
human-readable summary.

Validation
- `validate_signed_image(image, keys_dir=None) -> ValidationReport` — re-runs
everything the bootloader checks before launch: magic, protocol, sizes,
`FwTag` SHA-256, and (when a public key is available) the ECDSA header
signature. Accepts a full slot dump; bytes past `FwSize` are ignored.
- `ValidationReport` — per-check booleans (`magic_ok`, `fw_tag_ok`,
`signature_ok`, …), `structural_ok`, `ok`, and a three-state `verdict`:
`VALID`, `UNVERIFIED` (structure/hashes pass, no key for the signature
check), or `NOT VALID`. `describe()` prints the check table.

Errors
- `LIFUCryptoError` — signing/validation failures (missing keys, malformed
image). Subclasses the SDK's `LIFUError` when the full SDK is installed.

Command line

```
python -m openlifu_sdk.io.LIFUCrypto sign --keys DIR --firmware app.bin \
--version 1.2.7 --output app_signed.bin
python -m openlifu_sdk.io.LIFUCrypto sign --keys DIR --firmware app.bin \
--version-header build/Release/generated/version.h --output app_signed.bin
python -m openlifu_sdk.io.LIFUCrypto verify app_signed.bin --keys DIR # exit 0/1
python -m openlifu_sdk.io.LIFUCrypto info app_signed.bin [--keys DIR]
```

`--version` takes a semver string or a raw encoded integer;
`--version-header` reads `FW_VERSION` from the firmware build's generated
`version.h` instead. `verify` is CI-friendly (exit code); `info` prints the
header metadata plus the validation table.

Usage example

```py
from openlifu_sdk.io.LIFUCrypto import sign_firmware_file, validate_signed_image

hdr = sign_firmware_file(
firmware="lifu-console-fw.bin", keys_dir="bl-keys/console",
output="lifu-console-fw_signed.bin", version="1.2.7")
print(hdr.fw_version, hdr.fw_version_str) # 2119 1.2.7

report = validate_signed_image("lifu-console-fw_signed.bin",
keys_dir="bl-keys/console")
assert report.ok
```

See also: `docs/api/LIFUDFU.md` — the console update/migration paths consume
images produced here and use `validate_signed_image` as their pre-flash check.
Loading
Loading