hardwarelibrary 2.0.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/.claude/skills/pyhardwarelibrary/SKILL.md +20 -7
  2. hardwarelibrary-2.2.0/CHANGELOG.md +476 -0
  3. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/CLAUDE.md +21 -4
  4. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/PKG-INFO +223 -43
  5. {hardwarelibrary-2.0.0/docs/source → hardwarelibrary-2.2.0}/README.md +222 -42
  6. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0/docs/source}/README.md +222 -42
  7. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/__main__.py +30 -0
  8. hardwarelibrary-2.2.0/hardwarelibrary/capabilities.py +1366 -0
  9. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/debugport.py +81 -1
  10. hardwarelibrary-2.2.0/hardwarelibrary/communication/protocol.py +1425 -0
  11. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/usbport.py +72 -4
  12. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/daq/labjackdevice.py +21 -21
  13. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/daq/sr830device.py +25 -25
  14. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/motion/linearmotiondevice.py +16 -35
  15. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/motion/rotationdevice.py +14 -11
  16. hardwarelibrary-2.2.0/hardwarelibrary/motion/sutterdevice.py +185 -0
  17. hardwarelibrary-2.2.0/hardwarelibrary/physicaldevice.py +494 -0
  18. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/powermeters/powermeterdevice.py +7 -4
  19. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/powerstrips/pwrusb.py +0 -8
  20. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/spectrometers/base.py +31 -5
  21. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/spectrometers/oceaninsight.py +2 -2
  22. hardwarelibrary-2.2.0/hardwarelibrary/tests/testCapabilities.py +591 -0
  23. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testFieldMasterDevice.py +2 -2
  24. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testOISpectrometer.py +5 -0
  25. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testPhysicalDevice.py +8 -2
  26. hardwarelibrary-2.2.0/hardwarelibrary/tests/testProtocolDebugPort.py +167 -0
  27. hardwarelibrary-2.2.0/hardwarelibrary/tests/testProtocolPrototype.py +879 -0
  28. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testSR830.py +20 -15
  29. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testSutterDevice.py +11 -0
  30. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testSutterSerialPort.py +9 -4
  31. hardwarelibrary-2.2.0/hardwarelibrary/tests/testValidation.py +119 -0
  32. hardwarelibrary-2.2.0/hardwarelibrary/validation.py +110 -0
  33. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary.egg-info/PKG-INFO +223 -43
  34. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary.egg-info/SOURCES.txt +6 -0
  35. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary.egg-info/scm_file_list.json +172 -166
  36. hardwarelibrary-2.2.0/hardwarelibrary.egg-info/scm_version.json +8 -0
  37. hardwarelibrary-2.0.0/CHANGELOG.md +0 -255
  38. hardwarelibrary-2.0.0/hardwarelibrary/capabilities.py +0 -677
  39. hardwarelibrary-2.0.0/hardwarelibrary/motion/sutterdevice.py +0 -145
  40. hardwarelibrary-2.0.0/hardwarelibrary/physicaldevice.py +0 -264
  41. hardwarelibrary-2.0.0/hardwarelibrary.egg-info/scm_version.json +0 -8
  42. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/.gitattributes +0 -0
  43. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/.github/workflows/publish.yml +0 -0
  44. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/.github/workflows/tests.yml +0 -0
  45. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/.gitignore +0 -0
  46. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/.readthedocs.yaml +0 -0
  47. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README-1-USB.md +0 -0
  48. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README-2-RS232.md +0 -0
  49. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README-3-USB-Cameras.md +0 -0
  50. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README-4-New-device-coding-example.md +0 -0
  51. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README-5-Communication-ports.md +0 -0
  52. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README-6-DebugPort.md +0 -0
  53. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README-7-Sutter-ROE-200.md +0 -0
  54. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/Am.png +0 -0
  55. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/asiZwo.png +0 -0
  56. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/automation.png +0 -0
  57. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/image-20210415195013040.png +0 -0
  58. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/image-20210415195905341.png +0 -0
  59. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/image-20210417140020797.png +0 -0
  60. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/image-20210423112830851.png +0 -0
  61. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/image-20210423112945687.png +0 -0
  62. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/image-20210423113821265.png +0 -0
  63. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/image-20210423113954823.png +0 -0
  64. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/integra.png +0 -0
  65. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/integra.png.sb-90e26077-lWIRgO +0 -0
  66. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/README.assets/preview.png +0 -0
  67. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/SCPI.md +0 -0
  68. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/Makefile +0 -0
  69. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/make.bat +0 -0
  70. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/README-1-USB.md +0 -0
  71. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/README-2-RS232.md +0 -0
  72. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/README-3-USB-Cameras.md +0 -0
  73. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/README-4-New-device-coding-example.md +0 -0
  74. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/README-5-Communication-ports.md +0 -0
  75. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/README-6-DebugPort.md +0 -0
  76. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/README-7-Sutter-ROE-200.md +0 -0
  77. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/_static/.gitkeep +0 -0
  78. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/api/cameras.rst +0 -0
  79. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/api/communication.rst +0 -0
  80. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/api/core.rst +0 -0
  81. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/api/daq.rst +0 -0
  82. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/api/motion.rst +0 -0
  83. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/api/oscilloscope.rst +0 -0
  84. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/api/powermeters.rst +0 -0
  85. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/api/sources.rst +0 -0
  86. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/api/spectrometers.rst +0 -0
  87. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/conf.py +0 -0
  88. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/docs/source/index.rst +0 -0
  89. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/examples/1-simple/cobolt.py +0 -0
  90. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/examples/2-class/cobolt.py +0 -0
  91. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/examples/3-class+debugPort/cobolt.py +0 -0
  92. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/examples/verdig/README.md +0 -0
  93. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/examples/verdig/status_monitor_dll.py +0 -0
  94. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/examples/verdig/temperature_monitor_native.py +0 -0
  95. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/.idea/inspectionProfiles/profiles_settings.xml +0 -0
  96. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/.idea/vcs.xml +0 -0
  97. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/.idea/workspace.xml +0 -0
  98. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/__init__.py +0 -0
  99. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/cameras/__init__.py +0 -0
  100. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/cameras/camera.py +0 -0
  101. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/.idea/.gitignore +0 -0
  102. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/.idea/communication.iml +0 -0
  103. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/.idea/inspectionProfiles/profiles_settings.xml +0 -0
  104. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/.idea/misc.xml +0 -0
  105. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/.idea/modules.xml +0 -0
  106. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/.idea/vcs.xml +0 -0
  107. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/__init__.py +0 -0
  108. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/commands.py +0 -0
  109. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/communicationport.py +0 -0
  110. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/diagnostics.py +0 -0
  111. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/hidport.py +0 -0
  112. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/labviewtcpport.py +0 -0
  113. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/libusb/Darwin/libusb-1.0.0.dylib +0 -0
  114. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/libusb/MS32/libusb-1.0.dll +0 -0
  115. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/libusb/MS64/libusb-1.0.dll +0 -0
  116. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/prologixgpibport.py +0 -0
  117. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/serialport.py +0 -0
  118. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/communication/tcpport.py +0 -0
  119. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/daq/__init__.py +0 -0
  120. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/devicecontroller.py +0 -0
  121. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/devicemanager.py +0 -0
  122. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/echodevice.py +0 -0
  123. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Cobolt-owners-manual-0401-D0105-A.pdf +0 -0
  124. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Coherent-HOPS-1-Overview-and-Runbook.md +0 -0
  125. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Coherent-HOPS-2-USB-and-DLL-Protocol.md +0 -0
  126. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Coherent-HOPS-3-I2C-Circuit.png +0 -0
  127. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Coherent-HOPS-3-I2C-Circuit.svg +0 -0
  128. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Coherent-HOPS-3-I2C-Wire-Protocol.md +0 -0
  129. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Coherent_FieldMaster_GS_Manual_20199913940.pdf +0 -0
  130. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/HR2000+-OEM-Data-Sheet.pdf +0 -0
  131. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/MPC-325_OpMan.pdf +0 -0
  132. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Matisse Programmer's Guide v2.4.8.pdf +0 -0
  133. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Mightex CCD Line Camera Din8 Connector Description.pdf +0 -0
  134. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Mightex CCD Linear Camera User Manual.pdf +0 -0
  135. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Sirah-Matisse-Serial-Commands.md +0 -0
  136. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Spectra-Physics-Millennia-eV-Serial-Commands.md +0 -0
  137. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/TDS 1000B and TDS 2000B Manual.pdf +0 -0
  138. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/Tektronix TDS-1002 Programming manual.pdf +0 -0
  139. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/USB Primer.pdf +0 -0
  140. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/USB2000+-OEM-Data-Sheet.pdf +0 -0
  141. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/USB2000-OEM-Data-Sheet.pdf +0 -0
  142. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/User_Manual_Integra_V3.0.pdf +0 -0
  143. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/manuals/usb4000-oem-data-sheet.pdf +0 -0
  144. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/motion/__init__.py +0 -0
  145. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/motion/intellidrivedevice.py +0 -0
  146. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/motion/thorlabs.py +0 -0
  147. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/oscilloscope/__init__.py +0 -0
  148. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/oscilloscope/oscilloscopedevice.py +0 -0
  149. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/powermeters/__init__.py +0 -0
  150. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/powermeters/fieldmasterdevice.py +0 -0
  151. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/powermeters/integradevice.py +0 -0
  152. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/powerstrips/__init__.py +0 -0
  153. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/powerstrips/powerstripdevice.py +0 -0
  154. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/sources/__init__.py +0 -0
  155. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/sources/cobolt.py +0 -0
  156. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/sources/hopsdll.py +0 -0
  157. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/sources/hopsnative.py +0 -0
  158. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/sources/lasersourcedevice.py +0 -0
  159. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/sources/matisse.py +0 -0
  160. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/sources/millennia.py +0 -0
  161. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/sources/verdig.py +0 -0
  162. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/spectrometers/__init__.py +0 -0
  163. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/spectrometers/darkbulb.png +0 -0
  164. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/spectrometers/intelhexreader.py +0 -0
  165. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/spectrometers/lightbulb.png +0 -0
  166. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/spectrometers/stellarnet.zip +0 -0
  167. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/spectrometers/viewer.py +0 -0
  168. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/deprecated/cobolt.json +0 -0
  169. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/deprecated/socketclient.py +0 -0
  170. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/deprecated/socketmain.py +0 -0
  171. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/deprecated/testCobolt.py +0 -0
  172. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/deprecated/testCoboltDevice.py +0 -0
  173. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/deprecated/testCoboltSerial.py +0 -0
  174. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/deprecated/testIntegraSerialPort.py +0 -0
  175. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/deprecated/testJSON.py +0 -0
  176. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/deprecated/usbFinder.py +0 -0
  177. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/env.py +0 -0
  178. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testCobolt.py +0 -0
  179. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testCommandRecognition.py +0 -0
  180. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testCommunicationPorts.py +0 -0
  181. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testConnectSutter.py +0 -0
  182. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testDeviceController.py +0 -0
  183. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testDeviceManager.py +0 -0
  184. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testGenericSerialConverter.py +0 -0
  185. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testIntegraDevice.py +0 -0
  186. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testIntellidrive.py +0 -0
  187. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testLabjackU3.py +0 -0
  188. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testLabviewTCPPort.py +0 -0
  189. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testLaserCapabilities.py +0 -0
  190. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testLinearMotionDevice.py +0 -0
  191. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testMapPositionsFunction.py +0 -0
  192. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testMatisse.py +0 -0
  193. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testMillennia.py +0 -0
  194. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testModulePyFTDI.py +0 -0
  195. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testModulePySerial.py +0 -0
  196. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testNotificationCenter.py +0 -0
  197. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testOptotuneDevice.py +0 -0
  198. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testOscilloscope.py +0 -0
  199. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testPwrUSB.py +0 -0
  200. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testPyUSB.py +0 -0
  201. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testSerialHardwareFLow.py +0 -0
  202. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testSerialPortConverters.py +0 -0
  203. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testTCPPort.py +0 -0
  204. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testTableDrivenDebugPort.py +0 -0
  205. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testThorlabs.py +0 -0
  206. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/testVerdiG.py +0 -0
  207. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/tests/test_pylablib_kinesis.py +0 -0
  208. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary/utils.py +0 -0
  209. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary.egg-info/dependency_links.txt +0 -0
  210. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary.egg-info/requires.txt +0 -0
  211. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/hardwarelibrary.egg-info/top_level.txt +0 -0
  212. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/pyproject.toml +0 -0
  213. {hardwarelibrary-2.0.0 → hardwarelibrary-2.2.0}/setup.cfg +0 -0
@@ -122,7 +122,7 @@ rot.shutdownDevice()
122
122
 
123
123
  ### Spectrometers — Ocean Insight USB2000 / USB2000+ / USB4000 / USB650 / SAS
124
124
 
125
- Base: `Spectrometer`. The public method *is* the hardware hook (no `do*` wrapper).
125
+ Base: `Spectrometer`. Drivers implement `doGetSpectrum` / `doGetSerialNumber`; `getSpectrum()` forwards any keyword argument to the hook.
126
126
 
127
127
  ```python
128
128
  from hardwarelibrary.spectrometers import Spectrometer
@@ -252,20 +252,33 @@ Devices post Cocoa-style notifications (state changes, measurements, moves) inst
252
252
  requiring polling. Observe them from anywhere:
253
253
 
254
254
  ```python
255
- from hardwarelibrary.notificationcenter import NotificationCenter
255
+ from notificationcenter import NotificationCenter
256
256
  from hardwarelibrary.powermeters.powermeterdevice import PowerMeterNotification
257
257
 
258
258
  def onMeasure(notification):
259
- print("power:", notification.userInfo)
259
+ if notification.user_info["error"] is None:
260
+ print("power:", notification.user_info["result"])
260
261
 
261
- NotificationCenter().addObserver(self, onMeasure, PowerMeterNotification.didMeasure)
262
- # ... measurements now call onMeasure with the value in notification.userInfo
263
- NotificationCenter().removeObserver(self)
262
+ NotificationCenter().add_observer(self, onMeasure, PowerMeterNotification.didGetAbsolutePower)
263
+ # ... measurements now call onMeasure
264
+ NotificationCenter().remove_observer(self)
264
265
  ```
265
266
 
267
+ Every capability posts notifications too, named after the hook: an operation that
268
+ changes the instrument posts `will<Stem>` then `did<Stem>`, a read posts `did<Stem>`
269
+ only, and the `did` is posted even when the driver raised, with the exception under
270
+ `user_info["error"]` (the exception itself still propagates to the caller). The
271
+ payload also carries the method's arguments by name and the return value under
272
+ `"result"`. Each capability names its enum in its `notification` attribute, and
273
+ capabilities related by inheritance share one enum, so `AnalogNotification` covers
274
+ analog input, output, IO and streaming alike. `python -m hardwarelibrary
275
+ --capabilities` lists them all.
276
+
266
277
  Useful notification enums: `PhysicalDeviceNotification` (will/did initialize/shutdown,
267
278
  status), `LinearMotionNotification` (willMove/didMove/didGetPosition),
268
- `PowerMeterNotification.didMeasure`.
279
+ `RotationMotionNotification` (willMove/didMove/didGetOrientation),
280
+ `PowerMeterNotification.didGetAbsolutePower`, `SpectrometerNotification`
281
+ (willGetSpectrum/didGetSpectrum/didGetSerialNumber).
269
282
 
270
283
  ## Headless / GUI apps: DeviceController
271
284
 
@@ -0,0 +1,476 @@
1
+ # Changelog
2
+
3
+ Notable changes to PyHardwareLibrary, loosely following
4
+ [Keep a Changelog](https://keepachangelog.com/). Read this before upgrading:
5
+ API changes can land even when the minor version is unchanged.
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+ - **A protocol is described, not performed** (`communication/protocol.py`). A
11
+ `Frame` turns named values into the bytes to write and the bytes read back into
12
+ named values; it owns no port and stores nothing of what happened. `TextFrame`
13
+ states a `str.format` template and the regular expression that reads it back;
14
+ `BinaryFrame` states one struct format, whose constants are written on the way
15
+ out and *required* on the way in. A `Command` pairs two frames, a
16
+ `CommandDictionary` holds a device's commands and reads them from JSON. Nothing
17
+ is inferred anywhere: a byte order must be explicit, and so must both notations
18
+ of a text line.
19
+ - Both directions come out of the same objects. A driver writes the request and
20
+ reads the reply; a debug port reads the request and writes the reply. That is
21
+ what the old `DataCommand` needed a second encoder and decoder for, and those
22
+ could -- and did -- drift apart.
23
+ - `CommandDictionary.usage()` prints every command, its arguments and what it
24
+ answers, with types read off the description rather than a comment.
25
+ - `CommandDictionary.validate()` refuses a description that is wrong about
26
+ itself: a template and an expression that describe different lines, a struct
27
+ that packs a different number of values than it names, a command whose request
28
+ another command answers to first. It writes each command out with specimen
29
+ values made from the declared types and reads it straight back.
30
+ - **`ProtocolDebugPort`** (`communication/debugport.py`), a debug port with no code
31
+ of its own: hand it a `CommandDictionary` and it stands in for the instrument.
32
+ Whatever a request carries is remembered by name and whatever a reply carries is
33
+ answered from that memory. A command may add a `"sets"` clause for a state change
34
+ its bytes cannot express -- `HOME` carries nothing and still moves a stage.
35
+ - **`PhysicalDevice.protocol`** (default `None`) and **`PhysicalDevice.performTransaction()`**,
36
+ which performs one command of that protocol under the port's `transactionLock`,
37
+ reading by `readLength` for a binary reply and up to the terminator for a line.
38
+ A driver that sets `protocol` needs no send-and-receive code of its own.
39
+
40
+ ### Changed
41
+ - **`SutterDevice` speaks through a description.** Its protocol is data, it sends
42
+ through `performTransaction`, and it no longer checks acknowledgements by hand:
43
+ the description says `MOVE`, `HOME` and `WORK` answer `b"\r"`. Its nested
44
+ `DebugSerialPort` is gone; `serialNumber="debug"` now gets a `ProtocolDebugPort`.
45
+ Callers building the debug port themselves should use
46
+ `ProtocolDebugPort(SutterDevice.protocol)`.
47
+
48
+ ### Removed
49
+ - **`PhysicalDevice.sendCommand()`**. It looked a command up in `self.commands`,
50
+ sent it through `self.port` and handed the `Command` object back so a caller
51
+ could read the reply off it -- the pattern the description above exists to
52
+ replace. No driver in the library called it; the two tests that did now send
53
+ through the `Command` itself, which is all it ever did. Use
54
+ `performTransaction()` with a `protocol`, or a `Command`'s own `send()` while a
55
+ device still carries a `commands` dict.
56
+
57
+ ## [2.1.0] - 2026-08-04
58
+
59
+ ### Added
60
+ - **Notifications on every capability.** Each capability owns a
61
+ `<Capability>Notification` enum, reachable as its `notification` attribute, and
62
+ every public method is wrapped in the new `@notifies(will=..., did=...)`
63
+ decorator, so a driver gets notifications by implementing hooks and writing no
64
+ notification code at all. An operation that changes the instrument posts
65
+ `will<Stem>` then `did<Stem>`; a read (`doGet*`, `doReadStream`) posts only
66
+ `did<Stem>`, to keep a voltage sampled in a loop at one notification instead of
67
+ two. `Spectrometer` gets `SpectrometerNotification`, whose `getSpectrum` keeps a
68
+ `will` because an acquisition takes an integration time.
69
+ - `did*` is posted whether the operation succeeded or not, so a `will*` is always
70
+ followed by its `did*` and there is no separate failure member to pair up. The
71
+ exception is still **re-raised untouched**, since a driver's exception type is
72
+ part of its contract, so a caller sees it exactly as before while an observer
73
+ decides what to do from the payload.
74
+ - `user_info` is a dict of the public method's arguments by name, plus `"result"`
75
+ and `"error"`, exactly one of which is non-None.
76
+ - Capabilities related by inheritance share one enum, so `notification` is the
77
+ same object on all of them and the members are interchangeable:
78
+ `AnalogInputCapability`, `AnalogOutputCapability`, `AnalogIOCapability` and
79
+ `AnalogInputStreamCapability` all post `AnalogNotification`, and the digital
80
+ trio posts `DigitalNotification` (17 enums for 22 capabilities). Members are
81
+ keyed by identity, so without sharing an observer would have to know which
82
+ variant a device mixed in.
83
+ - Measured overhead on a read with no observer is ~1.6 us per call (~2.0 us with
84
+ one observer), against millisecond-scale device I/O.
85
+ - Removes the unused nested `AnalogInputStreamCapability.Notification`
86
+ (`willAcquire` / `didAcquire`), which was never posted; the equivalent members
87
+ are now `AnalogInputStreamNotification.willAcquireWaveform` / `didAcquireWaveform`.
88
+ - The family bases that already posted notifications now follow the same scheme,
89
+ through the same `@notifies` decorator, and name their enum in a `notification`
90
+ attribute like the capabilities do. **Breaking for observers**:
91
+ - `PowerMeterNotification.didMeasure` is now `didGetAbsolutePower`, named after
92
+ its hook like everywhere else.
93
+ - `LinearMotionNotification` and `RotationMotionNotification` keep their grouped
94
+ `willMove` / `didMove` (`moveTo`, `moveBy` and `home` are one operation to an
95
+ observer), but the payload changed: `user_info` is now a dict carrying the
96
+ method's arguments by name plus `"result"` and `"error"`, where it used to be
97
+ the bare position, displacement or angle. A handler reading
98
+ `notification.user_info` as a tuple must now read `user_info["position"]`,
99
+ `user_info["displacement"]` or `user_info["angle"]`.
100
+ - Every one of them now also reports failures: the `did*` is posted even when the
101
+ driver raised, with the exception under `user_info["error"]`.
102
+ - `Spectrometer` gained the `notification` attribute it was missing.
103
+ - `CameraDeviceNotification` is left alone: a capture session is a different
104
+ shape (`imageCaptured` fires per frame), not a will/did pair around one hook.
105
+ - **Contract-level argument validation on the capability public methods**, through a
106
+ new `validate=` parameter on `@notifies` and a new `hardwarelibrary/validation.py`
107
+ holding the shared `require*` checks. The validator runs before anything is posted,
108
+ so a refused call announces nothing and a will/did pair still means the driver was
109
+ invoked. **Breaking**: calls that used to be accepted silently now raise.
110
+ - `acquireWaveform(sampleCount=0)` returned an empty acquisition; now `ValueError`.
111
+ An empty `channels` list failed with `min() iterable argument is empty`; now it
112
+ names the parameter.
113
+ - `configureStream(sampleRate=-100)` was accepted; a rate must be positive, or
114
+ `None` to say the clock is external. `sampleRate=0` no longer means "external":
115
+ pass `None`, which is what the docstring always said.
116
+ - `setDigitalValue("yes", channel)` set the line True; a logic level must be a bool
117
+ (0 and 1 accepted). `setAnalogVoltage("2.5", channel)` now raises `TypeError`.
118
+ - `setSensitivity(-1)` and `setTimeConstant(0)` were silently snapped to a step;
119
+ both must be positive.
120
+ - `setWavelength` and `setDispersion` are checked against the range the driver
121
+ reports, so `matisse.setWavelength(50.0)` no longer drives the birefringent
122
+ filter outside the installed optics' 700-1000 nm.
123
+ - Outlets are checked against `doGetOutletCount()` in `OutletSwitchingCapability`
124
+ and `DefaultOutletCapability`, so `PwrUSBDevice._validateOutlet` is gone and every
125
+ future strip inherits the rule.
126
+ - `setInputSource` and `setTriggerSource` accept anything their enum accepts
127
+ (`setInputSource("Differential")`) and hand the driver a member.
128
+ - Instrument-specific limits stay in the drivers, unchanged.
129
+ - **Every notified operation now requires an initialized device.** `@notifies` calls
130
+ `validateReady()` before anything else, raising `PhysicalDevice.NotInitialized`
131
+ unless the device is `Ready` and naming the operation, the class and the actual
132
+ state. Previously such a call either failed deep inside the driver with
133
+ `AttributeError: 'NoneType' object has no attribute ...` on a port that was never
134
+ opened, or -- on a debug device -- answered as though the hardware had done it and
135
+ posted a `did*` claiming success. The check runs before the `will` is posted, so a
136
+ refused call announces nothing. `validateReady` is defined once, on
137
+ `PhysicalDevice`, where the device lifecycle belongs; `capabilities.py` only calls
138
+ it, so a class mixing in a capability without being a `PhysicalDevice` answers for
139
+ readiness itself rather than silently skipping the check. Methods that only report what a model supports
140
+ (`supportedInputSources`, `supportedSensitivities`, `supportedTimeConstants`,
141
+ `supportedTriggerSources`, `outletCount`) are exempt via `requiresReady=False`,
142
+ since a UI populates its menus before connecting.
143
+ - `allCapabilities()` in `hardwarelibrary/capabilities.py`: returns every capability
144
+ mixin the library defines, in declaration order. It answers the library-wide
145
+ question ("what can be expressed?"), where `PhysicalDevice.capabilities()` answers
146
+ the per-device one ("what does this instrument support?"). Enumerating the module
147
+ rather than walking `Capability.__subclasses__()` keeps the answer independent of
148
+ which device modules happen to be imported, and excludes the drivers, which are
149
+ `Capability` subclasses themselves.
150
+ - `capabilityInterface()` in `hardwarelibrary/capabilities.py`: describes one capability
151
+ as `extends` / `publicAPI` / `hooks` lists of `CapabilityMember(name, signature,
152
+ isAbstract)` tuples. The `do` prefix is what separates a hook from the public API,
153
+ not abstractness: a hook that is optional, or that defaults to a composition of the
154
+ others, is concrete. Members a parent capability declares are left to that parent.
155
+ - `python -m hardwarelibrary --capabilities` (`-c`): prints every capability with the
156
+ methods it defines and the hooks a driver must implement, so the list never has to
157
+ be maintained by hand.
158
+ - `hardwarelibrary/tests/testCapabilities.py`: covers `allCapabilities()` and
159
+ `capabilityInterface()`, and enforces the invariant that every capability mixin is
160
+ declared in `capabilities.py`, by comparing the module listing against a full walk
161
+ of the `Capability` subclass graph.
162
+
163
+ ### Changed
164
+ - **The public/`do*` template method pattern is now uniform across every capability.**
165
+ The DAQ, lock-in and trigger capabilities used to declare their public method
166
+ itself as the `@abstractmethod`; they now follow the same rule as every other
167
+ family: `getXxx()` is concrete and calls `doGetXxx()`, and only the hook is
168
+ abstract. This keeps the public method free for the argument validation,
169
+ notifications and error handling to be added there. Affected:
170
+ `AnalogInputCapability`, `AnalogOutputCapability`, `AnalogIOCapability`,
171
+ `AnalogInputStreamCapability`, `PhaseLockedDetectionCapability`,
172
+ `TriggerCapability`, `DigitalInputCapability`, `DigitalOutputCapability`,
173
+ `DigitalIOCapability`.
174
+ - **Callers are unaffected**: every public name and signature is unchanged.
175
+ - **Driver authors must rename their implementations** to the `do*` hook, e.g.
176
+ `getAnalogVoltage` -> `doGetAnalogVoltage`, `setDigitalValue` ->
177
+ `doSetDigitalValue`, `configureStream` -> `doConfigureStream`,
178
+ `softwareTrigger` -> `doSoftwareTrigger`, `supportedSensitivities` ->
179
+ `doGetSupportedSensitivities`. A driver that misses one fails loudly at
180
+ instantiation with `TypeError`, naming the missing hook. `LabjackDevice` and
181
+ `SR830Device` (and their debug counterparts) were migrated.
182
+ - `configureStream(channels, sampleRate=None, **parameters)` forwards extra
183
+ keyword arguments to `doConfigureStream`, so instrument-specific options
184
+ (the SR830's `sampleClock`, the LabJack's deprecated `scanRate`) still reach
185
+ the driver through the shared public method.
186
+ - **`Spectrometer` follows the same pattern**: `getSpectrum()` and
187
+ `getSerialNumber()` are now concrete and delegate to the abstract
188
+ `doGetSpectrum()` / `doGetSerialNumber()`, which is the last place in the
189
+ library where the public method was itself the hook. `getSpectrum(**parameters)`
190
+ forwards keywords to the driver, so `getSpectrum(maxRequests=2, maxWait=0.05)`
191
+ still reaches `OISpectrometer`. `OISpectrometer` was migrated; `DebugSpectro`
192
+ is unaffected because it does not subclass `Spectrometer`.
193
+ - **ACTION REQUIRED for the licenced StellarNet driver**, which is distributed
194
+ encrypted and is not in this repository: rename its `getSpectrum` and
195
+ `getSerialNumber` to `doGetSpectrum` and `doGetSerialNumber`. Until then
196
+ `StellarNet()` raises `TypeError` for the missing hooks.
197
+ - `hardwarelibrary/tests/testCapabilities.py` now also asserts that no
198
+ `PhysicalDevice` subclass in the library declares an abstract method outside its
199
+ `do*` hooks, so the pattern is enforced for family base classes, not just mixins.
200
+ `HOPSInterface` (`sources/verdig.py`) is deliberately exempt: it is a transport
201
+ strategy behind `VerdiGDevice`, closer to `CommunicationPort` than to a device API.
202
+ - README: the supported-hardware table now lists every driver in the tree. It was
203
+ missing `VerdiGDevice`, `FieldMasterDevice`, `SR830Device`, `PwrUSBDevice` and
204
+ `StellarNet`, and carried the Millennia without its USB identity
205
+ (`0x0483:0x5740`). Added a "Capabilities" section explaining the mixin pattern
206
+ from first principles, and refreshed the class-hierarchy diagram, which was
207
+ stale in the same way.
208
+
209
+ ## [2.0.0] - 2026-07-24
210
+
211
+ Recorded after the fact: this release was tagged without a changelog entry.
212
+
213
+ ### Changed
214
+ - **`NotificationCenter` now comes from the standalone `notifcenter` package** on
215
+ PyPI, added as a dependency, rather than living in the library.
216
+ `hardwarelibrary/notificationcenter.py` is deleted, and the 19 imports across the
217
+ tree point at `from notificationcenter import ...`. `hardwarelibrary/__init__.py`
218
+ re-exports the external package, so `from hardwarelibrary import
219
+ NotificationCenter` keeps working.
220
+ - **The notification API is snake_case**, a clean break with no aliases, across
221
+ roughly 286 call sites: `addObserver` -> `add_observer`, `postNotification` ->
222
+ `post_notification`, `removeObserver` -> `remove_observer`, `observersCount` ->
223
+ `observers_count`, and the keyword arguments and attributes `notificationName`,
224
+ `observedObject`, `userInfo`, `notifyingObject` -> `notification_name`,
225
+ `observed_object`, `user_info`, `notifying_object`. Any code observing a device
226
+ must be updated; this is what made the release a major one. Note that the rest of
227
+ the library remains camelCase -- the snake_case is the external package's
228
+ convention, not a change of style here.
229
+
230
+ ## [1.5.0] - 2026-07-22
231
+
232
+ ### Added
233
+ - PowerStrip device family (`hardwarelibrary/powerstrips/`) plus its first driver
234
+ `PwrUSBDevice` (and `DebugPwrUSBDevice`) for the PwrUSB / PowerUSB controllable
235
+ power strip (USB HID `04d8:003f`, enumerates as "Simple HID Device Demo"). The
236
+ family follows the interface-segregated capability-mixin pattern used by
237
+ `sources/` and `daq/`: `PowerStripDevice` is a thin marker base over
238
+ `PhysicalDevice`, and behaviour comes from `OutletSwitchingCapability`
239
+ (`turnOutletOn`/`turnOutletOff`/`setOutletState`/`isOutletOn`/`outletCount`,
240
+ outlets 1-based), `DefaultOutletCapability` (per-outlet power-on default
241
+ state), and `CurrentMeteringCapability` (`current()` in A, `accumulatedCharge()`
242
+ in Ah, `resetAccumulatedCharge()`), all in the shared
243
+ `hardwarelibrary/capabilities.py`. The strip speaks a single-byte HID report protocol
244
+ driven through a `HIDPort`; the protocol was reverse-engineered publicly and
245
+ cross-checked against aarossig/pwrusbctl (Apache-2.0) and pwrusb.com, but the
246
+ implementation is our own. Outlet state is cached on write because live
247
+ readback is unreliable on this firmware.
248
+ - `HIDPort` (`hardwarelibrary/communication/hidport.py`): a `CommunicationPort`
249
+ over a USB HID device, backed by hidapi (IOKit on macOS), alongside
250
+ `SerialPort` and `USBPort`. Needed because an HID device the OS claims has no
251
+ `/dev` node (so `SerialPort` cannot reach it) and cannot be claimed by libusb
252
+ (so `USBPort` cannot either) -- notably on macOS, where `IOHIDFamily` owns the
253
+ interface. hidapi is an optional dependency; install it with the new `pwrusb`
254
+ extra (`pip install -e .[pwrusb]`), required to drive the strip.
255
+ - `VerdiGDevice` (and `DebugVerdiGDevice`): a laser-source driver for the Coherent
256
+ "HOPS" (High Output Power Supply) laser -- Genesis heads / Verdi G-C, e.g. the
257
+ lab Genesis CX-Vis (head `G532`). A HOPS supply is not a serial device: its
258
+ FTDI FT2232 (`0x0403:0x6010`) is driven as bit-banged I2C, with power DAC, ADC,
259
+ shutter/enable GPIO, and the head identity/calibration EEPROM all on one I2C
260
+ bus (see `manuals/Coherent-HOPS-*`). `VerdiGDevice` combines `OnOffCapability`,
261
+ `ShutterCapability`, `PowerCapability`, and `InterlockCapability`, and drives the bus
262
+ through an interchangeable `HOPSInterface`:
263
+ - `HOPSNativeInterface` (`sources/hopsnative.py`): **pure-Python** pyftdi I2C,
264
+ no DLL (macOS/Linux). Hardware-confirmed end to end on the lab unit
265
+ (identity, on/off, shutter, remote, power setpoint, temperature). Its
266
+ `interlock()`/`faults()` raise `HOPSInterface.NotSupported` until the `?FF`
267
+ decode is reverse-engineered.
268
+ - `HOPSDLLInterface` (`sources/hopsdll.py`): Coherent's `CohrHOPS.dll` (ASCII
269
+ command set; Windows/Linux). Read + `REM`/`PCMD` write paths hardware-
270
+ confirmed; `KSWCMD`/`SHCMD` per the DLL spec, not yet exercised.
271
+ Selection: `VerdiGDevice(interface="auto")` tries native first, then the DLL;
272
+ pass `"native"`/`"dll"`/an interface instance to force one. Protocol and I2C
273
+ decode in `manuals/Coherent-HOPS-2-USB-and-DLL-Protocol.md` and
274
+ `manuals/Coherent-HOPS-3-I2C-Wire-Protocol.md`.
275
+
276
+ ### Changed
277
+ - **Breaking:** capability mixins across all families now use a uniform
278
+ `*Capability` suffix, reserving `*Device` for instantiable hardware drivers.
279
+ Public methods and behavior are unchanged; only the mixin class names change.
280
+ Drivers subclassing these must update their base-class lists and imports.
281
+ - DAQ: `AnalogInputDevice` -> `AnalogInputCapability`, `AnalogOutputDevice` ->
282
+ `AnalogOutputCapability`, `AnalogIODevice` -> `AnalogIOCapability`,
283
+ `AnalogInputStreamDevice` -> `AnalogInputStreamCapability`,
284
+ `DigitalInputDevice` -> `DigitalInputCapability`, `DigitalOutputDevice` ->
285
+ `DigitalOutputCapability`, `DigitalIODevice` -> `DigitalIOCapability`,
286
+ `PhaseLockedDetectionDevice` -> `PhaseLockedDetectionCapability`,
287
+ `TriggerableDevice` -> `TriggerCapability`.
288
+ - Laser sources: `OnOffControl` -> `OnOffCapability`, `ShutterControl` ->
289
+ `ShutterCapability`, `PowerControl` -> `PowerCapability`, `InterlockControl`
290
+ -> `InterlockCapability`, `AutostartControl` -> `AutostartCapability`,
291
+ `WavelengthControl` -> `WavelengthCapability`, `DispersionControl` ->
292
+ `DispersionCapability`.
293
+ - Power meters: `WavelengthCalibratable` -> `WavelengthCalibrationCapability`,
294
+ `AutoScalable` -> `AutoScaleCapability`, `ScaleAdjustable` ->
295
+ `ScaleCapability`.
296
+ - **Breaking:** all capability mixins are consolidated into a single module,
297
+ `hardwarelibrary/capabilities.py`, and share one `Capability` base class (the
298
+ per-family `sources/capabilities.py`, `powermeters/capabilities.py`, and
299
+ `daq/daqdevice.py` are removed; the DAQ enums `InputSource`, `TriggerSource`,
300
+ `SampleClock` move there too, and the acquisition notification enum is now
301
+ nested as `AnalogInputStreamCapability.Notification`). Imports must point at
302
+ `hardwarelibrary.capabilities` (the family package `__init__`s still re-export
303
+ their own mixins, so `from hardwarelibrary.daq import AnalogIOCapability` and
304
+ the like keep working). `capabilities()` / `hasCapability()` are hoisted onto
305
+ `PhysicalDevice`, so every device -- including DAQ drivers -- now supports
306
+ capability introspection; the duplicated methods on `LaserSourceDevice` and
307
+ `PowerMeterDevice` are gone (`LaserSourceDevice` is now a pure marker).
308
+
309
+ ## [1.4.0] - 2026-07-08
310
+
311
+ ### Added
312
+ - `SR830Device` (and `DebugSR830Device`): Stanford Research SR830 DSP lock-in
313
+ amplifier over a Prologix GPIB-USB controller. It combines several capabilities:
314
+ `AnalogInputStreamDevice` (the four rear-panel Aux A/D inputs via `OAUX?`, plus
315
+ hardware-timed buffered acquisition of the demodulated outputs from the internal
316
+ data buffer), `AnalogOutputDevice` (the four rear-panel Aux D/A outputs via
317
+ `AUXV`), `PhaseLockedDetectionDevice` (X/Y/R/theta, reference frequency, signal
318
+ input source, sensitivity, and time constant), and `TriggerableDevice` (the
319
+ rear-panel TRIG IN). Enums: `AuxInput`, `AuxOutput`, `StreamChannel`,
320
+ `InputSource`. `doInitializeDevice` self-discovers the Prologix among the
321
+ connected FTDI adaptors by confirming `*IDN?`, and pins the adaptor's serial.
322
+ - `PrologixGPIBPort` (`hardwarelibrary/communication/`): a `SerialPort` subclass
323
+ that speaks the Prologix GPIB-USB controller `++` protocol. GPIB instruments
324
+ talk to it with the ordinary `readString`/`writeString` primitives; the `++read
325
+ eoi` handshake is encapsulated in its `readString`.
326
+ - New DAQ capability contracts in `daq/daqdevice.py`: `PhaseLockedDetectionDevice`
327
+ (lock-in / phase-locked detection), `TriggerableDevice` with the `TriggerSource`
328
+ enum, and the `SampleClock` enum for stream sample clocking.
329
+
330
+ ### Changed
331
+ - **Breaking:** `AnalogInputStreamDevice`: the sample-rate parameter of
332
+ `configureStream`/`acquireWaveform` is renamed `scanRate` -> `sampleRate`.
333
+ Callers passing it positionally are unaffected; callers passing `scanRate=` by
334
+ keyword must switch to `sampleRate=`. `LabjackDevice.configureStream` keeps
335
+ `scanRate` as a temporary deprecated synonym, so LabJack callers are unaffected
336
+ for now.
337
+ - `LabjackDevice` now imports `u3` (LabJackPython) lazily at point of use, so
338
+ `import hardwarelibrary.daq` (and the new SR830 driver) works on hosts that do
339
+ not have LabJackPython installed.
340
+
341
+ ## [1.3.3] - 2026-07-08
342
+
343
+ ### Changed
344
+ - Heavy third-party modules are now imported lazily, at their point of use,
345
+ instead of at module load. `import hardwarelibrary` no longer pulls in
346
+ `matplotlib` or `numpy` (import time drops from ~336 ms to ~59 ms); they load
347
+ only when a plot is drawn or a spectrum is acquired. Affected: the
348
+ spectrometers package (`SpectraViewer` deferred into `display()`/`displayAny()`,
349
+ `numpy` into the methods that use it; `base.py` uses
350
+ `from __future__ import annotations` for its `-> np.array` hint),
351
+ `OscilloscopeDevice.displayWaveforms`, and the cameras module (`cv2`). Public
352
+ APIs are unchanged. Two behavioral notes: importing `hardwarelibrary.cameras`
353
+ no longer prints a warning when OpenCV is absent — a missing `cv2` now raises
354
+ `ModuleNotFoundError` when a camera operation is invoked; and the unused
355
+ matplotlib import block in `oceaninsight.py` was removed.
356
+
357
+ ### Removed
358
+ - Dead `from pyftdi.ftdi import Ftdi` imports in `SutterDevice` and `EchoDevice`
359
+ (both were unused and flagged `# FIXME: should not be here`). FTDI access still
360
+ goes through `SerialPort`, which owns the `pyftdi` dependency.
361
+
362
+ ## [1.3.2] - 2026-07-07
363
+
364
+ ### Added
365
+ - `MillenniaEv25Device` (and its `MillenniaDevice` alias) now discovers its port
366
+ by USB identity when constructed without a `portPath`. The class carries the
367
+ STM32 Virtual COM Port identity `classIdVendor = 0x0483` /
368
+ `classIdProduct = 0x5740`, and `doInitializeDevice` matches it over pyserial's
369
+ ports, raising `UnableToInitialize` naming the identity when none is found. An
370
+ explicit `portPath` still takes precedence, and a `serialNumber` narrows
371
+ discovery when several STM32 USB-CDC ports are present. Note: `0x0483:0x5740`
372
+ is STMicro's generic STM32 VCP identity shared by unrelated STM32 boards, so
373
+ pin `portPath` on a host that has more than one.
374
+
375
+ ## [1.3.1] - 2026-07-06
376
+
377
+ ### Added
378
+ - Power-meter capability mixins (`powermeters/capabilities.py`), mirroring the
379
+ laser-source `Capability` structure: `WavelengthCalibratable`
380
+ (`getCalibrationWavelength` / `setCalibrationWavelength`), `AutoScalable`
381
+ (`autoScaleIsOn` / `turnAutoScaleOn` / `turnAutoScaleOff`), and
382
+ `ScaleAdjustable` (`getScale` / `setScale` / `availableScales`), each
383
+ delegating to `do*` hooks the driver implements. `PowerMeterDevice` gains
384
+ `capabilities()` and `hasCapability(capabilityClass)` for introspection.
385
+
386
+ ### Changed
387
+ - The wavelength-calibration hooks (`doGetCalibrationWavelength`,
388
+ `doSetCalibrationWavelength`) and their public methods move off
389
+ `PowerMeterDevice` into the new `WavelengthCalibratable` mixin. The base now
390
+ requires only `doGetAbsolutePower`. `IntegraDevice` and `FieldMasterDevice`
391
+ declare `WavelengthCalibratable`, so their public API is unchanged; a new
392
+ power meter that calibrates by wavelength must now mix in
393
+ `WavelengthCalibratable` to expose those methods.
394
+ - `PhysicalDevice.__init__` is now a cooperative base: it calls
395
+ `super().__init__()` after consuming the device-identity arguments, so a
396
+ capability mixin combined with a device (e.g.
397
+ `IntegraDevice(PowerMeterDevice, WavelengthCalibratable)`) has its `__init__`
398
+ run instead of being skipped by the MRO. A mixin `__init__` must therefore
399
+ take no required arguments and call `super().__init__()` itself. No existing
400
+ device changes behavior.
401
+
402
+ ## [1.3.0] - 2026-07-06
403
+
404
+ ### Added
405
+ - `FieldMasterDevice` and `DebugFieldMasterDevice` (`powermeters/`): a driver
406
+ for the Coherent FieldMaster GS laser power/energy meter over RS-232 via an
407
+ FTDI adaptor (9600 8N1, LF terminator, `pw?`/`en?`/`wv?`/`v` commands). The
408
+ meter has no USB identity of its own, so `classIdVendor`/`classIdProduct` are
409
+ the generic FTDI values (0x0403/0x6001); disambiguate multiple FTDI adaptors
410
+ with the adaptor `serialNumber` or an explicit `portPath`. The message
411
+ terminator is a front-panel Menu setting (LF/CR/CR-LF); `initializeDevice`
412
+ probes with the configured `terminator` (default LF) and falls through the
413
+ other combinations until one replies, so a mismatched Menu self-heals. Note:
414
+ the meter only answers RS-232 while on its Home or Trend screen, and
415
+ `initializeDevice` raises with that hint when none of the terminators reply.
416
+ - `SerialPort.genericSerialConverterPorts()` and `isGenericSerialConverter()`,
417
+ plus the `genericSerialConverterVendors` table: discover connected ports that
418
+ come from a generic USB/RS-232 converter chip (FTDI, Prolific, Silicon Labs
419
+ CP210x, WCH CH34x), so an instrument with no USB identity of its own can be
420
+ located and disambiguated by the adaptor's serial number.
421
+ - `PhysicalDevice.usesGenericSerialConverter` flag (default `False`). A device
422
+ behind a generic converter matches any converter vendor (its `vidpids()`
423
+ expands to the whole table, product id wildcarded), and
424
+ `DeviceManager.candidateClassesForAutoDiscovery()` excludes such classes from
425
+ automatic probing, since their VID/PID identifies only the cable. FieldMaster,
426
+ oscilloscope, Echo and IntelliDrive are flagged and must be constructed
427
+ explicitly; Thorlabs (custom-EEPROM FTDI PID) stays a specific identity.
428
+
429
+ ### Changed
430
+ - `PhysicalDevice.isCompatibleWith` treats a `None` product id in a `vidpids()`
431
+ pair as a wildcard that matches any product from that vendor. Concrete
432
+ `(vendor, product)` pairs are unaffected.
433
+
434
+ ### Fixed
435
+ - `OISpectrometer.getSpectrum` no longer hangs. The wait for the "spectrum
436
+ ready" flag is now bounded (`maxRequests`/`maxWait`) and raises
437
+ `SpectrumRequestTimeoutError` instead of re-requesting a spectrum forever on a
438
+ transient USB glitch; transient `usb.core.USBError` (incl. `USBTimeoutError`)
439
+ during polling is absorbed and retried. `integrationTime` remains the first
440
+ optional argument, so all existing callers are unaffected.
441
+
442
+ ## [1.1.0] - 2026-05-29
443
+
444
+ ### Changed
445
+ - `CommunicationPort`: the optional matching-method argument `alternatePattern`
446
+ is renamed to `errorPattern` on `writeStringExpectMatchingString`,
447
+ `writeStringReadMatchingGroups`, `writeStringReadFirstMatchingGroup`, and
448
+ `readMatchingGroups`, and it now actually works on all of them (it was
449
+ inverted in one method and silently ignored in the others). A reply matching
450
+ `errorPattern` raises `CommunicationReadError` carrying that pattern's capture
451
+ groups; a reply matching neither pattern still raises
452
+ `CommunicationReadNoMatch`. Callers that passed the argument positionally are
453
+ unaffected; callers passing `alternatePattern=` by keyword must switch to
454
+ `errorPattern=`.
455
+ - `CommunicationReadError.__init__` now takes `(reply, groups)` instead of a
456
+ single argument. Catching the exception is unaffected; only code that
457
+ constructs or raises it directly must update.
458
+ - `MatisseDevice.queryString` is renamed to `query`. The high-level API
459
+ (`wavelength`, `setWavelength`, the BiFi/thin-etalon/piezo/scan get/set/lock
460
+ methods, and `sendSetting`) is unchanged.
461
+
462
+ ### Added
463
+ - `CommunicationReadError` exception (replaces `CommunicationReadAlternateMatch`).
464
+ - `CommunicationPort.writeStringReadMatch` and `CommunicationPort.matchReply`,
465
+ the shared write-then-read-then-match and pure-match helpers the matching
466
+ methods now delegate to.
467
+ - `DebugMatissePort`, a debug port that speaks the Matisse reply grammar so
468
+ `DebugMatisseDevice` runs the same port code path as the real device.
469
+
470
+ ### Removed
471
+ - `MatisseDevice.parseReply`. Its job is now expressed as the port's
472
+ `replyPattern`/`errorPattern`, with errors mapped to `MatisseCommanderError`.
473
+
474
+ ### Deprecated
475
+ - `CommunicationReadAlternateMatch` is kept as an alias for
476
+ `CommunicationReadError` and will be removed in a future release.
@@ -51,19 +51,36 @@ Each family has an **abstract base class**. A driver subclasses it and implement
51
51
  | Linear motion | `motion/` | `LinearMotionDevice` | `doMoveTo`, `doMoveBy`, `doGetPosition`, `doHome` |
52
52
  | Rotation | `motion/` | `RotationDevice` | `doMoveTo`, `doMoveBy`, `doGetOrientation`, `doHome` |
53
53
  | Power meter | `powermeters/` | `PowerMeterDevice` | `doGetAbsolutePower`, `doGetCalibrationWavelength`, `doSetCalibrationWavelength` |
54
- | Spectrometer | `spectrometers/` | `Spectrometer` (`base.py`) | `getSpectrum`, `getSerialNumber` (here the public method *is* the hook; no `doXxx` wrapper) |
54
+ | Spectrometer | `spectrometers/` | `Spectrometer` (`base.py`) | `doGetSpectrum`, `doGetSerialNumber` |
55
55
  | Oscilloscope | `oscilloscope/` | `OscilloscopeDevice` | instantiated directly (Tektronix), no family/driver split; per-instrument SCPI methods |
56
56
  | Camera | `cameras/` | `CameraDevice` | `doCaptureFrame` |
57
57
  | Laser source | `sources/` | `LaserSourceDevice` (marker) + capability mixins (see below) | the `do*` hooks of whichever mixins the driver declares |
58
- | DAQ | `daq/` | capability mixins (see below) | one method per mixin: `getAnalogVoltage` / `setAnalogVoltage` / `getDigitalValue` / `setDigitalValue` |
58
+ | DAQ | `daq/` | capability mixins (see below) | the `do*` hooks of whichever mixins the driver declares |
59
59
 
60
60
  **Thorlabs linear motion** is a backend dispatcher: `ThorlabsDevice` (in `motion/thorlabs.py`) routes to `ThorlabsKinesisDevice`, which drives the stage through `pylablib`'s Kinesis support. The old FTDI path (`ThorlabsFTDIDevice`) was dropped. Install with the `thorlabs` extra (`pip install -e .[thorlabs]`).
61
61
 
62
62
  All families **use interface-segregated capability mixins** instead of one fat base class, because a device may implement any subset of the capabilities. Every mixin lives in the single module `hardwarelibrary/capabilities.py` and subclasses the one `Capability` base there; mixins carry the `*Capability` suffix, and only instantiable hardware drivers are named `*Device`. `PhysicalDevice` provides `capabilities()` / `hasCapability(cls)`, which introspect any device by walking its MRO for `Capability` subclasses — so every family gets capability introspection for free.
63
63
 
64
- - DAQ: `AnalogInputCapability` (`getAnalogVoltage`), `AnalogOutputCapability` (`setAnalogVoltage`), `DigitalInputCapability` (`getDigitalValue`), `DigitalOutputCapability` (`setDigitalValue`), plus `AnalogIOCapability` / `DigitalIOCapability` that combine each pair, and `AnalogInputStreamCapability` for hardware-timed acquisition. The `configure*` and `direction*` methods are optional no-op hooks. Example: `class LabjackDevice(PhysicalDevice, AnalogIOCapability, DigitalIOCapability, AnalogInputStreamCapability)`.
65
- - Laser sources: `OnOffCapability` (`turnOn`/`turnOff`/`isLaserOn`), `ShutterCapability` (`openShutter`/`closeShutter`/`isShutterOpen`), `PowerCapability` (`setPower`/`power`), `InterlockCapability` (`interlock`), `AutostartCapability`, `WavelengthCapability` (`setWavelength`/`wavelength`), `DispersionCapability`. Each exposes a public method that calls a `do*` abstract hook the driver implements. `LaserSourceDevice` is a thin marker base; the behavior comes from the mixins. Examples: `class CoboltDevice(LaserSourceDevice, OnOffCapability, PowerCapability, ...)`, `class MillenniaEv25Device(LaserSourceDevice, OnOffCapability, ShutterCapability, PowerCapability)`, `class MatisseDevice(PhysicalDevice, WavelengthCapability)`.
64
+ **Every capability follows the same template**, without exception: the public method is concrete and delegates to a `do*` hook, `getXxx()` calling `doGetXxx()`, and only the hook is `@abstractmethod`. A driver implements hooks and never overrides the public method, which is where argument validation and notifications live. Hooks that are optional, or that default to a composition of the other hooks (`doAcquireWaveform`, `doGetDemodulatedValues`, `doGetSupported*`), are concrete but still carry the `do` prefix. `hardwarelibrary/tests/testCapabilities.py` enforces this: no public method on any capability may be abstract.
65
+
66
+ **Every capability posts notifications.** Each names its enum in its `notification` attribute, and every public method is wrapped in `@notifies(will=..., did=...)` from `capabilities.py`, so a driver gets notifications for free by implementing hooks. 17 enums cover the 22 capabilities. The rules, all enforced by `testCapabilities.py`:
67
+
68
+ - An operation that changes the instrument posts `will<Stem>` before and `did<Stem>` after; a **read (`doGet*`, `doReadStream`) posts only `did<Stem>`**, to keep hot paths (a voltage sampled in a loop) at one notification instead of two. `Spectrometer.getSpectrum` is the one read that keeps a `will`, because acquiring takes an integration time.
69
+ - **`did*` is posted whether the operation succeeded or not**, so a `will*` is always followed by its `did*` and there is no separate failure member to pair up. The **exception is still re-raised untouched** — a driver's exception type is part of its contract (`SR830Device` raises `ValueError` for an out-of-range Aux voltage, and callers rely on it). An observer decides what to do from the payload; a caller still sees the exception.
70
+ - `user_info` is a dict of the public method's arguments by name, plus `"result"` and `"error"`, exactly one of which is non-None. Test `user_info["error"]`, not the member, to tell success from failure.
71
+ - **Contract-level argument checks live in the capability**, in a `_validateXxx` method handed to `@notifies` as `validate=`, built from the `require*` helpers in `hardwarelibrary/validation.py` (`requireRealNumber`, `requireBool`, `requireAtLeast`, `requirePositive`, `requireWithinRange`, `requireNonEmpty`, `requireMember`). They raise `TypeError` for the wrong kind of value and `ValueError` for one out of bounds. The validator runs **before** anything is posted, so a refused call announces nothing: a will/did pair means the driver really was invoked. Two rules: limits that vary by model (the SR830's ±10.5 V, the Millennia's 0.05–25 W) stay in the driver's hook, next to the numbers they come from; and a validator calls `do*` hooks, never public methods, so validating never posts a notification of its own. Channel identity is deliberately unchecked — an SR830 addresses outputs with `AuxOutput` members where a LabJack uses bare ints.
72
+ - **The device must be initialized.** `@notifies` calls `validateReady()` first, which raises `PhysicalDevice.NotInitialized` unless the state is `Ready`, naming the operation, the class and the actual state. It runs *before* the `will` is posted, since nothing was attempted, so a guarded call posts nothing at all. `validateReady` is defined once, on `PhysicalDevice`, where the lifecycle lives; `capabilities.py` only calls it. A class that mixes in a capability without being a `PhysicalDevice` must therefore answer for readiness itself — the test stubs that stand in for a device do exactly that. Methods that only report what a model supports (`supported*`, `outletCount`) pass `requiresReady=False`, because a UI populates its menus before connecting.
73
+ - **Capabilities related by inheritance share one enum**, so `notification` is literally the same object on all of them and their members are interchangeable: `AnalogInputCapability`, `AnalogOutputCapability`, `AnalogIOCapability` and `AnalogInputStreamCapability` all post `AnalogNotification`; the digital trio posts `DigitalNotification`. This matters because members are keyed by identity — two same-named members in two enums would never cross-fire, so an observer would otherwise have to know whether a device mixed in the combined capability or the plain one.
74
+ - Members are keyed by enum identity, not by their string value, so same-named members in two enums never cross-fire — which is exactly why the sharing above is necessary rather than cosmetic.
75
+ - Composed hooks nest: `acquireWaveform` posts its own pair plus the pairs of the `configureStream` / `startStream` / `readStream` / `stopStream` calls its default implementation makes.
76
+
77
+ - DAQ: `AnalogInputCapability` (`getAnalogVoltage`), `AnalogOutputCapability` (`setAnalogVoltage`), `DigitalInputCapability` (`getDigitalValue`), `DigitalOutputCapability` (`setDigitalValue`), plus `AnalogIOCapability` / `DigitalIOCapability` that combine each pair, and `AnalogInputStreamCapability` for hardware-timed acquisition. Drivers implement `doGetAnalogVoltage` / `doSetAnalogVoltage` / `doGetDigitalValue` / `doSetDigitalValue` and the streaming hooks; the `doConfigure*` and `do*Direction` hooks are optional no-ops. `configureStream(channels, sampleRate, **parameters)` forwards any extra keyword to `doConfigureStream`, which is how instrument-specific options reach a driver (the SR830's `sampleClock`). Example: `class LabjackDevice(PhysicalDevice, AnalogIOCapability, DigitalIOCapability, AnalogInputStreamCapability)`.
78
+ - Laser sources: `OnOffCapability` (`turnOn`/`turnOff`/`isLaserOn`), `ShutterCapability` (`openShutter`/`closeShutter`/`isShutterOpen`), `PowerCapability` (`setPower`/`power`), `InterlockCapability` (`interlock`), `AutostartCapability`, `WavelengthCapability` (`setWavelength`/`wavelength`), `DispersionCapability`. `LaserSourceDevice` is a thin marker base; the behavior comes from the mixins. Examples: `class CoboltDevice(LaserSourceDevice, OnOffCapability, PowerCapability, ...)`, `class MillenniaEv25Device(LaserSourceDevice, OnOffCapability, ShutterCapability, PowerCapability)`, `class MatisseDevice(PhysicalDevice, WavelengthCapability)`.
66
79
  - Power meters: `WavelengthCalibrationCapability`, `AutoScaleCapability`, `ScaleCapability` (on top of the base `doGetAbsolutePower` every meter implements).
80
+ - Lock-in / triggering: `PhaseLockedDetectionCapability`, `TriggerCapability` (`SR830Device`).
81
+ - Power strips: `OutletSwitchingCapability`, `DefaultOutletCapability`, `CurrentMeteringCapability` (`PwrUSBDevice`).
82
+
83
+ To see every capability with the methods it defines and the hooks it requires: `python -m hardwarelibrary --capabilities`, or `allCapabilities()` / `capabilityInterface()` from `hardwarelibrary/capabilities.py`.
67
84
 
68
85
  ## Adding a new device
69
86