Commit Graph

16 Commits

Author SHA1 Message Date
Thomas Ales aeac5fe4d6 commit 2026-09-05 20:29:53 -05:00
Thomas Ales 6e8c1cb7a2 Auto-align: level the sample on the DC bias levels from the camera window
The operator frames a good spot, confirms the two DC levels the detector
reads there, and the rig then measures its own tilt: step 1.5 mm either side
on X and then on Y, and tilt the platform until those levels come back.  The
correction that fixes an offset point is the correction that levels the whole
travel — height error and tilt effect are both proportional to the offset —
so the procedure ends by applying it and leaving it applied.

Both directions are measured from the same starting tilt and averaged, which
makes their disagreement a flatness read-out rather than something averaged
away silently.

core/auto_align.py holds the geometry and the search, Qt-free.  The three
T-axes' azimuths are the whole geometry: T1 lies along +X so it alone tilts
along X, and T0/T2 move as an equal-and-opposite pair to tilt along Y without
touching X (tilt_response derives that, and the tests pin it — an axis map
that drifts would still converge, on the wrong axis).  The search is a secant
null on the split-detector difference: probe once to learn what a microstep
is worth, sign included, then step at the null.  It refuses to servo on a
scope that has not re-triggered, escalates a probe that reads as no response
before calling an axis dead, and stops at a per-axis travel limit.

gui/align_bridge.py runs it on a worker thread; stopping is a threading.Event
rather than a queued command, because the worker is inside a long handler for
the whole run.  The camera window carries the button and the progress window,
and locks the scan panel and the jog pads while a run owns the stage.

Adds immediate MEAN measurements and an acquisition count to the scope
driver, and read_bias_mv to core/scope_inspect — the one scalar the
inspection state was missing.

KNOWN_ISSUES.md records what only the rig can settle: the probe step, the
travel limit, the hold current, and whether the piston the X phase applies
alongside its tilt matters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:00:36 -05:00
Thomas Ales d6a56266b7 Driver support for multi-row FastFrame bursts; fix read_raw block parsing
Groundwork for burst acquisition: the scope needs to report and transfer a
whole multi-row FastFrame acquisition, and the BBD needs to gate its trigger
output per row rather than staying armed for the scan.

tektronix_base
- get_fastframe_max_frames() exposes HORizontal:FASTframe:MAXFRames?, which
  is what sizes a burst once the horizontal settings are fixed.
- transfer_fastframe_bulk() pulls a burst as one contiguous buffer. Unlike
  transfer_fastframe it does not assume how the scope frames the response:
  it accumulates until the expected byte count is reached, so one large IEEE
  block and one block per frame both work.
- set_data_encoding() / set_data_width() make the transfer format settable
  instead of inherited from whatever the front panel was left on.
- read_raw() had two real defects. The length-digit read used a bare recv()
  and only checked the length afterwards, so a short read raised "Failed to
  read data length" on a perfectly good transfer; it now goes through a
  _recv_exact() helper, as does the trailing separator. And a #0
  indeterminate-length block was parsed as int("") -> ValueError. #0 is
  normally delimited by EOI, which a raw socket never sees, so read_raw now
  takes expected_bytes to size it. The bulk transfer relies on this.

pybbd202
- arm_scan_gate(axis, armed) raises and drops the max-velocity trigger
  output the scope's AND-gate uses. A burst spans several rows with the
  scope running throughout, so the gate must be low for the flyback or the
  return move reaches max velocity and injects frames between rows.
- set_trigger_verified() reads the mode back after setting it. set_trigger
  is fire-and-forget over the shared TX queue; burst mode toggles the gate
  between every row, where a dropped change silently corrupts the file
  rather than failing loudly.
- set_trigger_gate_off() so the scan can leave the output idle on exit.
- TRIGOUT_GATE_OFF is deliberately marked unverified. §7.6 of the BBD203
  protocol doc describes `mode` as an enumeration capping at 0x11, which
  contradicts the bitmask this driver actually sends (TRIGOUT_MAXV = 0x90,
  known working), so the doc cannot settle which value idles the pin low.
  The engine's preflight check resolves it on the rig instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 12:16:20 -05:00
Thomas Ales 709dc529df Phase 6: strip signature-restating docstrings; correct README/SETUP
- Collapsed Args:/Returns:/Raises: blocks that only restated the
  signature (364 lines): tektronix_base 48% -> ~20% doc density,
  helios_laser and uc480_camera likewise. Only docstrings whose entire
  body was those sections were touched.
- Preserved verbatim the comments that carry hardware knowledge the code
  can't express: uc480's USB split-transaction contention note (with its
  measured fps), the IS_ALLOW_STARTER_FW_UPLOAD segfault explanation, the
  QImage-copy rationale, and tektronix's NUMFRAMESACQuired warning.
- README: project structure, quick start, and every usage example now
  describe code that exists (they referenced hardware/bbd202.py,
  CoherentHOPSLaser, get_curve_binary, and 'python -m scanengine.app',
  none of which do). Added a headless-scan example and a read-a-scan-file
  example, since reuse without the GUI is the point of the refactor.
- SETUP: structure section defers to README instead of keeping a second
  stale copy; documents the vendored uEye SDK and the Genesis quarantine.
- ruff is now clean repo-wide: fixed the remaining raise-from, unused
  loop variables, placeholder f-strings, and a non-strict zip; the
  widget-layout semicolon idiom is an explicit config ignore rather than
  22 standing warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 11:27:36 -05:00
Thomas Ales 44febe34b8 Phase 5: shared worker base, self-rescheduling polls, driver robustness
gui/qt_workers.py — one QueueWorker base replaces the per-device command
queue + dispatch + signal boilerplate. The loop blocks on the queue
instead of waking 10-20x/second forever (test_idle_worker_does_not_spin
asserts an idle worker burns ~no CPU). PollingQueueWorker adds
self-rescheduling polling: the next poll is queued only after the
previous finishes, so a device slower than the interval can't accumulate
a backlog (test_polling_never_overlaps_or_backs_up).

Helios responsiveness — the concrete bug that motivated the above: a free
running 1 s QTimer queued a status poll that took ~2 s, so the queue grew
for as long as the panel stayed connected.
- helios_laser._query reads until the CR terminator instead of sleeping a
  fixed 0.05 + 0.2 s per query
- one _query_int() helper replaces five copies of parse-with-logging
- polling is now driven by the worker; HeliosWindow's QTimer is gone
- dropped __del__, which disabled the laser and wrote to the serial port
  from the garbage collector at an unpredictable time

helios_test_app.py — the worker was moveToThread'd but every call site
invoked its methods directly, so all serial I/O (including the sleeps)
ran on the GUI thread; Query All froze the UI for ~2 s. Calls now go
through a queued signal to a pyqtSlot. Also: connect/disconnect cycles
leaked a QThread + worker + 9 connections each time; 16 copies of the
not-connected guard collapse to _require_connection(); the Query Power
button called a method that has never existed (AttributeError popup) and
is now disabled and documented in KNOWN_ISSUES.

DCBiasImageWidget preallocates its image and uses set_data/set_clim, so
the live preview stops rebuilding the array and the whole artist tree per
row (O(rows^2) over a scan).

bbd20x: connect() now raises when no bays respond instead of reporting
success on the wrong port; disconnect() joins with a timeout so a wedged
reader can't hang shutdown; one _channel_for() helper replaces four
copy-pasted axis mappings; hardcoded travel limits become TRAVEL_MM; the
joke error strings are gone.

gui/widgets.py adds the shared ConnectionBar / PortSelector / bounded
LogConsole / StatusGrid for the test benches to adopt. 65 tests passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 11:21:29 -05:00
Thomas Ales afe33249d1 Phase 4: extract headless ScanEngine; de-Qt the T3R driver
The headline of the refactor. Scan orchestration no longer lives inside a
QObject that reaches through Qt workers for its hardware handles.

core/scan_engine.py — ScanEngine(stage, scope, rotator, plan, out_path,
resume, callbacks). Takes the concrete drivers, blocks in run(), reports
via plain callables, and prompts through an injected blocking callable.
No Qt import anywhere in the path (test_engine_imports_without_qt proves
it), so a simpler GUI or a CLI can drive the identical acquisition.

Supporting extractions, all Qt-free:
- core/scope_sras.py  — SCPI policy: channel profiles, trigger programming,
  background average, per-row FastFrame transfer
- core/rotation.py    — RotationAxis + RotationSettings (the GR_* constants)
- core/scan_resume.py — frontier contiguity rule + settings compatibility
- gui/scan_bridge.py  — QtScanController, exposing exactly the signal
  surface the old ScanWorker had, so MainWindow's connections are unchanged

hardware/t3r_driver.py is now Qt-free: a plain Signal class, a threading
reader, and a polling thread instead of QObject/QThread/QTimer.
gui/qt_t3r.py re-emits its callbacks as queued Qt signals for the panels.

Fixes carried by the extraction:
- rotation waits on the driver's MOTION_DONE event instead of
  time.sleep(estimate + 0.5)
- abort during an operator prompt now takes effect; the old
  _prompt_event.wait() had no timeout and could not be interrupted
- the poll timer is a thread, so an I/O error tearing down the driver no
  longer calls QTimer.stop() from the wrong thread
- T3RDriver.disconnect() renamed close(); it shadowed QObject.disconnect()
- per-frame DC means use np.frombuffer over the joined block instead of
  struct.unpack per frame (~16k tuple allocations per row)

tests/fakes.py + test_scan_engine.py (14 tests) assert the exact command
sequence, file layout, resume seeking, abort/pause, and geometry
rejection before any hardware call; test_scan_resume.py covers the
frontier rule. 58 passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 11:11:09 -05:00
Thomas Ales dff9f69d78 Phase 2: extract headless core modules (sras_format, scan_geometry, config)
- core/sras_format.py: THE v6 implementation — create_scan_file (writer,
  byte-identical to the old one, enforced against the Phase-0 goldens),
  SrasFile parser with frontier/truncation walk, and zero-copy mmap
  load_angle/load_row views for multi-GB files
- core/scan_geometry.py: ScanPlan/AngleGeometry dataclasses, build_plan
  (rotated-bbox trig from MainWindow._build_scan_params), travel-limit
  validate_plan (limits now a StageLimits dataclass, not literals buried
  in the worker), format_eta + EtaEstimator (bounded deque)
- core/config.py: ScanDefaults dataclass replaces the module-import-time
  dict globals. FIXES: editing any main-window port used to rewrite
  aui_defaults.json without helios_port, silently reverting the Helios
  port every time (test_helios_port_survives_partial_update covers it).
  Also drops the inert laser_freq_hz plumbing — scans always used the
  LASER_FREQ_HZ constant.
- hardware/serial_util.py: shared 8N1 open + scored port enumeration
  (promoted from t3r_control_panel); helios_laser and the panel use it
- sc3_aui_app.py and sras_scan_manager.py migrated onto core (three
  format implementations down to one); ScanWorker now takes a ScanPlan
- tests: byte-identical writer vs golden, frontier over every truncation
  variant, mmap==eager, geometry vs golden fixtures + invariants, config
  round-trip. 28 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:20:44 -05:00
Thomas Ales 67aabde4b6 Phase 1c: prune remaining dead functions, unused imports, quarantine Genesis
- uc480_camera: drop never-called _capture_paused/get_framerate (the
  hardware question _capture_paused encoded is now in KNOWN_ISSUES.md)
- t3r_protocol: drop read_reg/write_reg/decode_reg/Reg (commands never
  wired into the driver)
- bbd20x: drop _update0x0212 (never dispatched) and 8 of 9 unused
  trigger convenience wrappers; apt_constants: drop TriggerBitsStepper
  (servo-only rig)
- ruff --fix: 35 unused imports across all apps; drop unused T3R_BAUD
- genesis_core.py: quarantine warning header; docs/genesis_verification.md
  bench checklist for the 7 divergences vs tools/genesis_laser_gui.py

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:08:48 -05:00
Thomas Ales 5148f0bca2 Phase 1b: prune 45 never-called methods from tektronix_base (~850 lines)
Verified by repo-wide name search + transitive closure over internal
calls: the live apps use 25 methods (plus raw write/query); everything
else — cosmetic label styling, unused getters/setters, transfer_waveform,
acquire_waveform — had no callers. Also: linear-time chunk join in
read_raw instead of quadratic bytes += concat, and a typed except on its
debug path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:04:41 -05:00
Thomas Ales 1d4e65f8ac Phase 1a: delete dead parallel app stack (~4,600 lines)
app.py was an abandoned skeleton (15 'pass # TODO' handlers, loads a
deleted .qss); everything else was reachable only from it:
ui_mainwindow.py (pyuic6 artifact), sc3-new.ui, motion_worker.py,
genesis_worker.py, coherent_hops_laser.py (stubs), scanning/ (dead C#
port + unused plan generator), config.json, plus helios_diagnostic.py
(sends wrong protocol commands) and helios_terminal.py (worse duplicate
of helios_test_app's Terminal tab).

hardware/__init__.py no longer wildcard-imports every driver, so the
stage driver imports without the uEye camera SDK installed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:02:36 -05:00
Thomas Ales 2041fc8439 pre merge cleanup commit 2026-07-28 09:20:58 -05:00
Thomas Ales [MSE] 1014533626 precommit 2026-07-21 20:10:06 -05:00
Thomas Ales [M S E] 57f50a733d pre local llm test 2026-05-28 18:29:24 -05:00
Thomas Ales [M S E] a0e0151b5d pre uc480 integration 2026-05-22 09:38:39 -05:00
Thomas Ales [M S E] 57613b7aca fixed app to use new thorlabs driver 2026-02-09 16:28:47 -06:00
Thomas Ales [M S E] 23f6331ba2 when'd i last commit this pos? 2026-02-09 14:40:34 -06:00