Skip to content

Commit dcbb607

Browse files
authored
Merge pull request #77 from DCC-Lab/labjack-rewrite
Rewrite LabjackDevice: by-serial open, U3-HV detection, debug device
2 parents 2f38e70 + f69068e commit dcbb607

5 files changed

Lines changed: 177 additions & 18 deletions

File tree

MANIFEST.in

Lines changed: 0 additions & 5 deletions
This file was deleted.

hardwarelibrary/daq/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#__all__ = ["sutterdevice"]
22

33
from .daqdevice import AnalogIOProtocol, DigitalIOProtocol
4-
from .labjackdevice import LabjackDevice
4+
from .labjackdevice import LabjackDevice, DebugLabjackDevice

hardwarelibrary/daq/labjackdevice.py

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,129 @@
22
from hardwarelibrary.daq import AnalogIOProtocol, DigitalIOProtocol
33
import u3
44

5+
56
class LabjackDevice(PhysicalDevice, AnalogIOProtocol, DigitalIOProtocol):
7+
"""LabJack U3 (LV and HV). Use self.dev for features beyond the wrapped methods."""
8+
69
classIdVendor = 0x0cd5
710
classIdProduct = 0x003
11+
812
def __init__(self, serialNumber="*", idProduct=0x003, idVendor=0x0cd5):
913
super().__init__(serialNumber, idProduct=idProduct, idVendor=idVendor)
1014
self.dev = None
1115

1216
def doInitializeDevice(self):
1317
self.dev = u3.U3(autoOpen=False)
14-
self.dev.open()
18+
if self.serialNumber == "*":
19+
self.dev.open()
20+
else:
21+
self.dev.open(firstFound=False, serial=int(self.serialNumber))
1522
self.dev.configU3()
23+
self.dev.getCalibrationData()
1624

1725
def doShutdownDevice(self):
1826
self.dev.close()
1927

20-
def setConfiguration(self, parameters:dict):
21-
raise NotImplementedError("You must use default parameters or call the U3's labjack.dev functions directly")
28+
@property
29+
def isHighVoltage(self):
30+
return hasattr(self.dev, 'hardwareVersion') and self.dev.hardwareVersion >= 2.0
31+
32+
def setConfiguration(self, parameters: dict):
33+
raise NotImplementedError(
34+
"You must use default parameters or call the U3's labjack.dev functions directly"
35+
)
2236

2337
def configuration(self):
2438
return self.dev.configU3()
2539

40+
def configureAnalogIO(self, parameters: dict):
41+
self.dev.configIO(**parameters)
42+
43+
def configureDigitalIO(self, parameters: dict):
44+
self.dev.configIO(**parameters)
45+
2646
def getAnalogVoltage(self, channel):
27-
value = self.dev.getAIN(channel)
28-
return value
47+
return self.dev.getAIN(channel)
2948

3049
def setAnalogVoltage(self, value, channel):
31-
# Writes the dac.
3250
if channel == 0:
3351
register = 5000
3452
elif channel == 1:
3553
register = 5002
54+
else:
55+
raise ValueError(f"DAC channel must be 0 or 1, got {channel}")
3656
self.dev.writeRegister(register, value)
3757

3858
def setDigitalValue(self, value, channel):
3959
self.dev.setDOState(channel, value)
4060

4161
def getDigitalValue(self, channel):
4262
return self.dev.getDIState(channel) != 0
63+
64+
def getTemperature(self):
65+
"""Returns Kelvin."""
66+
return self.dev.getTemperature()
67+
68+
def toggleLED(self):
69+
self.dev.toggleLED()
70+
71+
72+
class DebugLabjackDevice(LabjackDevice):
73+
"""Hardware-free U3 for tests. DAC channels 0,1 loop back to AIN 0,1."""
74+
75+
classIdProduct = 0xFFFB
76+
classIdVendor = 0xFFFF
77+
78+
def __init__(self, serialNumber='debug'):
79+
PhysicalDevice.__init__(
80+
self, serialNumber=serialNumber,
81+
idProduct=self.classIdProduct, idVendor=self.classIdVendor
82+
)
83+
self.dev = None
84+
self._analogValues = {}
85+
self._digitalValues = {}
86+
self._temperature = 298.0
87+
88+
def doInitializeDevice(self):
89+
pass
90+
91+
def doShutdownDevice(self):
92+
pass
93+
94+
@property
95+
def isHighVoltage(self):
96+
return False
97+
98+
def setConfiguration(self, parameters: dict):
99+
raise NotImplementedError(
100+
"You must use default parameters or call the U3's labjack.dev functions directly"
101+
)
102+
103+
def configuration(self):
104+
return {'DeviceName': 'DebugU3', 'SerialNumber': 0}
105+
106+
def configureAnalogIO(self, parameters: dict):
107+
pass
108+
109+
def configureDigitalIO(self, parameters: dict):
110+
pass
111+
112+
def getAnalogVoltage(self, channel):
113+
return self._analogValues.get(channel, 0.0)
114+
115+
def setAnalogVoltage(self, value, channel):
116+
if channel not in (0, 1):
117+
raise ValueError(f"DAC channel must be 0 or 1, got {channel}")
118+
self._analogValues[channel] = value
119+
120+
def setDigitalValue(self, value, channel):
121+
self._digitalValues[channel] = bool(value)
122+
123+
def getDigitalValue(self, channel):
124+
return self._digitalValues.get(channel, False)
125+
126+
def getTemperature(self):
127+
return self._temperature
128+
129+
def toggleLED(self):
130+
pass

hardwarelibrary/tests/testLabjackU3.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from hardwarelibrary.devicemanager import *
55
from hardwarelibrary.notificationcenter import NotificationCenter
66
from hardwarelibrary.physicaldevice import PhysicalDevice, DeviceState, PhysicalDeviceNotification
7-
from hardwarelibrary.daq import LabjackDevice
7+
from hardwarelibrary.daq import LabjackDevice, DebugLabjackDevice
88
from enum import Enum
99
from typing import Union, Optional, Protocol
1010

@@ -107,5 +107,76 @@ def testSetConfiguration(self):
107107
with self.assertRaises(NotImplementedError):
108108
self.device.setConfiguration(None)
109109

110+
class TestDebugLabjackDevice(unittest.TestCase):
111+
def setUp(self):
112+
self.device = DebugLabjackDevice()
113+
self.device.initializeDevice()
114+
115+
def tearDown(self):
116+
self.device.shutdownDevice()
117+
118+
def testCreate(self):
119+
self.assertIsNotNone(self.device)
120+
self.assertIsNone(self.device.dev)
121+
122+
def testInitializeAndShutdown(self):
123+
device = DebugLabjackDevice()
124+
device.initializeDevice()
125+
device.shutdownDevice()
126+
127+
def testIsHighVoltage(self):
128+
self.assertFalse(self.device.isHighVoltage)
129+
130+
def testConfiguration(self):
131+
config = self.device.configuration()
132+
self.assertEqual(config['DeviceName'], 'DebugU3')
133+
134+
def testSetConfiguration(self):
135+
with self.assertRaises(NotImplementedError):
136+
self.device.setConfiguration({})
137+
138+
def testGetAnalogVoltageDefault(self):
139+
value = self.device.getAnalogVoltage(channel=0)
140+
self.assertEqual(value, 0.0)
141+
142+
def testSetAndGetAnalogVoltage(self):
143+
self.device.setAnalogVoltage(value=2.5, channel=0)
144+
self.assertAlmostEqual(self.device.getAnalogVoltage(channel=0), 2.5)
145+
146+
def testSetAnalogVoltageChannel1(self):
147+
self.device.setAnalogVoltage(value=1.1, channel=1)
148+
self.assertAlmostEqual(self.device.getAnalogVoltage(channel=1), 1.1)
149+
150+
def testSetAnalogVoltageInvalidChannel(self):
151+
with self.assertRaises(ValueError):
152+
self.device.setAnalogVoltage(value=1.0, channel=2)
153+
154+
def testSetAndGetDigitalValue(self):
155+
self.device.setDigitalValue(value=True, channel=4)
156+
self.assertTrue(self.device.getDigitalValue(channel=4))
157+
158+
def testGetDigitalValueDefault(self):
159+
self.assertFalse(self.device.getDigitalValue(channel=4))
160+
161+
def testToggleDigitalValue(self):
162+
channel = 6
163+
self.device.setDigitalValue(value=True, channel=channel)
164+
self.assertTrue(self.device.getDigitalValue(channel=channel))
165+
self.device.setDigitalValue(value=False, channel=channel)
166+
self.assertFalse(self.device.getDigitalValue(channel=channel))
167+
168+
def testGetTemperature(self):
169+
self.assertAlmostEqual(self.device.getTemperature(), 298.0)
170+
171+
def testToggleLED(self):
172+
self.device.toggleLED()
173+
174+
def testConfigureAnalogIO(self):
175+
self.device.configureAnalogIO({'FIOAnalog': 0xFF})
176+
177+
def testConfigureDigitalIO(self):
178+
self.device.configureDigitalIO({'FIOAnalog': 0x00})
179+
180+
110181
if __name__ == '__main__':
111182
unittest.main()

pyproject.toml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[build-system]
2-
requires = ["setuptools<69", "wheel", "setuptools_scm"]
2+
requires = ["setuptools", "wheel", "setuptools_scm"]
33
build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "hardwarelibrary"
7-
dynamic = ["version"] # This line tells setuptools to use setuptools_scm for version
7+
dynamic = ["version"]
88
description = "Cross-platform (macOS, Windows, Linux, etc...) library to control various hardware devices mostly for scientific applications."
99
readme = "README.md"
10-
requires-python = ">=3.7"
10+
requires-python = ">=3.9"
1111
license = { text = "MIT" }
1212
authors = [
1313
{ name = "Daniel Cote", email = "dccote@cervo.ulaval.ca" },
@@ -24,7 +24,12 @@ classifiers = [
2424
"License :: OSI Approved :: MIT License",
2525
"Programming Language :: Python",
2626
"Programming Language :: Python :: 3",
27-
"Programming Language :: Python :: 3.7",
27+
"Programming Language :: Python :: 3.9",
28+
"Programming Language :: Python :: 3.10",
29+
"Programming Language :: Python :: 3.11",
30+
"Programming Language :: Python :: 3.12",
31+
"Programming Language :: Python :: 3.13",
32+
"Programming Language :: Python :: 3.14",
2833
"Operating System :: OS Independent"
2934
]
3035
dependencies = [
@@ -55,4 +60,4 @@ local_scheme = "no-local-version"
5560
[tool.pytest.ini_options]
5661
testpaths = ["hardwarelibrary/tests"]
5762
python_files = ["test*.py"]
58-
norecursedirs = ["deprecated"]
63+
norecursedirs = ["deprecated"]

0 commit comments

Comments
 (0)