Next - #104
Merged
Merged
Conversation
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>
…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
vpennam
approved these changes
Aug 24, 2026
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.
Summary
Licensing layer
license-manifestchange process (and board approval for anything leaving the core). Link the approval.Checklist
git commit -sSPDX-License-Identifierfor their layerCODEOWNERSreview requestedNotes for reviewers