Skip to content

Next - #104

Merged
boringethan merged 47 commits into
mainfrom
next
Aug 24, 2026
Merged

Next#104
boringethan merged 47 commits into
mainfrom
next

Conversation

@boringethan

Copy link
Copy Markdown
Contributor

Summary

Licensing layer

  • Apache-2.0 extension (3D Slicer, wellness/veterinary module, partner SDK, language binding)
  • AGPL-3.0 core (firmware, sensing/beamforming/reconstruction/signal-processing/calibration algorithms, hardware reference design)
  • Docs / data / tooling only
  • This PR moves a file between the AGPL core and the Apache layer — if checked, this is NOT an ordinary PR: it requires the license-manifest change process (and board approval for anything leaving the core). Link the approval.

Checklist

  • Commits are signed off (DCO): git commit -s
  • New files carry the correct SPDX-License-Identifier for their layer
  • Tests and docs updated
  • No incompatible third-party code introduced
  • CI is green and CODEOWNERS review requested

Notes for reviewers

boringethan and others added 30 commits August 3, 2026 10:32
Console FW tec_trip_evaluate() opens the safety disconnect and clears
TecStats.tec_status when the TEC sense voltage crosses TEC_TRIP_VALUE,
but the GUI ignored that bit: the Failure dot stayed grey and the Safety
line still read OK, so the laser stopped with no on-screen reason.

Fold the TEC trip into the existing safety-failure path so it drives the
same indicators as an EE/OPT interlock trip, listed as TEC_TRIP in the
fault text. The two sources are tracked separately and each holds its
last known verdict, so a poll where one doesn't answer can't clear a trip
latched by the other, and the composed text can't double-append.

Gate the TEC source on a new _tec_trip_known flag: _tec_good initialises
False, which is indistinguishable from tripped, and tec_status() leaves
it untouched on a failed read — so an unread or dropped poll goes stale
rather than reporting a trip that never happened.

Also correct the stale comment calling tec_good the TMPGD pin;
tec_trip_evaluate() is its only writer in console FW.

The Laser dot's trigger-on/off behaviour and the TEC panel's Temperature
dot are deliberately unchanged.

Refs #89

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…re-indicator

feat: light the Failure indicator on a TEC over-temp trip
New sidebar pane hosting automated procedures. Top bar: procedure
dropdown (laser tuning 4.1-4.5, dev-window variant, and calibration 4.6
as separate entries), Start/Stop, and a status pill (Idle grey /
In process yellow / Pass green / Fail red, reset on procedure change).
Below: a copy-pastable terminal streaming the procedure's output, with
Continue / Left / Right buttons appearing when the procedure asks for
operator input.

Procedures run the SDK's hardware-validated guided runner as a
subprocess: the app releases its console/sensor handles for the child
and reacquires them afterwards, issuing a stop_trigger backstop once the
console reconnects so a killed procedure can never leave the laser
firing. Machinery validated on hardware (release/spawn/prompt/answer/
reacquire/trigger-stop).

Requires an SDK checkout providing omotion.tuning and
scripts/wi15_guided.py (openmotion-sdk PR #215), or the
OPENMOTION_WI15_GUIDED env var pointing at the script.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unchecked (factory mode) the terminal shows only operator-facing lines:
instructions, phase banners, outcomes, pass/fail - the very obvious
what-to-do-next stream for operators. Checked shows the full technician
log. The stored log is always complete; the filter is display-only and
applies retroactively on toggle. Setting persists via QSettings so a
factory rig stays in simple mode across launches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Terminal now word-wraps (no horizontal scrolling). Non-verbose mode
shows the guided runner's @@simple plain-language instruction instead of
the detailed prompt, and hides CLI-only hint lines - the pane's buttons
replace them. Verbose mode unchanged (full detail, @@simple hidden).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…70)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…UI thread (Refs #70)

Clicking Start with connected devices could kill the app with a native
access violation (0xc0000005, libusb teardown). Two causes: the release
ran synchronously on the Qt main thread, and it stopped the
ConnectionMonitor BEFORE submitting the disconnects - request_disconnect
only queues a UserStop event for the monitor's thread, so the teardown
raced a dying monitor. Release now runs on a worker thread, disconnects
each handle while the monitor is alive and waits for its DISCONNECTED
transition (sensors first, console last), then stops the monitor;
QProcess creation returns to the main thread via a queued signal. Stop
during the release window aborts before launch. Reproduced the original
crash conditions (live monitor, all devices CONNECTED) in an offscreen
harness: release now completes cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The laserStateChanged signal was removed from motion_connector.py in
8a3675f (#56), but the empty QML handler stub survived, so every app
start logged a Connections warning that no signal of the target matches
the name. The handler body was a no-op, so nothing changes behaviorally.

Refs #92

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…handler

fix: drop dead onLaserStateChanged handler in Demo.qml
Convert the read-only Device ID and Serial Number value labels on the
Console, Sensor, and Settings pages from Text to TextEdit (readOnly +
selectByMouse) so long hex IDs can be selected and copied instead of
retyped by hand.

Refs #96

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-id-serial

feat: make device ID and serial number fields copyable text
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the wi15_guided-specific pane internals with a data-driven
registry: an entry is name + program + args + working directory, added
by appending to _build_procedures. SDK-checkout procedures resolve
through OPENMOTION_SDK_ROOT (falling back to the imported omotion
package's parent) and run in module form with the checkout as working
directory so its omotion package wins the import race.

Operator interaction is now uniform: any input() prompt (a stdout tail
ending ': ', or any tail after a quiet interval) is flushed to the
terminal and armed as a free-text prompt; a new input row under the
terminal writes answers to the child's stdin. The guided runner's
Continue/Left/Right buttons, '[y to continue' / '[left/right]' matching,
and @@simple protocol are gone; non-verbose mode now filters on a
generic operator-relevant pattern.

Registry now carries the three supported WI-00015 procedures
(single-sensor laser, dual-sensor laser, safety calibration) from the
SDK's per-procedure scripts.

Refs #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Operator defaults to the logged-in username and the fixture/rig ID to
the Wi-Fi adapter MAC (via getmac; netsh wlan is Location-gated), so
the WI15 scripts skip those two prompts. Values that cannot be
determined are omitted and the script prompts as before.

Refs #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refs #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a flushed prompt ends with a short slash-separated group -
'(left/right)', '(yes/no)', '[y/N]', '(single-left/single-right/dual)'
- the pane shows one button per option beside the free-text field;
clicking sends the option verbatim to the child's stdin. Free-form
prompts keep the text field alone. All current WI15 script prompts
already follow this shape, so no script changes are needed; any future
procedure ending a prompt this way gets buttons automatically.

Refs #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bench feedback: 13 px was too small to read across the bench; 16 px
for the terminal and the operator input field.

Refs #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The whitelist filter swallowed the safety procedure's new no-confirm
power-cycle instruction (a plain printed line), leaving the operator
waiting on a prompt that never appeared while the child timed out.
Non-verbose mode now hides only log-record-shaped lines; everything
else a procedure prints is operator output and stays visible.

Refs #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat: Procedures pane - guided WI-00015 runs from the app
A PyInstaller build's sys.executable is the app exe, so the Procedures
pane would relaunch the app instead of running the procedure script.
Frozen builds now resolve python from PATH and refuse with a clear
message when none is installed.

Refs #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The frozen app failed at boot with ModuleNotFoundError: crcmod - the
PYTHONPATH-sourced omotion imports it for wire-protocol CRCs and the
spec's analysis missed it.

Refs #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The WI-00015 runners now ship inside the omotion package
(omotion.scripts.*, openmotion-sdk PR #232), so the pane no longer
resolves an SDK checkout to run them from. Launch is python -m
omotion.scripts.<name> with the evidence root (~\Documents\OpenMotion)
as working directory; OPENMOTION_SDK_ROOT becomes an optional override
for which omotion wins the child's import race, and a bench whose Python
has the wheel installed needs no setup at all. The availability check
probes the interpreter for omotion.scripts only when no checkout
carries the modules.

Refs #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…python

fix: resolve a real Python interpreter for procedures in frozen builds
Audit trail: every line the pane terminal records - child procedure
output, operator answers (buttons and typed sends both funnel through
answerPrompt), pane status messages, and the final PASS/FAIL verdict -
is now also written to the application session log via the module
logger, regardless of the Verbose display filter. Pane opening
(Component.onCompleted on the re-instantiated page) and procedure
selection are logged too. Pinned by a launcher unit test.

Refs #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PyInstaller _internal directory ships the app Python's stdlib
extension modules alongside loose omotion package files, so it passed the
import-root file check and got prepended to the child's PYTHONPATH -
shadowing the bench interpreter's own stdlib. Seen live on the QA bench
2026-08-14: a Python 3.14 child resolved the bundled 3.13 _ctypes.pyd and
died with 'Module use of python313.dll conflicts with this version of
Python'. Frozen builds now rely on OPENMOTION_SDK_ROOT or the
interpreter's installed omotion wheel only.

Refs #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-python

fix: frozen import-root poisoning + pane audit logging (post-#99 commits)
A released build could not run a procedure at all unless the bench PC
happened to have both a Python on PATH and an openmotion-sdk wheel
installed in it - the pane shelled out to `python -m omotion.scripts.*`
and reported "not importable by <interpreter>" otherwise. That is a hard
blocker on a factory floor, and it was avoidable: the app already bundles
omotion, it just had no way to reach it.

A frozen build is now its own procedure runner. `--run-procedure <module>`
re-executes the bundle as a plain runner for that module, so the procedure
code, the omotion it drives and the Python running both come from the
release itself - nothing to install, and the app version pins the
procedure version for evidence. The console-subsystem twin is preferred
as the child: its stdio is what the pane's pipes expect, whereas the
windowed exe starts with its streams detached.

The flag is handled above main.py's imports on purpose. Importing
motion_singleton constructs a MOTIONInterface, which would grab the very
device handles the parent app just released for this child.

PyInstaller starts Python isolated, so the child ignores PYTHONUNBUFFERED
and PYTHONUTF8 - the runner reconfigures stdout/stderr to UTF-8
line-buffered itself. That same isolation is a feature here: a stray
PYTHONPATH on a bench cannot steer a released build away from its bundled
procedures.

OPENMOTION_SDK_ROOT survives as the deliberate opt-out, for trying a fixed
procedure ahead of an app build. Running from source is unchanged.

openwater.spec names the omotion.scripts modules explicitly - they are only
ever imported by name at runtime - and now fails the build if the SDK it
packages against does not carry them, so this cannot regress into a bench
failure again.

Verified on a real build: all four registry procedures run from
dist/TestApp/TestApp_console.exe with no external Python, an intruder
module on PYTHONPATH is refused, and a full run walks its operator
prompts, enters the workflow and writes run.json + report.html (failing at
bench preflight, as it should with no hardware attached).

Refs #70

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removes the OPENMOTION_SDK_ROOT override and the machinery that existed to
serve it. A release now runs the procedures in the omotion it was built
with, and nothing on the bench - env var, PYTHONPATH, or stray checkout -
can substitute a different copy.

The override answered "how do I run a procedure other than the bundled
one?". That question has a better answer outside the app: run it from the
SDK. Keeping an in-app path to it meant a release could produce calibration
evidence whose provenance the app could not vouch for, since the artifacts
record the app version but not which copy of the procedure actually ran.

What goes with it:

  _candidate_import_roots       ranked checkouts competing to win the
                                child's import race
  _interpreter_has_omotion_scripts + _probe_cache
                                subprocess-probed a foreign interpreter for
                                the wheel; impossible in a release anyway,
                                where no other interpreter exists to probe
  _python_interpreter, shutil   hunted for a python.exe on PATH
  import_root + the PYTHONPATH  made a checkout shadow the bundled copy
  block in _spawn_procedure

_sdk_module_procedure is now two branches - the bundle when frozen, this
interpreter when not - with availability from one in-process find_spec.
That is sound for both, because both give the child exactly this process's
imports.

Verified on a rebuilt bundle: with OPENMOTION_SDK_ROOT and PYTHONPATH both
aimed at a checkout whose copy of wi15_safety_calibration prints a marker,
the marker never appears - the bundled copy runs. All four procedures still
launch, and a full run walks its operator prompts and cancels cleanly.

Net -101 lines. OPENMOTION_SDK_ROOT survives in one place: a test asserting
it makes no difference.

Refs #70

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A bench run today ended with the app vanishing mid-procedure-start, leaving
nothing behind: no traceback, no log line, only a Windows c000041d
(STATUS_FATAL_USER_CALLBACK_EXCEPTION) record naming an unresolved module.

Nothing about that is unlucky - it is the designed behaviour. PyQt reports
an unhandled exception in a slot and then calls qFatal(), and a windowed
build has no stderr to report to, so main points it at os.devnull before
anything can be logged. Every exception in a slot is therefore a silent
abort. The pane also releases device handles on a worker thread, where an
exception has never been reported at all.

install_crash_logging covers the three ways this app can die quietly:

  sys.excepthook       runs as part of PyQt's reporting, so a slot's
                       traceback reaches the log before the abort
  threading.excepthook worker threads (the device-release worker)
  faulthandler         native faults, which no Python hook can catch -
                       dumps every thread's Python stack to a file beside
                       the app log, naming the call that was in flight

Handlers are flushed inside the hook because qFatal aborts the moment it
returns, and the faulthandler file is held open for the process lifetime
since it is written from a fault context.

This does not fix the crash. The suspected trigger is an SDK teardown race:
the crash run had a sensor stuck in an Access-denied connect-retry storm
when the pane called motion_interface.stop(), which then took 6.8 s to
return (instant in healthy runs). A later session with no storm completed
three release/reacquire cycles cleanly and passed a Safety Calibration on
hardware. The next occurrence will now leave a stack to work from.

Refs #70

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
boringethan and others added 17 commits August 17, 2026 15:11
…mport-6b1d40

fix: run test procedures from the release's own bundle
The WI-00015 procedures' meter layer defers "import win32com.client",
so nothing imports win32com statically and nothing declared pywin32 for
the build environment. A fresh factory PC running the packaged exe
failed with "Ophir COM construction failed: No module named 'win32com'"
(2026-08-17). Pin pywin32 in requirements.txt for CI, probe for it in
the spec so a bare build environment fails at build time instead of at
the bench, and bundle win32com plus pythoncom/pywintypes explicitly,
mirroring the crcmod precedent.

Refs #102

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aging

fix: bundle pywin32 so the packaged exe can drive the Ophir meter
The PyQt6 wheel carries 14.26 (2020) MSVC runtime DLLs in Qt6/bin.
Qt loads first at app startup and Windows pins the first msvcp140.dll
loaded, by base name, for the whole process — so when Ophir's
OphirLMMeasurement.dll (built against 14.44) later loads into the
packaged exe, its DllMain fails with ERROR_DLL_INIT_FAILED
(com_error -2147023782), seen on the 2026-08-17 factory install.

After Analysis, swap every bundled MSVC runtime DLL for the build
machine's System32 copy when that copy is at least as new, logging each
swap. The 14.x C++ runtime is ABI-stable, so Qt built against 14.26
runs unchanged on the newer runtime.

Refs #102

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix: ship a current MSVC runtime so Ophir's COM server can initialize
Hide "# "-prefixed detail lines (the SDK scripts' narration convention,
omotion.calibration.script_support.DETAIL_PREFIX) when Verbose is off;
the filter stays display-only and retroactive, and the audit log still
receives every line. The pane's own chrome joins the convention:
launching command line, device release/reacquire progress, and the
child's exit code become detail lines, with a single visible
"Preparing to start ..." replacing the handoff chatter. The
"procedure source:" provenance line moves to the application log only,
and the end-of-run verdict now reads "Final result: PASS/FAIL",
matching the scripts' own final line. Failure lines ("! ...") remain
always-visible.

Refs #109

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erbose-output

Procedures pane: honor the '# ' detail convention and trim chrome
The Procedures pane's dropdown and run banner carried a 'WI-00015 '
prefix on every procedure name. Per review feedback the work-instruction
number must not appear in operator-facing UI text, so the display names
drop it. Module paths (omotion.scripts.wi15_*), the wi15_out evidence
dir, and evidence traceability (PROCEDURE_ID in file names / recorded
revision) are functional identifiers and stay unchanged.

Refs OpenwaterHealth/openmotion-bloodflow-app#467

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Procedure evidence was rooted at ~/Documents/OpenMotion; a bench
install should carry its evidence with it instead, so the evidence
root is now the app's own folder - the release folder in a frozen
build, the checkout in a source run (wi15_out/ is gitignored there).
Existing evidence in Documents stays where it is; the pane still
prints each run's exact paths.

Refs #112

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pp-folder

feat: keep procedure report outputs in the app folder
…iage-434f66

fix: remove WI-00015 verbiage from operator-facing procedure names
Title bar (main.qml) and the Qt application name / QSettings scope
(utils/warranty_ack.py) still said Engineering App. The settings scope
holds the warranty acceptance flag, so the rename moves it; a one-time
read-only fallback migrates acceptances recorded under the legacy
"Open-MOTION Engineering App" scope so existing installs are not
re-prompted. Legacy key is never written or deleted; fails closed.

Refs #114

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename app from "Engineering App" to "Open-Motion Test App"
Undoes df7dc39: the pane passed --operator (logged-in username) and
--fixture-id (Wi-Fi adapter MAC) so the WI-15 scripts skipped those
prompts. Fixture collection is mandatory for every test, and the
prefill recorded machine identity where the evidence needs what the
operator attests - a shared Windows login is not an operator, and a
MAC address is not a fixture asset tag. The scripts now prompt for
both on every run.

Refs #119

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ethan: the logged-in-username operator prefill is fine to keep; only
the Wi-Fi-MAC fixture prefill must go, so the Fixture ID prompt runs
on every test. An undeterminable username is still omitted so the
script prompts instead of recording a wrong value.

Refs #119

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Procedures pane Verbose checkbox is now a session-only display
filter that starts OFF each launch, instead of defaulting on and
persisting via QSettings. The plain operator view is the default, and
an engineer leaving verbose on cannot change what the next operator
session shows. Display-only as before: the full transcript is always
retained and audit-logged.

Refs #121

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…naming-calibration-f2c22a

Procedures pane: stop prefilling the fixture ID; Verbose starts unchecked
@boringethan
boringethan requested a review from vpennam August 24, 2026 20:29
@boringethan
boringethan merged commit 4c75025 into main Aug 24, 2026
1 of 2 checks passed
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