Package: hardwarelibrary
PyHardwareLibraryA simple device-oriented library for controlling hardware devices in the laboratory.You may be here for one of two things:You want to use a device (e.g., Ocean Insight spectrometer), get data, and save it.You want to program a driver to get a new device to work on your computer.If this applies to you, then keep reading.Learning moreIf you are interested in learning more about:USB itselfand connectivity details, please readREADME-USB.md.RS-232 and its relation to USB, please readREADME-RS232.mdexperimenting with an RS-232 chip from FTDI, please readDAQ I/O with UM232R (french only). You can probably Google translate or DeepL translate the Markdown filehere.USB Cameras, please readREADME-USB-Cameras.mdhowPyHardwareLibrarydeals with the many different ports, please readREADME-Communication ports.mdthe process involved in supporting a new deviceinPyHardwareLibrarybut also in general, please readREADME-New-device-coding-example.mdIt would be a good plan to read all of the above, essentially in that order.What is the purpose of this hardware library?We often need to control devices in the laboratory (linear stages, spectrometers, cameras, shutters, etc...).  The drivers provided by many companies are a good start, but integrating the devices in custom software sometimes gets difficult. This Python module was created tofacilitate the development of drivers,facilitate the creation of applications, andprovide minimal but useful applicationsfor hardware that is often used in the lab. It originates from a (private) project that I personnally maintained for nearly 10 years where drivers were written in Objective-C and included support for more than 30 different devices used in my laboratory.  However, Python is more commonly taught in school and supports essentially all platforms, therefore I started this project so that I can 1) teach how to go about developing simple drivers, 2) teach good programming practices to students, 3) get the hardware working for my own lab regardless of the platforms used (we use macOS and Windows), 4) get help to shorten the development cycles to support more devices.Why Python? Python is object-oriented (essential) and offers reasonable performance. Python also has the quality of being a very nice team player: it is fairly easy to integrate Python with anything, on any platform and the community is extremely active.  It is obvious by the numerous Python SDKs from companies, the thousands of modules on PyPi.org, and the support from all vendors (Microsoft, Apple and Linux). Python is also not a dead language: I am very pleased to see the language evolve over the years with new language features and new standard modules.Getting started with using devicesTo use this module, you need to install it bydownloading itfrom GitHub then typing:python setup.py install.Right now, the only useful devices supported are the Ocean Insight spectrometers,USB2000andUSB4000. If you only want to use it, then the following two-line script will do:fromhardwarelibrary.spectrometersimportOISpectrometerOISpectrometer.displayAny()The first supported spectrometer connected will be chosen and a window will appear displaying the spectrum.If you want to do more such as integrating it in some other software you are writing, at this point the best option is to typehelp(someClass)to get the help from the code:>>>fromhardwarelibrary.spectrometersimportOISpectrometer>>>help(OISpectrometer)HelponclassOISpectrometerinmodulehardwarelibrary.spectrometers.oceaninsight:classOISpectrometer(builtins.object)|OISpectrometer(idProduct,model,serialNumber=None)||AnOceaninsight(OceanOptics)spectrometer.Thisallowscompleteaccess|tothehardwarewithsimplefunctionstogetthespectrum,ormodifythe|integrationtime.ItisthebaseclassforallOceanInsightSpectrometers,|butyouwillnotinstantiatethisdirectly:useUSB2000()orUSB4000(),|orsimply:OISpectrometer.any()togetanyspectrometer.||Accesstothedeviceisdonewithpyusbanddoesnotrequireany|additionalinformation.TheUSB-specificattributesofthespectrometersare|available,butarenotneededforstandardusage.Ifyouneedto|implementadditionalfunctionsandcommunicatewiththedevice(notall|capabilitiesarecurrentlycoded),thenyoucouldimplementthemina|separatefunction.|[...]|getCalibration(self)|Getthehardcodedcalibrationfromthespectrometer.Itisa|3rd-orderpolynomial.Currently,nononlinearitiesareconsidered.||getIntegrationTime(self)|Gettheintegrationtimeinasafloatvalueinmilliseconds|cls.timeScaleis1formsand1000ifitisstoredinµs||getParameter(self,index)|Getanyofthe20parametershardcodedintothespectrometer.||Parameters|----------||index:int|0–SerialNumber|1–0thorderWavelengthCalibrationCoefficient|2–1storderWavelengthCalibrationCoefficient[...]Of course, other devices will be supported in the near future.  As mentionned elsewhere in this document, the presentPyHardwareLibraryproject originates from a privateHardwareLibraryproject that supports many different devices, such as Sutter Instruments stages, many Thorlabs devices (stages, shutters, rotation stages, flip mirrors), Spectra Physics Lasers, Cobolt Lasers, Olympus microscopes, GenTech-EO power meters, LabJack, Zaber, Marzhauer, Prior, Newport, Intellidrive, so it is a question of time (and need) before they are ported toPyHardwareLibrary.Getting started with coding for new devicesBut maybe your interest is not just in using the devices, but also in learning how to code to control them. You should find extensive documentation here on how to proceed.You will find a simple, trivial script namedcobolt.pyto change the power of a Cobolt laser. There are three versions, you should read all three examples :1-simple: a very trivial implementation with simple commands in sequence2-class: a class implementation ofCoboltLaserthat partially encapsulates the details and exposes a few functions:setPower()andpower()3-class+debugPort: a class implementation with a debug port that mimicks the real deviceThe main part of the code has aCoboltDevicethat supportsturnOn()turnOff(),setPower()andpower()This is just a very simple example with a laser that probably few people have access to, but should give a general idea.StrategyHow does one go about supporting a new device? What is the best strategy?Obtain the manual.  Look for connectivity information (typically, search forASCIIorserialin the text). You will find information such as "baud rate, stop bits, hardware handshake" and most importantly "ASCII or binary commands". This is what you need.If you can't get the manual from the web site, contact the company.  As mentionned above, many will gladly help you: they usually want to sell devices or satisfy customers who did buy them.Connect to the device, one way or another.If necessary, a driver may need to be installed to serialize the device (to make it appear as a serial port). In this case, you would use theSerialPortclass after having installed that driver.Not all devices can appear as a "serial port".  Simple devices (e.g. a translation stage) are fine because they simply read commands ('MOVE") and reply ("OK").  However, others (camera, spectrometers) respond to commands and transmit data, sometime a lot of it and require many communication lines.  The USB standard provides that (withendpoints), but not the old-style serial port that essentially is just a two-way communication on a single channel.Also, for a device to appear as a serial port, the manufacturer needs to provide a certain amount of information in the USB descriptor of the device. If they don't, you are out of luck.If standard serial ports are not available, direct USB access may be needed withlibusbandPyUSB.  This is the most elegant solution, but requires some knowledge of USB.PyHardwareLibrarymakes use ofPyUSBextensively, andUSBPortsimplifies communication.Figure out (ideally through testing, see next point) how to connect withSerialPortorUSBPort, both derived classes fromCommunicationPortIdentify commands and write very simple tests withSerialPortto confirm connectivity and validate command syntax (see the othersectionbelow for more details):classTestCoboltSerialPort(unittest.TestCase):deftestLaserOn(self):self.port=SerialPort("COM5")# Are settings right? Baud rate, stop bits, etc...self.port.writeStringExpectMatchingString('l1\r',replyPattern='OK')Create aDebugSerialPort, based onCommunicationPortreplicating the behaviour ofSerialPort()to mimic a real serial port.  SeeCoboltDebugSerialfor an example.Completeserialtests that will test both the real port and the debug port. Both must behave identicially.This part is not fully implemented yet:Start wrapping the complex serial communication inside aPhysicalDevice-derivative (e.g.,LaserSourceDevice,LinearMotionDevice, etc…). For an example, seeCoboltDevicewhich derives fromLaserSourceDevice.  For more details on the strategy forPhysicalDevice, see the section :PhysicalDeviceimplementation.Write a series of device tests.  For examples, seetestCoboltDevice.In your device, you must be able to use yourDebugSerialPort.  That way, thetestCoboltDevicecan run both on a real device and a debug device.When all tests pass (Port,DebugPort,Device,DebugDevice), you are doneTesting serial portsWhen testing serial ports, we want to test both the real connection to a given device and a mock implementation (e.g,DebugPort) that behaves like it.  Hence, we want to run a series of tests on each port. The best strategy to run a series of tests on two different instances is the following:Create aBaseTestCasesclass that does not inherit fromunittest.TestCase, with an internal class that does inherit fromunittest.TestCases:classBaseTestCases:classTestCoboltSerialPort(unittest.TestCase):self.port=None...Declare variables that are useful for the test (self.portfor instance).Do not definesetUp()ortearDown()Populate the class with all test methods you need, with names that start withtest*:classBaseTestCases:classTestCoboltSerialPort(unittest.TestCase):port=NonedeftestCreate(self):self.assertIsNotNone(self.port)deftestCantReopen(self):self.assertTrue(self.port.isOpen)withself.assertRaises(Exception)ascontext:self.port.open()...In the same file, define two test subclasses that inherit fromBaseTestCaseswithsetUp()andtearDown()mehods that are specific to either the real port or debug port. They will therefore inherit all the methods from the parent classBaseTestCasesand have all test methods.classTestDebugCoboltSerialPort(BaseTestCases.TestCoboltSerialPort):defsetUp(self):self.port=CommunicationPort(port=CoboltDebugSerial())self.assertIsNotNone(self.port)self.port.open()deftearDown(self):self.port.close()classTestRealCoboltSerialPort(BaseTestCases.TestCoboltSerialPort):defsetUp(self):try:self.port=CommunicationPort(port="COM5")self.port.open()except:raiseunittest.SkipTest("No cobolt serial port at COM5")deftearDown(self):self.port.close()If you have test methods that are specific to a given port, then define them in the specific class.Add the following at the end of the file:if__name__=='__main__':unittest.main()By running the tests in this file withpython testCoboltSerial.py, the unittest framework will automatically run all tests from bothTestDebugCoboltSerialPortandTestRealCoboltSerialPort.  Of course, both should pass all tests for success.This strategy can be reused to test aDeviceand itsDebugDevicecounterpart.Design goalsThis part is not fully implemented yet in this library, but was implemented in a separate project.Communicating with the device through serial ports is the first step.  However, most of the time, we care about some tasks we want to do with the device (turn on and use laser, acquire spectrum from spectrometer, etc...).  Therefore, after having figured out what the commands are and how the device responds, it is important to "wrap" or encapsulate all of those commands inside a class (or object) that represents the device to the end-user and make it easy to use without having to know the details.PyHardwareLibraryuses a base class calledPhysicalDeviceA real physical device is not simple to handle: errors can occur at any time (because of the device itself), because the user did not connect it or did not turn it on, because the device is in an irregular  state (e.g., it reached the end of the travel range for instance).  Hence, it becomes important to handle errors gracefully but especially robustly.The strategy used by the present library is the following:Many properties of devices are common: the have a USB vendor ID, a product ID, a serial number etc…  This is included in a parent class calledPhysicalDevicethat is the parent to all devices.Many methods are also common: all devices must be initialized, shutdown, etc… These methods are defined in the parent class, but call the device-specific method of the derived class. For instance,initializeDevice()does a bit of housekeeping (is the device already initialized? was the underlying initializing successful?) and callsdoInitializeDevicethat must be implemented by the derived class. If initialization fails, it must raise an error. The class must confirm the device responds to at least one internal command to confirm it is indeed the expected device.For specific classes of devices (e.g.,LaserSourceDevice), specific methods are used to hide the details of the implementation:LaserSourceDevice.turnOn(),LaserSourceDevice.power(),LaserSourceDevice.setPower(), etc… These methods call device-specific methods with similar names (prefixed bydo) in the derived class (e.g.,doTurnOn())Methods that start withdowill communicatewith the device through the serial port.  They must store the result of the request into an instance variable (to cache the value and to avoid to go back to the serial port each time the value is needed). For instance, an instanceself.powerstores the result obtained fromdoGetPower().domethods arenevercalled by users.  Users call theturnOn()method but not thedoTurnOn()method. If Python as a language allowed it, thedomethods would be hidden and private, but it does not look possible: the only convention is to use_dobut it is only a convention, functions can still be called.MotivationI must also vent my frustration that end-user software from the manufacturers is often abysmaIly-designed, buggy and/or simply frustrating to use but most of the time, all of the above. I have even seen example code from companies that simply does not even compile. Others will only support Windows 7, and even say it with a straight face in 2021 like it's totally normal. On top of that, many companies will claim (erroneously) that their hardware cannot run on macOS, my platform of choice.  This is usually because of shear laziness or straight out incompetence: as long as it can connect to the computer, it can be supported.  For USB devices, it is oftentrivialto write a "driver" to support a device with appropriate documentation, and I have done it on numerous occasions. The rule of thumb is that the companies that have good software say, on Windows, usually have good software on many platforms, as they obviously understand how to program and undertand the simplicity of writing cross-platform code if you make it a design requirement. On the other hand, I have found that lack of support for platforms other than Windows usually translates in fairly crappy software on Windows anyway: these companies tend to be hardware companies that consider software only secondary and probably farm it out.  Shout out to ActiveSilicon, Sutter Instruments, Hamamatsu, Ocean Insight (for their documentation but certainly not for their software), and Thorlabs for being friendly to developers: they provide all the necessary information upon request and are of great help to scientists. On the other hand, here is a middle finger🖕 to many other companies I will not name here, but many camera providers come to mind (some located nearPrincetonUniversity) as well as a prominent company that rhymes withationalinstrumentsthat wins the grand prize for its uselessness and overall incompetence at providing anything useful in software to their end users for the last 20 years despite producing great hardware (somebody should also let them know that more than 12 pixels can be used to draw icons because thiswith a big red x in it apparently represents "automation" and thisis "amplitude modulation". It would be funny if it wasn't so sad).ContactProf. Daniel Côté, Ph.D. and P.Eng,dccote@cervo.ulaval.caGroup web site:http://www.dcclab.caYoutube channel:http://www.youtube.com/user/dccote