Skip to content

Add Stanford Research SR830 lock-in amplifier driver over Prologix GPIB#112

Merged
dccote merged 5 commits into
masterfrom
sr830-lockin
Jul 9, 2026
Merged

Add Stanford Research SR830 lock-in amplifier driver over Prologix GPIB#112
dccote merged 5 commits into
masterfrom
sr830-lockin

Conversation

@dccote

@dccote dccote commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a driver for the Stanford Research Systems SR830 DSP lock-in amplifier, reached over a Prologix GPIB-USB controller. Three reusable pieces result: a GPIB transport, new DAQ capability contracts, and the driver itself. Verified against the lab SR830 (s/n 86552).

What's included

PrologixGPIBPort (communication/prologixgpibport.py) — a SerialPort subclass speaking the Prologix ++ controller protocol. GPIB instruments talk to it with the ordinary readString/writeString primitives; it overrides only open() (controller handshake) and readString() (the ++read eoi step). Uses ++auto 0 + explicit reads and ++eot_enable 0 because the SR830 already terminates replies with CR-LF.

New DAQ capability contracts (daq/daqdevice.py), following the interface-segregated mixin convention:

  • PhaseLockedDetectionDevice — X/Y/R/theta, reference frequency, input source, sensitivity, time constant, all in physical units with driver-side snapping to hardware steps. Includes the InputSource enum.
  • TriggerableDevicesetTriggerSource/getTriggerSource/softwareTrigger, with the TriggerSource enum.
  • SampleClock enum for stream sample clocking (distinct from triggering).

SR830Device / DebugSR830Device (daq/sr830device.py) — combines AnalogInputStreamDevice (four rear-panel Aux inputs via OAUX?, plus hardware-timed buffered acquisition of the demodulated outputs), AnalogOutputDevice (four Aux outputs via AUXV), PhaseLockedDetectionDevice, and TriggerableDevice. Enums: AuxInput, AuxOutput, StreamChannel. doInitializeDevice self-discovers the Prologix among connected FTDI adaptors by confirming *IDN? returns an SR830, then pins the adaptor serial.

Changed (API-affecting)

  • AnalogInputStreamDevice: the sample-rate parameter of configureStream/acquireWaveform is renamed scanRate -> sampleRate. LabjackDevice.configureStream keeps scanRate as a temporary deprecated synonym.
  • LabjackDevice imports u3 lazily so import hardwarelibrary.daq works without LabJackPython.

Testing

  • pytest hardwarelibrary/tests/testSR830.py — 43 passed (debug/contract/port tests always run; 8 hardware tests skip cleanly when no SR830 is attached, and were run green against the real instrument).

🤖 Generated with Claude Code

https://claude.ai/code/session_01T2DmAfLHSiK1fb3PiXDyJP

dccote and others added 5 commits July 8, 2026 21:37
Problem: The DAQ family had no capability contract for lock-in (phase-locked)
detection or for triggered acquisition, so a lock-in driver would have to bolt
ad-hoc methods onto one class. The stream sample-rate parameter was named
scanRate, and importing hardwarelibrary.daq eagerly imported u3 (LabJackPython),
so the whole package failed to import on hosts without the LabJack driver.

Solution:
- Add PhaseLockedDetectionDevice (X/Y/R/theta, reference frequency, input source,
  sensitivity, time constant, all in physical units) and TriggerableDevice
  (setTriggerSource/getTriggerSource/softwareTrigger) with the TriggerSource and
  SampleClock enums, following the interface-segregated mixin convention.
- Rename the stream sample-rate parameter scanRate -> sampleRate on
  AnalogInputStreamDevice.configureStream/acquireWaveform. LabjackDevice keeps
  scanRate as a temporary deprecated synonym.
- Defer the u3 import in labjackdevice to point of use so the daq package imports
  without LabJackPython installed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2DmAfLHSiK1fb3PiXDyJP
Problem: The library had no way to reach a GPIB (IEEE-488) instrument. The common
lab bridge is a Prologix GPIB-USB controller, which enumerates as a generic FTDI
serial port and speaks a "++" controller protocol on top of the serial link.

Solution: Add PrologixGPIBPort, a SerialPort subclass that reuses the FTDI
discovery and read/write primitives and adds only the controller handshake in
open() (++mode 1 / ++addr / ++auto 0 / ++eoi 1 / ++eos 2 / ++eot_enable 0 /
++read_tmo_ms). Manual-read mode (++auto 0) is used because auto mode addresses
the instrument to talk after every command, including no-reply commands, wedging
the bus; the required ++read eoi is encapsulated in readString so instruments
talk to the port with the ordinary readString/writeString primitives. Export it
from the communication package.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2DmAfLHSiK1fb3PiXDyJP
Problem: The lab's SR830 DSP lock-in amplifier, reached over a Prologix GPIB-USB
adaptor, had no driver in PyHardwareLibrary.

Solution: Add SR830Device (and DebugSR830Device) combining PhysicalDevice,
AnalogInputStreamDevice, AnalogOutputDevice, PhaseLockedDetectionDevice, and
TriggerableDevice:
- Aux A/D inputs (OAUX?) via getAnalogVoltage(AuxInput); Aux D/A outputs (AUXV)
  via setAnalogVoltage(value, AuxOutput).
- Demodulated reads X/Y/R/theta (OUTP?/SNAP?), reference frequency (FREQ?), input
  source (ISRC), sensitivity (SENS) and time constant (OFLT) in physical units
  with nearest-step snapping.
- Buffered acquisition of the internal data buffer (DDEF/SRAT/SEND/REST/STRT/
  PAUS/SPTS?/TRCA?) via the AnalogInputStreamDevice primitives, with a
  StreamChannel enum and a SampleClock (internal rate vs external per-edge).
- Trigger via TSTR (setTriggerSource) and TRIG (softwareTrigger).
- doInitializeDevice self-discovers the Prologix among the connected FTDI adaptors
  by confirming *IDN? contains SR830, and pins the adaptor serial for reconnects.
- DebugSR830Device swaps in a DebugPrologixGPIBPort (an in-memory CommunicationPort
  that follows the readData/writeData structure) so all parse/validate paths run
  without hardware.

Verified against the lab SR830 (s/n 86552): tests/testSR830.py has 43 tests,
including 8 that talk to the real instrument (reads, aux out round-trip, buffered
acquisition, trigger) and skip cleanly when no SR830 is attached.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2DmAfLHSiK1fb3PiXDyJP
Problem: several methods added in this branch lacked docstrings, and a few
method-level explanatory comments read as inline "what this does" comments
rather than the docstrings the project style calls for.

Solution: add concise docstrings to every method and capability contract added
in this branch (PrologixGPIBPort, SR830Device/DebugSR830Device,
DebugPrologixGPIBPort, and the new PhaseLockedDetectionDevice/TriggerableDevice
abstract hooks and touched AnalogInputStreamDevice methods). Method-level
comments explaining behavior are converted to docstrings; genuine "why"
comments inside method bodies are kept inline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2DmAfLHSiK1fb3PiXDyJP
Problem: SR830Device.doInitializeDevice and _candidateAdaptors (the FTDI
adaptor probe/confirm/pin logic) were only exercised on real hardware, because
DebugSR830Device overrides doInitializeDevice. Several defensive branches
(unsupported-enum guards, the sensitivity clamp, base-capability defaults, and
DebugPrologixGPIBPort conveniences) were also untested, and the Prologix
handshake test asserted the pre-fix command set without exercising the real
port.

Solution: add hardware-free tests. TestSR830Discovery patches comports() and
PrologixGPIBPort to cover discovery, serial pinning, serial-number narrowing,
explicit portPath, and both failure raises. Add TestDebugPrologixGPIBPort for
the port primitives, TestCapabilityDefaults for the base optional hooks and
base getDemodulatedValues, and debug-device tests for the enum guards, the
sensitivity clamp, supportedSampleRates, and the aux/frequency SNAP codes.
Rewrite the handshake test to exercise the real configureController and assert
the manual-read (++auto 0, ++eot_enable 0) handshake.

This takes sr830device.py and prologixgpibport.py to 100% line coverage (62
tests, all passing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2DmAfLHSiK1fb3PiXDyJP
@dccote
dccote merged commit 0341024 into master Jul 9, 2026
14 checks passed
@dccote
dccote deleted the sr830-lockin branch July 9, 2026 02:15
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.

1 participant