Skip to content

Commit e7227e1

Browse files
dccoteclaude
andcommitted
Unify HOPS driver: HOPSDevice with pluggable HOPS interfaces
Problem: The two device classes (VerdiGDevice over CohrHOPS.dll, and NativeVerdiGDevice over pyftdi I2C) were the same laser split only by transport, with duplicated capability logic. Solution: a single HOPSDevice that delegates every capability hook to an interchangeable HOPSInterface: - HOPSInterface (hops.py): abstract transport contract; interlock/faults default to raising HOPSInterface.NotSupported. - HOPSNativeInterface (hopsnative.py): pure-Python pyftdi I2C (no DLL). Native interlock/faults raise NotSupported until the ?FF decode is reverse-engineered. - HOPSDLLInterface (hopsdll.py): Coherent's CohrHOPS.dll (ASCII), with the ctypes binding CohrHOPS. HOPSDevice(interface="auto") tries native first, then the DLL; "native"/"dll"/an instance force one. HOPSDevice keeps InterlockControl in its capability set; interlock() simply raises NotSupported on the native transport (to be fixed). Removes VerdiGDevice / NativeVerdiGDevice and their tests; adds testHOPSDevice.py (debug + native-on-mock + read-modify-write always run; a hardware class skips when no laser is present). Hardware-confirmed end to end on the lab Genesis CX-Vis with interface="auto" (picks native): identity, on/off, shutter, remote, power, temperature; interlock() raises NotSupported as designed. Docs/CHANGELOG updated to HOPSDevice. 44 pass, 7 skip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01841beMNdCw3zYf6fdBUoy7
1 parent 19a870c commit e7227e1

11 files changed

Lines changed: 869 additions & 1052 deletions

CHANGELOG.md

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,26 @@ API changes can land even when the minor version is unchanged.
77
## [Unreleased]
88

99
### Added
10-
- `VerdiGDevice` (and `DebugVerdiGDevice`), plus `hops.py` (`HOPSInterface`): a
11-
driver for Coherent OPSL lasers on a "HOPS" power supply, controlled over USB
12-
through Coherent's `CohrHOPS.dll` rather than a serial protocol. The supply
13-
enumerates as an FTDI FT2232 (`0x0403:0x6010`) but is driven as bit-banged I2C
14-
to an NXP micro-controller, so the DLL (ASCII command API over that binary
15-
transport) is the only supported path; `hops.py` binds it with ctypes.
16-
`VerdiGDevice` combines `OnOffControl` (software switch `KSWCMD`),
17-
`PowerControl` (`PCMD`/`?P`), `ShutterControl` (`SHCMD`/`?SH`), and
18-
`InterlockControl` (`?FF` fault bitmask). Verified on the lab unit (which
19-
reports as a Genesis CX-Vis, head `G532`): the read path and the remote/power
20-
setpoint write path (`REM=`, `PCMD=`) are hardware-confirmed; the emission
21-
enable (`KSWCMD`) and shutter (`SHCMD`) are per the DLL spec but not yet
22-
exercised on hardware. The DLL is Windows/Linux-only,
23-
so on macOS `doInitializeDevice` raises `UnableToInitialize`; use
24-
`DebugVerdiGDevice`. Protocol reference in
25-
`manuals/Coherent-HOPS-USB-Protocol.md`.
26-
- `NativeVerdiGDevice` (+ `DebugNativeVerdiGDevice`, `HOPSNativeI2C`, `MockHOPSBus`)
27-
in `sources/hopsnative.py`: a **pure-Python** driver for the same HOPS laser
28-
over `pyftdi` I2C, with no CohrHOPS.dll (works on macOS/Linux). Reverse-
29-
engineered from the sniffed I2C protocol (see `manuals/Coherent-HOPS-I2C-*`):
30-
identity/temperature from EEPROM 0x52 / ADC 0x48, on-off/shutter/remote from
31-
I/O expander 0x25 (read-modify-write), power setpoint to DAC 0x29. Advertises
32-
`OnOffControl` / `ShutterControl` / `PowerControl`; not `InterlockControl`,
33-
because the full `?FF` fault decode, `PLIM`, and the ADC power-read scale were
34-
not pinned down by the capture (documented gaps). Hardware-confirmed end to end
35-
on the lab Genesis CX-Vis (identity, states, remote/shutter/enable writes,
36-
power=0, temperature). Requires pyftdi + a reachable libusb; on Ventura it
37-
claims the FT2232 without unloading Apple's VCP driver.
10+
- `HOPSDevice` (and `DebugHOPSDevice`): one laser-source driver for a Coherent
11+
"HOPS" (High Output Power Supply) laser -- Genesis heads / Verdi G-C, e.g. the
12+
lab Genesis CX-Vis (head `G532`). A HOPS supply is not a serial device: its
13+
FTDI FT2232 (`0x0403:0x6010`) is driven as bit-banged I2C, with power DAC, ADC,
14+
shutter/enable GPIO, and the head identity/calibration EEPROM all on one I2C
15+
bus (see `manuals/Coherent-HOPS-*`). `HOPSDevice` combines `OnOffControl`,
16+
`ShutterControl`, `PowerControl`, and `InterlockControl`, and drives the bus
17+
through an interchangeable `HOPSInterface`:
18+
- `HOPSNativeInterface` (`sources/hopsnative.py`): **pure-Python** pyftdi I2C,
19+
no DLL (macOS/Linux). Hardware-confirmed end to end on the lab unit
20+
(identity, on/off, shutter, remote, power setpoint, temperature). Its
21+
`interlock()`/`faults()` raise `HOPSInterface.NotSupported` until the `?FF`
22+
decode is reverse-engineered.
23+
- `HOPSDLLInterface` (`sources/hopsdll.py`): Coherent's `CohrHOPS.dll` (ASCII
24+
command set; Windows/Linux). Read + `REM`/`PCMD` write paths hardware-
25+
confirmed; `KSWCMD`/`SHCMD` per the DLL spec, not yet exercised.
26+
Selection: `HOPSDevice(interface="auto")` tries native first, then the DLL;
27+
pass `"native"`/`"dll"`/an interface instance to force one. Protocol and I2C
28+
decode in `manuals/Coherent-HOPS-USB-Protocol.md` and
29+
`manuals/Coherent-HOPS-I2C-Decode.md`.
3830

3931
## [1.4.0] - 2026-07-08
4032

hardwarelibrary/manuals/Coherent-HOPS-USB-Protocol.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,24 @@ x86-64).
8383

8484
## Using it from this library
8585

86-
`hardwarelibrary/sources/hops.py` binds the DLL with ctypes:
86+
`HOPSDevice` drives the laser; the DLL is one of its interchangeable transports
87+
(`HOPSDLLInterface`), the native pyftdi I2C path (`HOPSNativeInterface`) the
88+
other. Normally you just let it pick:
8789

8890
```python
89-
from hardwarelibrary.sources.hops import HOPSInterface
90-
91-
hops = HOPSInterface() # loads CohrHOPS.dll (Windows/Linux)
92-
print("DLL", hops.version())
93-
handles = hops.checkForDevices() # USB discovery
94-
head = hops.initializeHandle(handles[0]) # -> head-type string, or HOPSError(INVALID_HEAD)
95-
print("head:", head)
96-
print(hops.sendCommand(handles[0], "?HTYPE"))
97-
print(hops.sendCommand(handles[0], "?LASERMODEL"))
98-
hops.close(handles[0])
91+
from hardwarelibrary.sources.hops import HOPSDevice
92+
93+
laser = HOPSDevice(interface="auto") # native first, then the DLL
94+
laser.initializeDevice()
95+
print(laser.laserModel, laser.headType, laser.headSerialNumber)
96+
laser.setPower(0.5); laser.turnOn(); laser.openShutter()
97+
laser.shutdownDevice()
9998
```
10099

101-
On macOS this raises `HOPSUnavailable` (no DLL exists for the platform).
100+
To force the DLL transport, use `HOPSDevice(interface="dll")`. The low-level
101+
ctypes binding to `CohrHOPS.dll` is `CohrHOPS` in `sources/hopsdll.py`; on macOS
102+
constructing the DLL interface raises `HOPSInterface.Unavailable` (no DLL for the
103+
platform), so `interface="auto"` falls through to native.
102104

103105
## Open question for the Verdi G specifically
104106

hardwarelibrary/manuals/Coherent-HOPS-Verdi-G-Runbook.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ do it again. Written so a future session (human or AI) can reproduce it without
55
rediscovering the dead ends.
66

77
For the protocol details see `Coherent-HOPS-USB-Protocol.md`; for the driver see
8-
`hardwarelibrary/sources/verdig.py` and `hardwarelibrary/sources/hops.py`. This
9-
file is the *workflow*.
8+
`hardwarelibrary/sources/hops.py` (`HOPSDevice`) and its interfaces
9+
`hopsnative.py` / `hopsdll.py`. This file is the *workflow*.
1010

1111
---
1212

@@ -229,20 +229,20 @@ cleared interlock + `REM=1` + `KSWCMD=1` + `PCMD>0`; the mechanical shutter
229229

230230
## The driver
231231

232-
`hardwarelibrary/sources/hops.py``HOPSInterface`, a ctypes binding to
233-
`CohrHOPS.dll` (raises `HOPSUnavailable` where the DLL cannot load, e.g. macOS).
232+
`hardwarelibrary/sources/hops.py``HOPSDevice` (+ `DebugHOPSDevice`), one
233+
laser-source driver combining `OnOffControl` / `ShutterControl` / `PowerControl`
234+
/ `InterlockControl`. It drives the bus through an interchangeable
235+
`HOPSInterface`, chosen with `HOPSDevice(interface="auto")` (native first, then
236+
DLL), or forced with `"native"` / `"dll"` / an instance:
234237

235-
`hardwarelibrary/sources/verdig.py``VerdiGDevice` (+ `DebugVerdiGDevice`),
236-
mapping the HOPS command set onto the library's capability mixins: `OnOffControl`
237-
(software switch `KSWCMD`), `PowerControl` (`PCMD`/`?P`), `ShutterControl`
238-
(`SHCMD`/`?SH`), `InterlockControl` (`?FF`). Bulk telemetry is exposed via a
239-
`diagnosticReads` table + `diagnostics()` / `diagnostic(name)`, with `query()` as
240-
the raw escape hatch. Tests: `hardwarelibrary/tests/testVerdiG.py`.
238+
- `hopsnative.py``HOPSNativeInterface`: pure-Python pyftdi I2C, no DLL
239+
(macOS/Linux). Hardware-confirmed end to end on the lab unit; `interlock()` /
240+
`faults()` raise `HOPSInterface.NotSupported` until the `?FF` decode is done.
241+
- `hopsdll.py``HOPSDLLInterface`: Coherent's `CohrHOPS.dll` (ASCII command
242+
set; Windows/Linux), with the ctypes binding `CohrHOPS`.
241243

242-
It runs only where `CohrHOPS.dll` loads (the Windows VM). On macOS, use
243-
`DebugVerdiGDevice`. A native-macOS path would require reimplementing the
244-
bit-banged-I2C framing over `pyftdi` MPSSE (the DLL's `.pdb` symbols map the
245-
`NXP`/`I2C` classes) — a separate project, not done here.
244+
Tests: `hardwarelibrary/tests/testHOPSDevice.py` (debug + native-on-mock always
245+
run; a hardware class skips when no laser is reachable).
246246

247247
---
248248

@@ -251,7 +251,7 @@ bit-banged-I2C framing over `pyftdi` MPSSE (the DLL's `.pdb` symbols map the
251251
- Read path (identity, power, interlock/faults, all temperatures/hours): OK.
252252
- Write path `REM=1` and `PCMD=0.5` -> read back `?PCMD`=0.498 (head quantizes
253253
to ~2 mW, inside the driver's tolerance), `?P` stayed 0 (no emission, shutter
254-
closed), then restored. So `VerdiGDevice`'s REM/PCMD/confirm logic is proven.
254+
closed), then restored. So the DLL interface's REM/PCMD/confirm logic is proven.
255255
- **Not yet exercised on hardware (safety):** `KSWCMD` (emission enable) and
256256
`SHCMD` (shutter open). Verify with the beam blocked and interlock cleared.
257257
- Note: at probe time `?FF`=`0x0220` — an active **interlock fault** (+ glue-board
@@ -271,5 +271,5 @@ bit-banged-I2C framing over `pyftdi` MPSSE (the DLL's `.pdb` symbols map the
271271
`RUN_ME.bat` (read-only probe) — confirm `?LASERMODEL` / head type.
272272
5. Read `hops_probe_output.txt` back on the Mac.
273273
6. For control, either run PowerShell that calls the DLL (as above) in the guest,
274-
or run `VerdiGDevice` (this library) inside the guest with a real Python +
274+
or run `HOPSDevice(interface="dll")` (this library) inside the guest with a real Python +
275275
the DLLs. For emission, first clear the interlock and block the beam.

hardwarelibrary/sources/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
MillenniaEv25Device, DebugMillenniaEv25Device,
1111
MillenniaDevice, DebugMillenniaDevice,
1212
)
13-
from .verdig import VerdiGDevice, DebugVerdiGDevice
14-
from .hopsnative import (
15-
HOPSNativeI2C, NativeVerdiGDevice, DebugNativeVerdiGDevice, MockHOPSBus,
13+
from .hops import (
14+
HOPSDevice, DebugHOPSDevice, HOPSInterface, DebugHOPSInterface,
1615
)
16+
from .hopsnative import HOPSNativeInterface, HOPSNativeI2C, MockHOPSBus
17+
from .hopsdll import HOPSDLLInterface

0 commit comments

Comments
 (0)