"""Oscilloscope configuration for pre-scan angle inspection. Inspection is read-on-the-instrument: nothing in this module transfers or plots waveform data. The app puts the scope into a free-running, edge- triggered state and drives the stage to the point being inspected; the operator judges the SAW response and the bias levels on the scope screen. That split is deliberate. A scan's acquisition trigger is the logic AND of the laser pulse and the stage's max-velocity gate, and its transfers are FastFrame blocks — neither is useful for looking at one point by eye. Here the trigger is a plain edge on the laser pulse, FastFrame is off, and the acquisition free-runs, so the display updates continuously while the stage sits still. CH1 keeps the acquisition front-end so what is on screen is what a scan would record. CH3 and CH4 are rescaled as DC bias monitors (see BIAS_* below). """ from __future__ import annotations import logging from dataclasses import replace from core.scope_sras import SAMPLE_RATE_HZ, SRAS_CHANNELS, configure_channels logger = logging.getLogger(__name__) # CH2 carries the laser pulse. The scan triggers it at 0.5 V as one term of a # logic AND; inspection triggers well above that so a slow edge or a noisy # baseline cannot free-run the display. INSPECT_TRIG_LEVEL_V = 2.0 # CH3/CH4 are the DC bias monitors during inspection. The signal never goes # negative and spans roughly 0–700 mV, so both channels get the *same* scale # and position — the point of inspecting them is comparing the two by eye, and # that only works if a division means the same thing on each. # # Ground sits BIAS_POSITION_DIV divisions below centre, which puts the whole # 0–700 mV range above the centre line with a little room underneath for # undershoot. With 100 mV/div and ground 3.5 divisions low, the visible window # runs from about -50 mV to +750 mV on an 8-division display and wider on a # 10-division one, so 0–700 mV sits comfortably inside either. BIAS_CHANNELS = (3, 4) BIAS_WINDOW_V = 0.700 BIAS_SCALE_V_DIV = 0.100 BIAS_POSITION_DIV = -3.5 BIAS_LABELS = {3: "Bias - A", 4: "Bias - B"} def inspect_channel_profiles() -> dict: """Channel front-end config for inspection. CH1 and CH2 are the acquisition profiles verbatim. CH3 and CH4 differ only in label, scale and position — termination, coupling and bandwidth stay as the scan sets them, so the bias reading is the same measurement the scan records, just displayed usefully. """ profiles = dict(SRAS_CHANNELS) for ch in BIAS_CHANNELS: profiles[ch] = replace( SRAS_CHANNELS[ch], label=BIAS_LABELS[ch], scale_v_div=BIAS_SCALE_V_DIV, position_div=BIAS_POSITION_DIV, ) return profiles def configure_inspection(scope) -> None: """Put the scope into free-running inspection mode. Leaves the acquisition running, so the display stays live while the operator moves between angles and points. """ configure_channels(scope, inspect_channel_profiles()) # Plain edge trigger on the laser pulse — no logic pattern, so the stage # gate plays no part and a stationary stage still triggers. scope.write("TRIGger:A:TYPe EDGE") scope.set_trigger_source(2) scope.set_trigger_slope("RISE") scope.set_trigger_level(2, INSPECT_TRIG_LEVEL_V) scope.set_trigger_mode("NORMAL") # No averaging: a weak or intermittent SAW response is exactly what the # operator is looking for, and averaging would hide it. scope.set_acquire_mode("SAMPLE") scope.set_fastframe_state(False) scope.set_sample_rate(SAMPLE_RATE_HZ) scope.write("HORizontal:POSition 30") # Free-run rather than single-sequence, so the trace keeps updating. scope.write("ACQuire:STOPAfter RUNSTop") scope.write("ACQuire:STATE RUN") def stop_inspection(scope) -> None: """Halt the free-running acquisition. The next scan reconfigures the scope from scratch, so this only needs to stop the sweep — it does not try to restore the acquisition profile. """ scope.write("ACQuire:STATE STOP")