Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,6 @@ cython_debug/
.DS_Store
stellarnet.py
stellarnet.hex

# Coherent HOPS vendor DLLs, capture logs, and local reversing prototypes (not for VCS)
scratch-hops/
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ API changes can land even when the minor version is unchanged.

## [Unreleased]

### Added
- `VerdiGDevice` (and `DebugVerdiGDevice`): a laser-source driver for the Coherent
"HOPS" (High Output Power Supply) laser -- Genesis heads / Verdi G-C, e.g. the
lab Genesis CX-Vis (head `G532`). A HOPS supply is not a serial device: its
FTDI FT2232 (`0x0403:0x6010`) is driven as bit-banged I2C, with power DAC, ADC,
shutter/enable GPIO, and the head identity/calibration EEPROM all on one I2C
bus (see `manuals/Coherent-HOPS-*`). `VerdiGDevice` combines `OnOffControl`,
`ShutterControl`, `PowerControl`, and `InterlockControl`, and drives the bus
through an interchangeable `HOPSInterface`:
- `HOPSNativeInterface` (`sources/hopsnative.py`): **pure-Python** pyftdi I2C,
no DLL (macOS/Linux). Hardware-confirmed end to end on the lab unit
(identity, on/off, shutter, remote, power setpoint, temperature). Its
`interlock()`/`faults()` raise `HOPSInterface.NotSupported` until the `?FF`
decode is reverse-engineered.
- `HOPSDLLInterface` (`sources/hopsdll.py`): Coherent's `CohrHOPS.dll` (ASCII
command set; Windows/Linux). Read + `REM`/`PCMD` write paths hardware-
confirmed; `KSWCMD`/`SHCMD` per the DLL spec, not yet exercised.
Selection: `VerdiGDevice(interface="auto")` tries native first, then the DLL;
pass `"native"`/`"dll"`/an interface instance to force one. Protocol and I2C
decode in `manuals/Coherent-HOPS-2-USB-and-DLL-Protocol.md` and
`manuals/Coherent-HOPS-3-I2C-Wire-Protocol.md`.

## [1.4.0] - 2026-07-08

### Added
Expand Down
16 changes: 16 additions & 0 deletions examples/verdig/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Verdi-G / HOPS examples

Read-only monitors for a Coherent HOPS-supply laser (Genesis / Verdi-G). Both
stream once per second and stop on Ctrl-C. Neither takes remote control, so the
front-panel wheel keeps working while they run.

- `temperature_monitor_native.py` — main temperature over **native pyftdi I2C**
(no DLL). macOS/Linux; needs pyftdi + libusb and the laser on this host.
- `status_monitor_dll.py` — full status (all four servo temperatures, power,
shutter, emission, interlock/faults) over **Coherent's `CohrHOPS.dll`**.
Windows/Linux; needs the DLLs (see `../../hardwarelibrary/manuals/`
`Coherent-HOPS-2-USB-and-DLL-Protocol.md`).

For the full driver and the two transports, see
`hardwarelibrary/sources/verdig.py` (`VerdiGDevice`) and the `Coherent-HOPS-*`
docs under `hardwarelibrary/manuals/`.
53 changes: 53 additions & 0 deletions examples/verdig/status_monitor_dll.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Stream full Verdi-G / HOPS status over Coherent's CohrHOPS.dll (Windows/Linux).

Read-only: it does not change the laser state or take remote control (the front
panel stays usable). Unlike the native transport, the DLL path also reads all
four thermal-servo temperatures and the interlock/fault state.

Setup: place CohrHOPS.dll and CohrFTCI2C.dll (matching your Python's bitness)
next to hardwarelibrary/sources/hopsdll.py, or pass dllDirectory=... below.

Run: python examples/verdig/status_monitor_dll.py [count]
"""
import datetime
import sys
import time

from hardwarelibrary.sources.hopsdll import HOPSDLLInterface


def main():
count = int(sys.argv[1]) if len(sys.argv) > 1 else None
interface = HOPSDLLInterface() # or HOPSDLLInterface(dllDirectory=r"C:\dlls")
interface.open()

identity = interface.identity()
print("Connected via CohrHOPS.dll: {0} (head {1}, s/n {2}, max {3:.3f} W)".format(
identity["model"], identity["headType"], identity["serialNumber"],
identity["maxPower"]))

read = 0
try:
while count is None or read < count:
diag = interface.diagnostics() # includes the four servo temperatures
temps = ("main={mainTemperature:.1f} SHG={shgTemperature:.1f} "
"BRF={brfTemperature:.1f} etalon={etalonTemperature:.1f}").format(**diag)
faults = interface.faults()
interlock = "ok" if interface.interlockOk() else "FAULT"
stamp = datetime.datetime.now().strftime("%H:%M:%S")
print("{0} {1} C | P={2:.3f} W shutter={3} emission={4} interlock={5}{6}".format(
stamp, temps, interface.getPower(),
"open" if interface.shutterOpen() else "closed",
"ON" if interface.emissionOn() else "off", interlock,
(" faults=" + ",".join(faults)) if faults else ""))
read += 1
if count is None or read < count:
time.sleep(1.0)
except KeyboardInterrupt:
print()
finally:
interface.close()


if __name__ == "__main__":
main()
40 changes: 40 additions & 0 deletions examples/verdig/temperature_monitor_native.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Stream the Verdi-G / HOPS main temperature over native pyftdi I2C (no DLL).

Read-only: it does not change the laser state or take remote control (so the
front panel stays usable). Works on macOS/Linux with the laser attached and
pyftdi + libusb available.

Run: python examples/verdig/temperature_monitor_native.py [count]
On macOS, put the directory that holds libusb-1.0 on the loader path, e.g.:
DYLD_LIBRARY_PATH=/opt/homebrew/lib python examples/verdig/temperature_monitor_native.py
"""
import datetime
import sys
import time

from hardwarelibrary.sources.hopsnative import HOPSNativeInterface


def main():
count = int(sys.argv[1]) if len(sys.argv) > 1 else None
interface = HOPSNativeInterface() # or HOPSNativeInterface(url="ftdi://...")
interface.open()
print("Verdi-G / HOPS main-temperature monitor (Ctrl-C to stop). "
"Native calibration valid ~32-40 C.")
read = 0
try:
while count is None or read < count:
celsius = interface.mainTemperature()
stamp = datetime.datetime.now().strftime("%H:%M:%S")
print("{0} TMAIN = {1:6.2f} C".format(stamp, celsius))
read += 1
if count is None or read < count:
time.sleep(1.0)
except KeyboardInterrupt:
print()
finally:
interface.close()


if __name__ == "__main__":
main()
Loading
Loading