Skip to content

Commit 55bc55d

Browse files
committed
Refactor to use DirectUSB programming and SCPI commands instead of Windows-only DLL
1 parent 72ba241 commit 55bc55d

1 file changed

Lines changed: 74 additions & 56 deletions

File tree

Lines changed: 74 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import os
22
from typing import TYPE_CHECKING
33

4+
import usb1
5+
from libusb1 import libusb_error
6+
from usb1 import USBContext, USBDevice, USBDeviceHandle, USBError
7+
48
# QCoDeS imports
59
from qcodes.instrument_drivers.Minicircuits.Base_SPDT import (
610
MiniCircuitsSPDTBase,
@@ -12,37 +16,68 @@
1216

1317
from qcodes.instrument import InstrumentBaseKWArgs
1418

15-
try:
16-
import clr # pyright: ignore[reportMissingTypeStubs,reportMissingImports]
17-
except ImportError:
18-
raise ImportError(
19-
"""Module clr not found. Please obtain it by
20-
installing QCoDeS with the
21-
minicircuits_usb_spdt extra, e.g. by running
22-
pip install qcodes[minicircuits_usb_spdt]"""
23-
)
19+
MINICIRCUITS_VENDOR_ID = 0x20CE
20+
RF_SWITCH_PRODUCT_ID = 0x0022
21+
22+
23+
def open_switch_with_sn(serial_number: str | None) -> USBDeviceHandle | None:
24+
usb_context = USBContext()
25+
if serial_number is not None:
26+
device_iterator = usb_context.getDeviceIterator(
27+
skip_on_error=True,
28+
)
29+
try:
30+
for device in device_iterator:
31+
if (
32+
device.getVendorID() == MINICIRCUITS_VENDOR_ID
33+
and device.getProductID() == RF_SWITCH_PRODUCT_ID
34+
):
35+
handle = device.open()
36+
if get_serial_number(handle) == serial_number:
37+
return handle
38+
device.close() # Unsure what missing arguments are needed or what they do
39+
finally:
40+
device_iterator.close()
41+
else: # If no SN is provided, we can use the built-in function to return the first Minicircuits switch
42+
return usb_context.openByVendorIDAndProductID(
43+
MINICIRCUITS_VENDOR_ID, RF_SWITCH_PRODUCT_ID
44+
)
45+
return None
46+
47+
48+
def get_serial_number(handle: USBDeviceHandle) -> str:
49+
handle.resetDevice()
50+
handle.claimInterface(0)
51+
cmd = [
52+
41,
53+
]
54+
cmd_array = bytearray([0] * 64)
55+
cmd_array[0 : len(cmd)] = cmd
56+
handle.interruptWrite(endpoint=1, data=cmd_array, timeout=50)
57+
response = handle.interruptRead(endpoint=1, length=64, timeout=1000)
58+
resp_length = response.index(bytearray([0]))
59+
trimmed_response = response[1:resp_length]
60+
return trimmed_response.decode("ascii")
2461

2562

2663
class MiniCircuitsUsbSPDTSwitchChannel(
2764
MiniCircuitsSPDTSwitchChannelBase["MiniCircuitsUsbSPDT"]
2865
):
2966
def _set_switch(self, switch: int) -> None:
30-
self.parent.switch.Set_Switch(self.channel_letter, switch - 1)
67+
self.parent._query_scpi(f"set{self.channel_letter}={switch - 1}")
3168

3269
def _get_switch(self) -> int:
33-
status = self.parent.switch.GetSwitchesStatus(self._parent.address)[1]
34-
return int(f"{status:04b}"[-1 - self.channel_number]) + 1
70+
all_ports_state = int(self.parent._query_scpi("SWPORT?"))
71+
bitmask = 2**self.channel_number
72+
return int((all_ports_state & bitmask) >= 1) + 1
3573

3674

3775
class MiniCircuitsUsbSPDT(MiniCircuitsSPDTBase):
3876
CHANNEL_CLASS = MiniCircuitsUsbSPDTSwitchChannel
39-
PATH_TO_DRIVER = r"mcl_RF_Switch_Controller64"
40-
PATH_TO_DRIVER_45 = r"mcl_RF_Switch_Controller_NET45"
4177

4278
def __init__(
4379
self,
4480
name: str,
45-
driver_path: str | None = None,
4681
serial_number: str | None = None,
4782
**kwargs: "Unpack[InstrumentBaseKWArgs]",
4883
):
@@ -51,59 +86,42 @@ def __init__(
5186
5287
Args:
5388
name: the name of the instrument
54-
driver_path: path to the dll
5589
serial_number: the serial number of the device
5690
(printed on the sticker on the back side, without s/n)
5791
kwargs: kwargs to be passed to Instrument class.
5892
5993
"""
60-
# import .net exception so we can catch it below
61-
# we keep this import local so that the module can be imported
62-
# without a working .net install
63-
clr.AddReference("System.IO")
64-
from System.IO import ( # pyright: ignore[reportMissingImports] # noqa: PLC0415
65-
FileNotFoundException,
66-
)
67-
6894
super().__init__(name, **kwargs)
69-
if os.name != "nt":
70-
raise ImportError("""This driver only works in Windows.""")
71-
try:
72-
if driver_path is None:
73-
try:
74-
clr.AddReference(self.PATH_TO_DRIVER)
75-
except FileNotFoundError:
76-
clr.AddReference(self.PATH_TO_DRIVER_45)
77-
else:
78-
clr.AddReference(driver_path)
79-
80-
except (ImportError, FileNotFoundException):
81-
raise ImportError(
82-
"""Load of mcl_RF_Switch_Controller64.dll or mcl_RF_Switch_Controller_NET45.dll
83-
not possible. Make sure the dll file is not blocked by Windows.
84-
To unblock right-click the dll to open properties and check the 'unblock' checkmark
85-
in the bottom. Check that your python installation is 64bit."""
86-
)
87-
try:
88-
import mcl_RF_Switch_Controller64 as mw_driver # pyright: ignore[reportMissingImports]# noqa: PLC0415
89-
except ImportError:
90-
import mcl_RF_Switch_Controller_NET45 as mw_driver # pyright: ignore[reportMissingImports]# noqa: PLC0415
91-
92-
self.switch = mw_driver.USB_RF_SwitchBox()
95+
self._handle: USBDeviceHandle | None = open_switch_with_sn(serial_number)
9396

94-
if not self.switch.Connect(serial_number):
95-
raise RuntimeError("Could not connect to device")
96-
self.address = self.switch.Get_Address()
97-
self.serial_number = self.switch.Read_SN("")[1]
98-
self.connect_message()
9997
self.add_channels()
98+
self.connect_message()
99+
100+
@property
101+
def handle(self) -> USBDeviceHandle:
102+
if self._handle is None:
103+
raise Exception # TODO Make better
104+
return self._handle
105+
106+
def _query_scpi(self, command: str) -> str:
107+
cmd_bytes = bytearray([42, 58]) # Interrupt code for Send SCPI Command
108+
cmd_bytes.extend(bytearray(command, "ascii"))
109+
cmd_bytes.extend(bytearray(64 - len(cmd_bytes)))
110+
111+
self.handle.interruptWrite(endpoint=1, data=cmd_bytes, timeout=50)
112+
113+
response = self.handle.interruptRead(endpoint=1, length=64, timeout=1000)
114+
resp_length = response.index(bytearray([0]))
115+
trimmed_response = response[1:resp_length]
116+
117+
return trimmed_response.decode("ascii")
100118

101119
def get_idn(self) -> dict[str, str | None]:
102120
# the arguments in those functions is the serial number or none if
103121
# there is only one switch.
104-
fw = self.switch.GetFirmware()
105-
MN = self.switch.Read_ModelName("")[1]
106-
SN = self.switch.Read_SN("")[1]
122+
fw = self._query_scpi("FIRMWARE?")
123+
MN = self._query_scpi("MN?")
124+
SN = self._query_scpi("SN?")
107125

108126
id_dict = {"firmware": fw, "model": MN, "serial": SN, "vendor": "Mini-Circuits"}
109127
return id_dict

0 commit comments

Comments
 (0)