diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 1fd78cc..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,409 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1619125880800 - - - - - - - - - - - - - - - file://$PROJECT_DIR$/hardwarelibrary/communication/communicationport.py - 55 - - - file://$PROJECT_DIR$/hardwarelibrary/spectrometers/oceaninsight.py - 679 - - - file://$PROJECT_DIR$/hardwarelibrary/__main__.py - 15 - - - file://$PROJECT_DIR$/hardwarelibrary/powermeters/powermeterdevice.py - 37 - - - file://$PROJECT_DIR$/hardwarelibrary/devicemanager.py - 314 - - - file://$PROJECT_DIR$/hardwarelibrary/tests/testPhysicalDevice.py - 235 - - - file://$PROJECT_DIR$/hardwarelibrary/echodevice.py - 25 - - - file://$PROJECT_DIR$/hardwarelibrary/echodevice.py - 24 - - - file://$PROJECT_DIR$/hardwarelibrary/communication/serialport.py - 116 - - - file://$PROJECT_DIR$/hardwarelibrary/cameras/camera.py - 105 - - - file://$PROJECT_DIR$/hardwarelibrary/tests/testPhysicalDevice.py - 46 - - - file://$PROJECT_DIR$/hardwarelibrary/tests/testIntellidrive.py - 29 - - - file://$PROJECT_DIR$/hardwarelibrary/tests/testIntellidrive.py - 66 - - - file://$PROJECT_DIR$/hardwarelibrary/communication/serialport.py - 193 - - - file://$PROJECT_DIR$/hardwarelibrary/motion/intellidrivedevice.py - 85 - - - file://$PROJECT_DIR$/hardwarelibrary/motion/intellidrivedevice.py - 115 - - - file://$PROJECT_DIR$/hardwarelibrary/spectrometers/base.py - 262 - - - file://$PROJECT_DIR$/hardwarelibrary/devicemanager.py - 215 - - - file://$PROJECT_DIR$/hardwarelibrary/tests/testPhysicalDevice.py - 298 - - - file://$PROJECT_DIR$/hardwarelibrary/tests/testPhysicalDevice.py - 293 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/hardwarelibrary/communication/communicationport.py b/hardwarelibrary/communication/communicationport.py index 2952c88..4fd2a57 100644 --- a/hardwarelibrary/communication/communicationport.py +++ b/hardwarelibrary/communication/communicationport.py @@ -73,14 +73,12 @@ def readString(self, endPoint=None) -> str: except CommunicationReadTimeout as err: raise CommunicationReadTimeout("Only obtained {0}".format(data)) - string = data.decode(encoding='utf-8') - - return string + return data.decode('utf-8', 'replace') def writeString(self, string, endPoint=None) -> int: nBytes = 0 with self.portLock: - data = bytearray(string, "utf-8") + data = bytearray(string, 'utf-8', 'replace') nBytes = self.writeData(data, endPoint) return nBytes diff --git a/hardwarelibrary/communication/debugport.py b/hardwarelibrary/communication/debugport.py index b7ebe45..a22ab0b 100644 --- a/hardwarelibrary/communication/debugport.py +++ b/hardwarelibrary/communication/debugport.py @@ -56,18 +56,19 @@ def bytesAvailable(self, endPoint=0): return len(self.outputBuffers[endPoint]) def flush(self): - self.buffers = [bytearray(),bytearray()] + self.inputBuffers = [bytearray()] + self.outputBuffers = [bytearray()] def readData(self, length, endPoint=None): if endPoint is None: - endPointIndex = 0 + endPoint = 0 with self.portLock: time.sleep(self.delay*random.random()) data = bytearray() for i in range(0, length): - if len(self.outputBuffers[endPointIndex]) > 0: - byte = self.outputBuffers[endPointIndex].pop(0) + if len(self.outputBuffers[endPoint]) > 0: + byte = self.outputBuffers[endPoint].pop(0) data.append(byte) else: raise CommunicationReadTimeout("Unable to read data") @@ -76,12 +77,12 @@ def readData(self, length, endPoint=None): def writeData(self, data, endPoint=None): if endPoint is None: - endPointIndex = 0 + endPoint = 0 with self.portLock: - self.inputBuffers[endPointIndex].extend(data) + self.inputBuffers[endPoint].extend(data) - self.processInputBuffers(endPointIndex=endPointIndex) + self.processInputBuffers(endPoint) return len(data) diff --git a/hardwarelibrary/communication/serialport.py b/hardwarelibrary/communication/serialport.py index dc3a7c7..151403c 100644 --- a/hardwarelibrary/communication/serialport.py +++ b/hardwarelibrary/communication/serialport.py @@ -62,7 +62,7 @@ def matchAnyPort(cls, idVendor=None, idProduct=None, serialNumber=None): return None @classmethod - def matchPorts(cls, idVendor=None, idProduct=None, serialNumber=None): + def matchPortObjects(cls, idVendor=None, idProduct=None, serialNumber=None): # We must provide idVendor, idProduct and serialNumber # or idVendor and idProduct # or idVendor @@ -81,7 +81,7 @@ def matchPorts(cls, idVendor=None, idProduct=None, serialNumber=None): pass - # It sometimes happens on macOS that the ports are "doubled" because two user-space DriverExtension + # It sometimes happens on macOS that the ports are "doubled" because two user-space DriverExtension # prepare a port (FTDI and Apple's for instance). If that is the case, then we try to remove duplicates portObjects = [] @@ -102,14 +102,18 @@ def matchPorts(cls, idVendor=None, idProduct=None, serialNumber=None): if re.search(serialNumber, port.serial_number, re.IGNORECASE): portObjects.append(port) + return portObjects + @classmethod + def matchPorts(cls, idVendor=None, idProduct=None, serialNumber=None): + portObjects = cls.matchPortObjects(idVendor, idProduct, serialNumber) ports = [] # portsAlreadyAdded = [] for port in portObjects: # uniqueIdentifier = (port.vid, port.pid, port.serial_number) - # if not (uniqueIdentifier in portsAlreadyAdded): + # if not (uniqueIdentifier in portsAlreadyAdded): ports.append(port.device) - # portsAlreadyAdded.append(uniqueIdentifier) + # portsAlreadyAdded.append(uniqueIdentifier) return ports @@ -191,7 +195,7 @@ def readString(self, endPoint=0): with self.portLock: data = self.port.read_until(expected=self.terminator) - return data.decode() + return data.decode('utf-8', 'replace') def readData(self, length, endPoint=0) -> bytearray: with self.portLock: diff --git a/hardwarelibrary/communication/usbport.py b/hardwarelibrary/communication/usbport.py index 8f6f357..52f34b7 100644 --- a/hardwarelibrary/communication/usbport.py +++ b/hardwarelibrary/communication/usbport.py @@ -163,6 +163,6 @@ def readString(self, endPoint=None) -> str: try: data += self.readData(length=1, endPoint=endPoint) if data[-1] == 10: # How to write '\n' ? - return data.decode(encoding='utf-8') + return data.decode('utf-8', 'replace') except Exception as err: raise IOError("Unable to read string terminator: {0}".format(err)) diff --git a/hardwarelibrary/irises/__init__.py b/hardwarelibrary/irises/__init__.py new file mode 100644 index 0000000..c39ad64 --- /dev/null +++ b/hardwarelibrary/irises/__init__.py @@ -0,0 +1,3 @@ + +from .irisdevice import IrisDevice +from .uniblitzai25device import UniblitzAI25Device diff --git a/hardwarelibrary/irises/driverAI25/README.md b/hardwarelibrary/irises/driverAI25/README.md new file mode 100644 index 0000000..4ba2fdf --- /dev/null +++ b/hardwarelibrary/irises/driverAI25/README.md @@ -0,0 +1,10 @@ +# driverAI25 + +Arduino Uno driver to control the Uniblitz AI25 Auto Iris. Requires a 12V 2A power supply (2.1 x 5.5 mm barrel jack). + +You need to install `NeoSWSerial` in the Arduino IDE Library Manager. + +Reference manual: https://www.uniblitz.com/wp-content/uploads/2021/03/ai25-direct-control-v1-2.pdf + +Pinout: +![driverAI25 pinout](driverAI25_pinout.jpg) diff --git a/hardwarelibrary/irises/driverAI25/driverAI25.ino b/hardwarelibrary/irises/driverAI25/driverAI25.ino new file mode 100644 index 0000000..e86b335 --- /dev/null +++ b/hardwarelibrary/irises/driverAI25/driverAI25.ino @@ -0,0 +1,44 @@ +/* + * Uniblitz AI25 Auto Iris driver v1.1 + * for Arduino Uno/Nano/Micro + * + * Pin 2 -> AI25 Interrupt (Micro USB3 pin 9) + * Pin 3 -> AI25 RX (Micro USB3 pin 7) + * Pin 4 -> AI25 TX (Micro USB3 pin 6) + */ + +#include + +const byte interruptPin = 2; +NeoSWSerial irisSerial(4, 3); // Driver RX/TX -> AI25 TX/RX +String command; // input buffer + +void setup() +{ + Serial.begin(9600); + irisSerial.begin(9600); + pinMode(2, OUTPUT); + while (!Serial); // Wait for serial port to connect; needed for native USB only + Serial.println("driverAI25 Ready"); +} + +void loop() +{ + if (Serial.available()) + { + command = Serial.readStringUntil('\n'); + + if (command == "V") + { + Serial.println("driverAI25 v1.1"); + return; + } + + digitalWrite(interruptPin, HIGH); + delay(2); + digitalWrite(interruptPin, LOW); + + irisSerial.println(command); // send command + Serial.println(irisSerial.readStringUntil('\n')); // return reply + } +} diff --git a/hardwarelibrary/irises/driverAI25/driverAI25_pinout.jpg b/hardwarelibrary/irises/driverAI25/driverAI25_pinout.jpg new file mode 100644 index 0000000..dbbe6f6 Binary files /dev/null and b/hardwarelibrary/irises/driverAI25/driverAI25_pinout.jpg differ diff --git a/hardwarelibrary/irises/irisdevice.py b/hardwarelibrary/irises/irisdevice.py new file mode 100644 index 0000000..7dac7dd --- /dev/null +++ b/hardwarelibrary/irises/irisdevice.py @@ -0,0 +1,118 @@ +# +# irisdevice.py +# + +from enum import Enum +from hardwarelibrary.physicaldevice import * +from hardwarelibrary.notificationcenter import NotificationCenter, Notification + + +class IrisNotification(Enum): + willMove = "willMove" + didMove = "didMove" + didGetPosition = "didGetPosition" + + +class IrisDevice(PhysicalDevice): + def __init__(self, serialNumber: str, idProduct: int, idVendor: int): + super().__init__(serialNumber, idProduct, idVendor) + self.minStep = 0 # closed + self.maxStep = 2**31 # open + self.micronsPerStep = 1000 + self.minAperture = -1 # microns when closed + + def convertMicronsToStep(self, microns: float) -> int: + """ Converts an aperture size in microns to a native step position. """ + if self.micronsPerStep == 0: + raise ValueError("micronsPerStep == 0") + if self.minAperture < 0: + raise ValueError("minAperture < 0") + + return round((microns - self.minAperture) / self.micronsPerStep + self.minStep) + + def convertStepToMicrons(self, step: int) -> float: + """ Converts a native step position to an aperture size in microns. """ + if self.minAperture < 0: + raise ValueError("minAperture < 0") + + return (step - self.minStep) * self.micronsPerStep + self.minAperture + + def currentStep(self) -> int: + """ Returns the current native position. """ + step = self.doGetCurrentStep() + NotificationCenter().postNotification(IrisNotification.didGetPosition, notifyingObject=self, userInfo=step) + return step + + def aperture(self) -> float: + """ Returns the current aperture size in microns. """ + return self.convertStepToMicrons(self.currentStep()) + + def home(self): + NotificationCenter().postNotification(IrisNotification.willMove, notifyingObject=self) + self.doHome() + NotificationCenter().postNotification(IrisNotification.didMove, notifyingObject=self) + + def moveTo(self, step: int): + NotificationCenter().postNotification(IrisNotification.willMove, notifyingObject=self, userInfo=step) + self.doMoveTo(step) + NotificationCenter().postNotification(IrisNotification.didMove, notifyingObject=self, userInfo=step) + + def moveToMicrons(self, microns): + self.moveTo(self.convertMicronsToStep(microns)) + + def moveBy(self, steps: int): + NotificationCenter().postNotification(IrisNotification.willMove, notifyingObject=self, userInfo=steps) + self.doMoveBy(steps) + NotificationCenter().postNotification(IrisNotification.didMove, notifyingObject=self, userInfo=steps) + + def moveByMicrons(self, microns): + self.moveBy(round(microns / self.micronsPerStep)) + + def isValidStep(self, step: int) -> bool: + return min(self.minStep, self.maxStep) < step < max(self.minStep, self.maxStep) + + def isValidAperture(self, microns: float) -> bool: + return self.isValidStep(self.convertMicronsToStep(microns)) + + def doGetCurrentStep(self) -> int: + raise NotImplementedError + + def doHome(self): + raise NotImplementedError + + def doMoveTo(self, step): + raise NotImplementedError + + def doMoveBy(self, steps): + raise NotImplementedError + + +class DebugIrisDevice(IrisDevice): + classIdProduct = 0xFFFD + classIdVendor = debugClassIdVendor + + def __init__(self): + super().__init__("debug", DebugIrisDevice.classIdProduct, DebugIrisDevice.classIdVendor) + self.minStep = 0 # iris closed + self.maxStep = 100 # iris open + self.micronsPerStep = 100 + self.minAperture = 0 # microns when closed + self.lastSetStep = 0 + + def doHome(self): + self.lastSetStep = 0 + + def doGetCurrentStep(self): + return self.lastSetStep + + def doMoveTo(self, step): + self.lastSetStep = step + + def doMoveBy(self, steps): + self.lastSetStep += steps + + def doInitializeDevice(self): + pass + + def doShutdownDevice(self): + pass diff --git a/hardwarelibrary/irises/uniblitzai25device.py b/hardwarelibrary/irises/uniblitzai25device.py new file mode 100644 index 0000000..7fee632 --- /dev/null +++ b/hardwarelibrary/irises/uniblitzai25device.py @@ -0,0 +1,138 @@ +# +# uniblitzai25device.py +# +# Allows controlling the Uniblitz AI25 Auto Iris using a custom driver. +# See "driverAI25" in the "irises" folder and "ai25-direct-control-v1-2.pdf" in the "manuals" folder. +# + +from serial.tools.list_ports_common import ListPortInfo +from hardwarelibrary.irises.irisdevice import * +from hardwarelibrary.communication.communicationport import CommunicationPort +from hardwarelibrary.communication.serialport import SerialPort +from hardwarelibrary.communication.debugport import DebugPort + + +class UniblitzAI25Device(IrisDevice): + classIdVendor = 0x2341 # Arduino + + def __init__(self, serialNumber: str = None): + super().__init__(serialNumber=serialNumber, idVendor=self.classIdVendor, idProduct=self.classIdProduct) + + self.minStep = 54 # iris closed + self.maxStep = 0 # iris open + self.micronsPerStep = -440 + self.minAperture = 1500 # microns when closed + # self.incrementMicrons = 440 + + self.port: CommunicationPort = None + self.isHomed = False + self.lastSetStep = 0 + + def __del__(self): + self.doShutdownDevice() + + def doInitializeDevice(self): + try: + if 'debug' in self.serialNumber: + self.port = self.DebugSerialPort() + self.port.open() + else: + try: + portObject: ListPortInfo = SerialPort.matchPortObjects(self.idVendor, self.idProduct, self.serialNumber)[0] + except: + raise Exception("No Uniblitz Iris connected") + + self.idVendor = portObject.vid + self.idProduct = portObject.pid + self.serialNumber = portObject.serial_number + + portPath = portObject.device + self.port = SerialPort(portPath=portPath) + + if self.port is None: + raise Exception("Cannot allocate port " + portPath) + + self.port.open(baudRate=9600, timeout=3) + + self.port.readMatchingGroups('^(driverAI25 )?Ready\r') + self.isHomed = False + # self.doMoveBy(-1) + # self.doMoveBy(1) + + except Exception as err: + self.doShutdownDevice() + raise PhysicalDevice.UnableToInitialize(err) + + def doShutdownDevice(self): + if self.port and self.port.isOpen: + self.port.close() + self.port = None + + def sendCommand(self, command: str): + """ Writes a command to the AI25 driver. It will initialize the device if needed. On failure, it will warn and shutdown. """ + if self.port is None or not self.port.isOpen: + self.initializeDevice() + + self.port.flush() + self.port.writeStringExpectMatchingString(command + '\r\n', replyPattern='^!\r') + + def doGetCurrentStep(self): + return self.lastSetStep + + def doHome(self): + self.sendCommand('H') + self.lastSetStep = 0 + self.isHomed = True + + def doMoveTo(self, step: int): + self.sendCommand('{}{:02d}'.format('M' if self.isHomed else 'G', step)) + self.lastSetStep = step + self.isHomed = True + + def doMoveBy(self, steps: int): + increment = -1 if steps < 0 else 1 + command = 'I' if steps < 0 else 'D' # decrease step = [I]ncrease aperture + for _ in range(abs(steps)): + self.sendCommand(command) + self.lastSetStep += increment + + class DebugSerialPort(DebugPort): + def __init__(self): + super().__init__() + self.minStep = 54 + self.maxStep = 0 + self.currentStep = 0 + + def open(self): + super().open() + self.writeToOutputBuffer(bytearray('driverAI25 Ready\r\n', 'utf-8'), 0) + + def processInputBuffers(self, endPointIndex): + command = self.inputBuffers[endPointIndex].decode('utf-8') + reply = '!' + + # Home + if re.match('^H\r', command): + self.currentStep = 0 + + # Increment + elif re.match('^I\r', command): + self.currentStep = min(self.currentStep - 1, self.maxStep) + + # Decrement + elif re.match('^D\r', command): + self.currentStep = max(self.currentStep + 1, self.minStep) + + # Move + elif match := re.match('^[MG](\\d\\d)\r', command): + target = int(match.group(1)) + if min(self.minStep, self.maxStep) <= target <= max(self.minStep, self.maxStep): + self.currentStep = target + time.sleep(0.5) + else: + reply = '?' + else: + reply = '?' + + self.writeToOutputBuffer(bytearray(reply + '\r\n', 'utf-8'), endPointIndex) + self.inputBuffers[endPointIndex] = bytearray() diff --git a/hardwarelibrary/manuals/ai25-direct-control-v1-2.pdf b/hardwarelibrary/manuals/ai25-direct-control-v1-2.pdf new file mode 100644 index 0000000..f9a58a4 Binary files /dev/null and b/hardwarelibrary/manuals/ai25-direct-control-v1-2.pdf differ diff --git a/hardwarelibrary/motion/sutterdevice.py b/hardwarelibrary/motion/sutterdevice.py index 5625bf1..f9b4320 100644 --- a/hardwarelibrary/motion/sutterdevice.py +++ b/hardwarelibrary/motion/sutterdevice.py @@ -49,7 +49,7 @@ def doInitializeDevice(self): self.port.open(baudRate=128000, timeout=10) if self.port is None: - raise PhysicalDevice.UnableToInitialize("Cannot allocate port {0}".format(self.portPath)) + raise PhysicalDevice.UnableToInitialize("Cannot allocate port {0}".format(portPath)) self.positionInMicrosteps() diff --git a/hardwarelibrary/physicaldevice.py b/hardwarelibrary/physicaldevice.py index 4fec4c1..3e612f8 100644 --- a/hardwarelibrary/physicaldevice.py +++ b/hardwarelibrary/physicaldevice.py @@ -69,7 +69,7 @@ def vidpids(cls): @classmethod def isCompatibleWith(cls, serialNumber, idProduct, idVendor): for compatibleIdVendor, compatibleIdProduct in cls.vidpids(): - if idVendor == compatibleIdVendor and idProduct == compatibleIdProduct: + if idVendor == compatibleIdVendor and (idProduct == compatibleIdProduct or compatibleIdProduct is None): return True return False diff --git a/hardwarelibrary/sources/cobolt.py b/hardwarelibrary/sources/cobolt.py index fa87712..22c15c9 100644 --- a/hardwarelibrary/sources/cobolt.py +++ b/hardwarelibrary/sources/cobolt.py @@ -177,7 +177,7 @@ def read(self, length) -> bytearray: def write(self, data:bytearray) -> int : with globalLock: - string = data.decode('utf-8') + string = data.decode('utf-8', 'replace') match = re.search("pa\\?", string) if match is not None: diff --git a/hardwarelibrary/tests/testIrisDevice.py b/hardwarelibrary/tests/testIrisDevice.py new file mode 100644 index 0000000..7da49f7 --- /dev/null +++ b/hardwarelibrary/tests/testIrisDevice.py @@ -0,0 +1,139 @@ +import env +import unittest + +from hardwarelibrary.irises.irisdevice import * +from hardwarelibrary.irises.uniblitzai25device import UniblitzAI25Device +from hardwarelibrary.notificationcenter import NotificationCenter + + +class BaseTestCases: + class TestIrisDevice(unittest.TestCase): + device: IrisDevice + + def setUp(self): + # Set self.device in subclass + self.willNotificationReceived = False + self.didNotificationReceived = False + if self.device is None: + raise (unittest.SkipTest("No device defined in subclass of BaseTestCase")) + + try: + self.device.initializeDevice() + except: + raise unittest.SkipTest("No devices connected") + + def tearDown(self): + self.device.shutdownDevice() + + def testCurrentStep(self): + step = self.device.currentStep() + self.assertIsNotNone(step) + self.assertTrue(step >= 0) + + def testAperture(self): + aperture = self.device.aperture() + self.assertIsNotNone(aperture) + self.assertTrue(aperture >= 0) + + def testMove(self): + target = self.device.minStep + (self.device.maxStep - self.device.minStep) // 3 + self.device.moveTo(target) + self.assertEqual(self.device.currentStep(), target) + + def testMoveBy(self): + step = self.device.currentStep() + target = 2 + self.device.moveBy(target) + self.assertEqual(self.device.currentStep(), step + target) + + def testMoveToMicrons(self): + target = self.device.minAperture + self.device.micronsPerStep * ((self.device.maxStep - self.device.minStep) // 2) + self.device.moveToMicrons(target) + self.assertEqual(self.device.aperture(), target) + + def testMoveByMicrons(self): + aperture = self.device.aperture() + target = aperture + self.device.micronsPerStep * 2 + self.device.moveToMicrons(target) + self.assertEqual(self.device.aperture(), target) + + def testDeviceHome(self): + self.device.home() + self.assertEqual(self.device.currentStep(), 0) + + def handleWill(self, notification): + self.willNotificationReceived = True + + def handleDid(self, notification): + self.didNotificationReceived = True + + def testPositionNotifications(self): + NotificationCenter().addObserver(self, method=self.handleDid, notificationName=IrisNotification.didGetPosition) + self.device.currentStep() + self.assertTrue(self.didNotificationReceived) + NotificationCenter().removeObserver(self) + + def testDeviceMoveNotifications(self): + NotificationCenter().addObserver(self, method=self.handleWill, notificationName=IrisNotification.willMove) + NotificationCenter().addObserver(self, method=self.handleDid, notificationName=IrisNotification.didMove) + + self.assertFalse(self.willNotificationReceived) + self.assertFalse(self.didNotificationReceived) + + self.device.moveTo(self.device.minStep + (self.device.maxStep - self.device.minStep) // 2) + + self.assertTrue(self.willNotificationReceived) + self.assertTrue(self.didNotificationReceived) + + NotificationCenter().removeObserver(self) + + def testDeviceMoveByNotifications(self): + NotificationCenter().addObserver(self, method=self.handleWill, notificationName=IrisNotification.willMove) + NotificationCenter().addObserver(self, method=self.handleDid, notificationName=IrisNotification.didMove) + + self.assertFalse(self.willNotificationReceived) + self.assertFalse(self.didNotificationReceived) + + self.device.moveBy(2) + + self.assertTrue(self.willNotificationReceived) + self.assertTrue(self.didNotificationReceived) + + NotificationCenter().removeObserver(self) + + def testDeviceHomeNotifications(self): + + NotificationCenter().addObserver(self, method=self.handleWill, notificationName=IrisNotification.willMove) + NotificationCenter().addObserver(self, method=self.handleDid, notificationName=IrisNotification.didMove) + + self.assertFalse(self.willNotificationReceived) + self.assertFalse(self.didNotificationReceived) + + self.device.home() + + self.assertTrue(self.willNotificationReceived) + self.assertTrue(self.didNotificationReceived) + + NotificationCenter().removeObserver(self) + + +class TestDebugLinearMotionDeviceBase(BaseTestCases.TestIrisDevice): + def setUp(self): + self.device = DebugIrisDevice() + super().setUp() + + +class TestDebugUniblitzAI25DeviceBase(BaseTestCases.TestIrisDevice): + def setUp(self): + self.device = UniblitzAI25Device("debug") + super().setUp() + + +class TestRealUniblitzAI25DeviceBase(BaseTestCases.TestIrisDevice): + def setUp(self): + self.device = UniblitzAI25Device() + super().setUp() + + +if __name__ == '__main__': + unittest.main() diff --git a/hardwarelibrary/tests/testLinearMotionDevice.py b/hardwarelibrary/tests/testLinearMotionDevice.py index 7bdb9e9..45903c2 100644 --- a/hardwarelibrary/tests/testLinearMotionDevice.py +++ b/hardwarelibrary/tests/testLinearMotionDevice.py @@ -1,13 +1,15 @@ import env import unittest -from hardwarelibrary.motion.linearmotiondevice import DebugLinearMotionDevice, LinearMotionNotification +from hardwarelibrary.motion.linearmotiondevice import * from hardwarelibrary.motion.sutterdevice import SutterDevice from hardwarelibrary.notificationcenter import NotificationCenter class BaseTestCases: class TestLinearMotionDevice(unittest.TestCase): + device: LinearMotionDevice + def setUp(self): # Set self.device in subclass self.willNotificationReceived = False @@ -96,16 +98,6 @@ def testDeviceHome(self): self.assertEqual(y, 0) self.assertEqual(z, 0) - - def testDevicePosition(self): - (x, y, z) = self.device.position() - self.assertIsNotNone(x) - self.assertIsNotNone(y) - self.assertIsNotNone(z) - self.assertTrue(x >= 0) - self.assertTrue(y >= 0) - self.assertTrue(z >= 0) - def handleWill(self, notification): self.willNotificationReceived = True @@ -162,20 +154,24 @@ def testDeviceHomeNotifications(self): NotificationCenter().removeObserver(self) + class TestDebugLinearMotionDeviceBase(BaseTestCases.TestLinearMotionDevice): def setUp(self): self.device = DebugLinearMotionDevice() super().setUp() + class TestDebugSutterDeviceBase(BaseTestCases.TestLinearMotionDevice): def setUp(self): self.device = SutterDevice("debug") super().setUp() + class TestRealSutterDeviceBase(BaseTestCases.TestLinearMotionDevice): def setUp(self): self.device = SutterDevice() super().setUp() + if __name__ == '__main__': unittest.main() diff --git a/setup.py b/setup.py index 1e485f2..30bf792 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ keywords='hardware devices usb communication app control spectrometer powermeter camera', packages=setuptools.find_packages(), install_requires=['numpy','matplotlib','PySerial','PyUSB','pyftdi','LabJackPython'], - python_requires='>=3.7', + python_requires='>=3.8', package_data = { # If any package contains *.txt or *.rst files, include them: '': ['*.png'], @@ -44,7 +44,7 @@ # that you indicate whether you support Python 2, Python 3 or both. 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Operating System :: OS Independent' ]