wasatch 2.3.12__tar.gz → 2.4.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 (104) hide show
  1. {wasatch-2.3.12 → wasatch-2.4.0}/CHANGELOG.md +149 -0
  2. {wasatch-2.3.12 → wasatch-2.4.0}/PKG-INFO +1 -1
  3. {wasatch-2.3.12 → wasatch-2.4.0}/WasatchShell/wasatch-shell.py +4 -4
  4. {wasatch-2.3.12 → wasatch-2.4.0}/pyproject.toml +1 -1
  5. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/AbstractUSBDevice.py +4 -2
  6. wasatch-2.4.0/wasatch/AccessoryConnector.py +75 -0
  7. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/AndorDevice.py +94 -82
  8. wasatch-2.4.0/wasatch/AssemblyRevision.py +56 -0
  9. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/AutoRaman.py +233 -101
  10. wasatch-2.4.0/wasatch/BLEDevice.py +1428 -0
  11. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/CSVLoader.py +3 -4
  12. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/DeviceFinderBLE.py +1 -1
  13. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/DeviceFinderUSB.py +4 -0
  14. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/DeviceID.py +3 -0
  15. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/EEPROM.py +257 -118
  16. wasatch-2.4.0/wasatch/EtalonCorrection.py +73 -0
  17. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/FeatureIdentificationDevice.py +371 -305
  18. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/FirmwareRequirements.py +5 -4
  19. wasatch-2.4.0/wasatch/HardwareInfo.py +15 -0
  20. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/IDSCamera.py +7 -0
  21. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/IDSDevice.py +244 -53
  22. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/IMX385.py +42 -0
  23. wasatch-2.4.0/wasatch/InGaAsCorrection.py +79 -0
  24. wasatch-2.4.0/wasatch/InterfaceDevice.py +255 -0
  25. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/MockUSBDevice.py +3 -6
  26. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/OceanDevice.py +2 -41
  27. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/ProcessedReading.py +29 -1
  28. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/Reading.py +20 -2
  29. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/RealUSBDevice.py +5 -0
  30. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/SPIDevice.py +1 -39
  31. wasatch-2.4.0/wasatch/SpectrometerRequest.py +41 -0
  32. wasatch-2.4.0/wasatch/SpectrometerResponse.py +41 -0
  33. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/SpectrometerSettings.py +95 -44
  34. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/SpectrometerState.py +12 -2
  35. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/TCPDevice.py +1 -2
  36. wasatch-2.4.0/wasatch/USBCPowerConnectionState.py +92 -0
  37. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/WasatchBus.py +10 -12
  38. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/WasatchDevice.py +139 -214
  39. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/WasatchDeviceWrapper.py +115 -50
  40. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/WrapperWorker.py +51 -67
  41. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/__init__.py +1 -1
  42. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/utils.py +14 -0
  43. wasatch-2.3.12/wasatch/BLEDevice.py +0 -1032
  44. wasatch-2.3.12/wasatch/CommandSettings.py +0 -95
  45. wasatch-2.3.12/wasatch/HardwareInfo.py +0 -36
  46. wasatch-2.3.12/wasatch/InterfaceDevice.py +0 -40
  47. wasatch-2.3.12/wasatch/PollStatus.py +0 -15
  48. wasatch-2.3.12/wasatch/SpectrometerRequest.py +0 -15
  49. wasatch-2.3.12/wasatch/SpectrometerResponse.py +0 -38
  50. {wasatch-2.3.12 → wasatch-2.4.0}/.example_pypirc +0 -0
  51. {wasatch-2.3.12 → wasatch-2.4.0}/.flake8 +0 -0
  52. {wasatch-2.3.12 → wasatch-2.4.0}/.gitignore +0 -0
  53. {wasatch-2.3.12 → wasatch-2.4.0}/.pylintrc +0 -0
  54. {wasatch-2.3.12 → wasatch-2.4.0}/Doxyfile +0 -0
  55. {wasatch-2.3.12 → wasatch-2.4.0}/LICENSE +0 -0
  56. {wasatch-2.3.12 → wasatch-2.4.0}/MANIFEST.in +0 -0
  57. {wasatch-2.3.12 → wasatch-2.4.0}/Makefile +0 -0
  58. {wasatch-2.3.12 → wasatch-2.4.0}/README.md +0 -0
  59. {wasatch-2.3.12 → wasatch-2.4.0}/README_PYPI.md +0 -0
  60. {wasatch-2.3.12 → wasatch-2.4.0}/README_RPI.md +0 -0
  61. {wasatch-2.3.12 → wasatch-2.4.0}/README_SETTINGS.md +0 -0
  62. {wasatch-2.3.12 → wasatch-2.4.0}/README_VIRTUAL.md +0 -0
  63. {wasatch-2.3.12 → wasatch-2.4.0}/WasatchShell/README.md +0 -0
  64. {wasatch-2.3.12 → wasatch-2.4.0}/WasatchShell/laser-test.py +0 -0
  65. {wasatch-2.3.12 → wasatch-2.4.0}/WasatchShell/load-test.py +0 -0
  66. {wasatch-2.3.12 → wasatch-2.4.0}/WasatchShell/one-shot.py +0 -0
  67. {wasatch-2.3.12 → wasatch-2.4.0}/demo-auto-raman.py +0 -0
  68. {wasatch-2.3.12 → wasatch-2.4.0}/demo-ids-raman.py +0 -0
  69. {wasatch-2.3.12 → wasatch-2.4.0}/demo-simple.py +0 -0
  70. {wasatch-2.3.12 → wasatch-2.4.0}/demo-virtual.py +0 -0
  71. {wasatch-2.3.12 → wasatch-2.4.0}/demo-workflow.py +0 -0
  72. {wasatch-2.3.12 → wasatch-2.4.0}/demo.py +0 -0
  73. {wasatch-2.3.12 → wasatch-2.4.0}/environments/conda-linux.yml +0 -0
  74. {wasatch-2.3.12 → wasatch-2.4.0}/environments/conda-macos.yml +0 -0
  75. {wasatch-2.3.12 → wasatch-2.4.0}/environments/conda-rpi.yml +0 -0
  76. {wasatch-2.3.12 → wasatch-2.4.0}/environments/conda-win10.yml +0 -0
  77. {wasatch-2.3.12 → wasatch-2.4.0}/screenshots/multiplatform.png +0 -0
  78. {wasatch-2.3.12 → wasatch-2.4.0}/scripts/dark-vs-integ.py +0 -0
  79. {wasatch-2.3.12 → wasatch-2.4.0}/scripts/deploy +0 -0
  80. {wasatch-2.3.12 → wasatch-2.4.0}/testSpectrometers/SiG_785/eeprom/EEPROM-EM-9c65d19f4c.json +0 -0
  81. {wasatch-2.3.12 → wasatch-2.4.0}/testSpectrometers/SiG_785/readings/enlighten-20210329-094722-799520-WP-00686.csv +0 -0
  82. {wasatch-2.3.12 → wasatch-2.4.0}/testSpectrometers/SiG_785/readings/enlighten-20210329-094755-288958-WP-00686.csv +0 -0
  83. {wasatch-2.3.12 → wasatch-2.4.0}/testSpectrometers/WP-00887/eeprom/WP-00887-mock.json +0 -0
  84. {wasatch-2.3.12 → wasatch-2.4.0}/testSpectrometers/WP-00904/eeprom/WP-00904-mock.json +0 -0
  85. {wasatch-2.3.12 → wasatch-2.4.0}/testSpectrometers/WP-00913/eeprom/WP-00913-mock.json +0 -0
  86. {wasatch-2.3.12 → wasatch-2.4.0}/tests/existing_implementations.py +0 -0
  87. {wasatch-2.3.12 → wasatch-2.4.0}/tests/noise-by-setpoint.py +0 -0
  88. {wasatch-2.3.12 → wasatch-2.4.0}/tests/queues.py +0 -0
  89. {wasatch-2.3.12 → wasatch-2.4.0}/tests/unit.py +0 -0
  90. {wasatch-2.3.12 → wasatch-2.4.0}/udev/10-wasatch.rules +0 -0
  91. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/AreaScanImage.py +0 -0
  92. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/AutoRamanRequest.py +0 -0
  93. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/BalanceAcquisition.py +0 -0
  94. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/ControlObject.py +0 -0
  95. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/DetectorROI.py +0 -0
  96. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/DetectorRegions.py +0 -0
  97. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/DeviceFinderTCP.py +0 -0
  98. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/EEPROM_multical.sav +0 -0
  99. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/FPGAOptions.py +0 -0
  100. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/ROI.py +0 -0
  101. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/StatusMessage.py +0 -0
  102. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/TakeOneRequest.py +0 -0
  103. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/applog.py +0 -0
  104. {wasatch-2.3.12 → wasatch-2.4.0}/wasatch/simulation_protocol.py +0 -0
@@ -1,5 +1,154 @@
1
1
  # Changelog
2
2
 
3
+ _Note: in the following, FID = wasatch.FeatureInterfaceDevice_
4
+
5
+ - 2026-05-27 2.4.0 (enlighten-4.2.2)
6
+ - InterfaceDevice
7
+ - This was a major change. The InterfaceDevice base class has existed for
8
+ years, but didn't contribute anything beyond handle_requests. I've
9
+ moved message_queue and alert_queue up from WasatchDevice/FID so all
10
+ InterfaceDevice subclasses can use that functionality directly.
11
+ - Also, the handle_requests([ SpectrometerRequest(cmd, args=[value]) ])[0]
12
+ syntax was neurose-inducing, so I've added convenience shortcuts for
13
+ handle_request(SpectrometerRequest) (singular), and handle_cmd(name[, value]).
14
+ - WasatchDevice
15
+ - deprecated command_queue (pending_commands)
16
+ - handle_requests now directly peeks into FID to see which commands can
17
+ be handled by "the hardware layer," and processes those commands in
18
+ real-time, accumulating the list of responses. This allows callers to
19
+ use gettor methods like GET_LASER_WARNING_TIME_SEC and actually receive
20
+ the response value, which was previously impossible when such commands
21
+ were shoved onto the "pending command queue" (which offered no method
22
+ for response delivery).
23
+ - FeatureInterfaceDevice
24
+ - deprecated SpectrumAndRow (legacy Area Scan design)
25
+ - removed handle_requests overload (not sure why this was there)
26
+ - added get_power_connection_state
27
+ - added periodic check for late-arriving attributes like ble_firmware_version
28
+ - enabled get_microcontroller_serial_number
29
+ - tweak laser password validation logic
30
+ - add default laser warning delay sec for old FW
31
+ - IDSDevice
32
+ - paired laser_device
33
+ - A key goal for this release was to allow IDSDevice "sensor
34
+ control" objects to "pair" with WasatchDevice "laser control"
35
+ objects, for the new case where an IDS sensor was being combined
36
+ with a Wasatch laser driver to create a 2-device Raman spectrometer.
37
+ - Note that once "paired," they essentially merge EEPROMs (starting
38
+ with whatever WasatchDevice loaded from USB, then folding-in
39
+ whatever IDSDevice found in JSON).
40
+ - In this first implementation, I chose to connect the two (make one
41
+ device "discoverable" by the other) through WasatchDeviceWrapper.
42
+ That created a weird import loop and maybe was higher than it
43
+ needed to be. I may refactor this later so they're discoverable /
44
+ joined at the InterfaceDevice level itself, which resolves the loop
45
+ and makes the pair usable without WasatchDeviceWrapper (a class
46
+ essentially designed by and for ENLIGHTEN).
47
+ - Part of the point of the above was to support a subtype of Auto-Raman,
48
+ tentatively called Auto-Collection, which skips the integ/gain
49
+ optimization and goes straight for averaging dark-collected Raman
50
+ within a measurement window.
51
+ - EEPROM
52
+ - A third key goal of this release was to support all the new EEPROM fields
53
+ added from Rev18 to Rev19 of ENG-0034, including:
54
+ - startup_laser_tec_setpoint
55
+ - light_source_type
56
+ - assembly_revision (and AssemblyRevision)
57
+ - feature_mask_xs
58
+ - max_battery_temp_deg_c
59
+ - max_laser_temp_deg_c
60
+ - acc_state
61
+ - acc_state_gpio1
62
+ - acc_state_gpio2
63
+ - acc_cont_strobe_count
64
+ - acc_cont_strobe_delay_us
65
+ - acc_cont_strobe_period_us
66
+ - acc_cont_strobe_width_us
67
+ - pixel_correction_type
68
+ - deprecated baud_rate, linearity_coeffs
69
+ - made EEPROM subclasses JSON serializable (hopefully)
70
+ - fixed EEPROM.pack for binary types ("*") for our growing set of "binary"
71
+ (marshalled) fields
72
+ - moved further into using the new un/pack_field functions, automatically
73
+ pulling offset/size/type from eeprom_fields table
74
+ - revisited user_text / user_data relationship
75
+ - consolidating backup location when writing EEPROM (can probably just
76
+ get rid of this)
77
+ - BLEDevice
78
+ - added laser PWM
79
+ - AndorDevice
80
+ - renamed "shutter_enable" (whatever that meant) to "shutter_open"
81
+ - SpectrometerResponse
82
+ - deprecated progress and incomplete
83
+ - starting to consider Safe Mode
84
+ - 2026-04-18 2.3.24
85
+ - add avg_resolution to IDSDevice EEPROM
86
+ - change XS attenuator default from 127 to 40
87
+ - 2026-04-16 2.3.23
88
+ - XS BLE
89
+ - tidied BLEDevice
90
+ - added USBCPowerConnectionState
91
+ - added SpectrometerState.power_connection_state
92
+ - added SpectrometerSettings.cpu_unique_id
93
+ - added SET_POWER_OFF, SET_RESET_UNIT, GET/SET_AUTO_RAMAN_PARAMS,
94
+ GET_IMAGE_SENSOR_STATE, GET_CPU_UNIQUE_ID, GET_USB_ADAPTER_INFO,
95
+ GET/SET_LASER_OFF_DELAY_MS
96
+ - suffixed all async methods with _async
97
+ - provided synchronous wrappers around user-facing asynchronous calls
98
+ - added BLEDevice.testing for enlighten.plugins.RnD.BluetoothTester
99
+ - battery percentage matches USB
100
+ - ambient_temperature_deg_c now flowing up to ENLIGHTEN
101
+ - POWER_OFF working
102
+ - decoding STM32 CPU_UNIQUE_ID (debug-only)
103
+ - ignore 1-line vertical ROI
104
+ - SET_AUTO_RAMAN_PARAMS bugfix
105
+ - 2026-04-10 2.3.22
106
+ - added Reading.protocol
107
+ - XS
108
+ - disable detector timeout during area scan
109
+ - adding Auto-Raman to BLEDevice
110
+ - only use one ACQUIRE to start Area Scan readout
111
+ - send extra ACQUIRE on occasional timeout
112
+ - 2026-04-09 2.3.21
113
+ - XS Area Scan
114
+ - fixed line increment
115
+ - disable area scan while changing line increment
116
+ - XS BLE
117
+ - move firmware revisions to SpectrometerSettings
118
+ - fix gain
119
+ - fix laser enable
120
+ - fixed heartbeat
121
+ - working on LASER_STATE and BATTERY_STATE
122
+ - 2026-04-09 2.3.20
123
+ - XS BLE
124
+ - fix None in utils.to_hex
125
+ - only parse first 8 EEPROM fields until BLE FW updated to support 9
126
+ - stop checking for old FX2 code string in corrupted EEPROMs
127
+ - support V2.EVT prefix
128
+ - add new ACQUIRE status codes
129
+ - add new SPECTRA Characteristic
130
+ - read as many EEPROM bytes per page as supported by Peripheral
131
+ - 2026-04-06 2.3.18
132
+ - replaced WrapperWorker.is_FOO with .class_name
133
+ - XL / Andor
134
+ - only warn once on missing driver
135
+ - more robust support for blank model
136
+ - default EEPROM.model
137
+ - XS
138
+ - support is_laser_firing on all XS
139
+ - 2026-04-01 2.3.17
140
+ - adding Reading.is_auto_raman()
141
+ - temporarily bumping XS spectra timeout
142
+ - filling in standard device meta-data in Auto-Raman Readings
143
+ - 2026-03-30 2.3.16
144
+ - assume all XS have interlock feedback
145
+ - 2026-03-13 2.3.15
146
+ - capture changes to vertical ROI in IDSDevice's EEPROM
147
+ - 2026-03-09 2.3.14
148
+ - save IDS detector in EEPROM
149
+ - 2026-03-02 2.3.13
150
+ - add experimental FPGA reset for InGaAs
151
+ - add Reading.image_format
3
152
  - 2026-02-17 2.3.12
4
153
  - added laser_password, startup_scans_to_average, laser_attenuator
5
154
  - added [un]pack_field
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wasatch
3
- Version: 2.3.12
3
+ Version: 2.4.0
4
4
  Summary: Application-level driver for Wasatch Photonics spectrometers
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -105,7 +105,7 @@ class WasatchShell:
105
105
  "get_selected_adc", # SR
106
106
  "get_selected_laser", # SR
107
107
  "get_sensor_line_length", # SR
108
- "get_shutter_enabled", # SR
108
+ "get_shutter_open", # SR
109
109
  "get_strobe_enabled", # SR
110
110
  "get_tec_enabled", # SR
111
111
  "get_trigger_delay", # SR
@@ -146,7 +146,7 @@ class WasatchShell:
146
146
 
147
147
  set_fan_enable - takes bool argument
148
148
  set_lamp_enable - takes bool argument
149
- set_shutter_enable - takes bool argument
149
+ set_shutter_open - takes bool argument
150
150
  set_strobe_enable - takes bool argument
151
151
  set_mod_enable - takes bool argument
152
152
  set_mod_period_us - takes int argument
@@ -394,8 +394,8 @@ class WasatchShell:
394
394
  self.device.change_setting("lamp_enable", self.read_bool())
395
395
  self.display(1)
396
396
 
397
- elif command == "set_shutter_enable":
398
- self.device.change_setting("shutter_enable", self.read_bool())
397
+ elif command == "set_shutter_open":
398
+ self.device.change_setting("shutter_open", self.read_bool())
399
399
  self.display(1)
400
400
 
401
401
  elif command == "set_strobe_enable":
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wasatch"
3
- version = "2.3.12"
3
+ version = "2.4.0"
4
4
  description= "Application-level driver for Wasatch Photonics spectrometers"
5
5
  requires-python = ">=3.9"
6
6
  readme = "README.md"
@@ -1,5 +1,7 @@
1
-
2
1
  class AbstractUSBDevice:
2
+ """
3
+ This is NOT an InterfaceDevice. This is used by MockUSBDevice and RealUSBDevice.
4
+ """
3
5
 
4
6
  def __init__(self):
5
7
  pass
@@ -26,4 +28,4 @@ class AbstractUSBDevice:
26
28
  pass
27
29
 
28
30
  def send_code(self):
29
- pass
31
+ pass
@@ -0,0 +1,75 @@
1
+
2
+
3
+ class GPIOState:
4
+ MODE_FUNCTION = 0
5
+ MODE_MANUAL = 1
6
+
7
+ DIR_INPUT = 0
8
+ DIR_OUTPUT = 1
9
+
10
+ GPIO_FUNCTION_DISABLED = 0
11
+
12
+ GPIO1_FUNCTION_EXT_TRIGGER = 1
13
+ GPIO1_FUNCTION_LASER_OVERRIDE = 2
14
+
15
+ GPIO2_FUNCTION_CONT_STROBE = 1
16
+ GPIO2_FUNCTION_DATA_READY = 2
17
+ GPIO2_FUNCTION_LASER_MIRROR = 3
18
+ GPIO2_FUNCTION_LASER_ERROR = 4
19
+
20
+ def __init__(self, num):
21
+ self.num = num
22
+ self.mode = GPIOState.MODE_FUNCTION
23
+ self.dir = GPIOState.DIR_INPUT
24
+ self.function = GPIOState.GPIO_FUNCTION_DISABLED
25
+ self.value = 0
26
+
27
+ def serialize(self):
28
+ mask = 0x00
29
+ mask |= 0x01 if self.mode == self.MODE_MANUAL else 0
30
+ mask |= 0x02 if self.dir == self.DIR_OUTPUT else 0
31
+ if self.dir == self.DIR_OUTPUT:
32
+ mask |= 0x04 if self.value else 0
33
+ # bit 3 reserved
34
+ mask |= (self.function << 4)
35
+
36
+ def deserialize(self, mask):
37
+ self.mode = mask & 0x01
38
+ self.dir = mask & 0x02
39
+ if self.dir == self.DIR_INPUT:
40
+ self.value = mask & 0x04
41
+ self.function = (mask >> 4) & 0xf
42
+
43
+ def __repr__(self):
44
+ return f"GPIO{self.num}< mode {self.mode}, dir {self.dir}, func {self.function}, value {self.value} >"
45
+
46
+ class AccessoryConnector:
47
+
48
+ def __init__(self):
49
+ self.acc_gpio_enabled = False
50
+ self.acc_5v_enabled = False
51
+ self.acc_5v_good = False # read-only
52
+
53
+ self.state_gpio1 = GPIOState(1)
54
+ self.state_gpio2 = GPIOState(2)
55
+
56
+ self.strobe_period_us = None
57
+ self.strobe_width_us = None
58
+ self.strobe_delay_us = None
59
+ self.strobe_count = None
60
+
61
+ def serialize_acc_state(self):
62
+ mask = 0x0000
63
+ mask |= 0x0001 if self.acc_gpio_enabled else 0
64
+ mask |= 0x0002 if self.acc_5v_enabled else 0
65
+ # acc_5v_good read-only
66
+ # bits 3-15 reserved
67
+ return mask
68
+
69
+ def deserialize_acc_state(self, mask):
70
+ self.acc_gpio_enabled = 0 != mask & 0x01
71
+ self.acc_5v_enabled = 0 != mask & 0x02
72
+ self.acc_5v_good = 0 != mask & 0x04 # read-only
73
+
74
+ def __repr__(self):
75
+ return f"AccessoryConnector < gpio_enabled {self.acc_gpio_enabled}, 5v_enabled {self.acc_5v_enabled}, 5v_good {self.acc_5v_good}"
@@ -10,7 +10,7 @@ from ctypes import *
10
10
 
11
11
  from .SpectrometerSettings import SpectrometerSettings
12
12
  from .SpectrometerResponse import SpectrometerResponse
13
- from .InterfaceDevice import InterfaceDevice
13
+ from .InterfaceDevice import InterfaceDevice, InterfaceDeviceClassUnavailable
14
14
  from .StatusMessage import StatusMessage
15
15
  from .DeviceID import DeviceID
16
16
  from .Reading import Reading
@@ -24,54 +24,24 @@ class AndorDevice(InterfaceDevice):
24
24
 
25
25
  @todo have check_result return a SpectrometerResponse
26
26
  @todo try to auto-detect whether x-axis needs inverted via DLL.GetImageFlip()
27
-
28
- ##########################################################################
29
- This class adopts the external device interface structure.
30
- This involves receiving a request through the handle_request function.
31
- A request is processed based on the key in the request.
32
- The processing function passes the commands to the requested device.
33
- Once it receives a response from the connected device it then passes that
34
- back up the chain.
35
- @verbatim
36
- Enlighten Request
37
- |
38
- handle_requests
39
- |
40
- -----------
41
- | | | | |
42
- { get_laser status, acquire, set_laser_watchdog, etc....}
43
- | | | | |
44
- -----------
45
- |
46
- {self.driver.some_andor_sdk_call}
47
- @endverbatim
48
- ############################################################################
49
27
  """
50
28
 
51
29
  SUCCESS = 20002 #!< see load_error_codes()
52
30
  SHUTTER_SPEED_MS = 50 #!< allow time for mechanical shutter to stabilize
53
31
  TEMPERATURE_CACHE_SEC = 1.0
54
32
 
33
+ DRIVER_NOT_INSTALLED = False
34
+
55
35
  def __init__(self, device_id, message_queue=None, alert_queue=None):
56
- # if passed a string representation of a DeviceID, deserialize it
57
- super().__init__()
58
- if type(device_id) is str:
59
- device_id = DeviceID(label=device_id)
36
+ super().__init__(device_id=device_id, message_queue=message_queue, alert_queue=alert_queue)
60
37
 
61
- self.device_id = device_id
62
- self.message_queue = message_queue
63
- self.alert_queue = alert_queue
38
+ if self.DRIVER_NOT_INSTALLED:
39
+ raise InterfaceDeviceClassUnavailable("Andor driver not installed")
64
40
 
65
41
  self.load_error_codes()
66
42
 
67
43
  self.connected = False
68
44
 
69
- # Receives ENLIGHTEN's 'change settings' commands in the spectrometer
70
- # process.
71
- self.command_queue = []
72
-
73
- self.immediate_mode = False
74
-
75
45
  # An AndorDevice has-a SpectrometerSettings, which has-a EEPROM, which
76
46
  # has-a MultiWavelengthCalibration
77
47
  self.settings = SpectrometerSettings(self.device_id)
@@ -119,8 +89,9 @@ class AndorDevice(InterfaceDevice):
119
89
  break
120
90
 
121
91
  if self.driver is None:
122
- log.error(f"could not find {filename} in search path: {dll_paths}")
123
- # MZ: interesting that we don't return here
92
+ # only need to display / log this once per session
93
+ self.DRIVER_NOT_INSTALLED = True
94
+ raise InterfaceDeviceClassUnavailable(f"could not find {filename} in search path: {dll_paths}")
124
95
 
125
96
  # "serial_number", "model" etc are ambiguous in an Andor configuration
126
97
  # file -- do they refer to the camera (Andor), or the spectrometer
@@ -135,8 +106,9 @@ class AndorDevice(InterfaceDevice):
135
106
  # (all but has_cooling can be overridden via config file)
136
107
 
137
108
  # Andor API doesn't(?) seem to have access to detector info.
138
- # Note that we use non-iDus cameras, including the Newton.
109
+ # Note that we use non-iDus cameras, including the Newton and iVac.
139
110
  self.settings.eeprom.detector = "iDus"
111
+ self.settings.eeprom.model = "WP-XL"
140
112
 
141
113
  self.settings.eeprom.has_cooling = True
142
114
  self.settings.eeprom.startup_integration_time_ms = 10
@@ -171,40 +143,39 @@ class AndorDevice(InterfaceDevice):
171
143
  except:
172
144
  log.error(f"failed to enqueue StatusMessage {msg}", exc_info=1)
173
145
 
174
- def not_implemented(self):
146
+ def not_implemented(self, arg):
175
147
  pass
176
148
 
177
149
  def _init_process_funcs(self):
178
150
  process_f = {}
179
151
 
180
- process_f["connect"] = self.connect
181
- process_f["acquire_data"] = self.acquire_data
182
- process_f["set_shutter_enable"] = self.set_shutter_enable
183
- process_f["set_integration_time_ms"] = self.set_integration_time_ms
184
- process_f["get_serial_number"] = self.get_serial_number
185
- process_f["init_tec_setpoint"] = self.init_tec_setpoint
186
- process_f["set_tec_setpoint"] = self.set_tec_setpoint
187
- process_f["init_detector_area"] = self.init_detector_area
188
- process_f["scans_to_average"] = self.set_scans_to_average
189
- process_f["high_gain_mode_enable"] = self.high_gain_mode_enable
190
- process_f["save_config"] = self.save_config
152
+ # setting and function have same name
153
+ for fn_name in [
154
+ "connect",
155
+ "acquire_data",
156
+ "set_shutter_open",
157
+ "set_integration_time_ms",
158
+ "get_serial_number",
159
+ "init_tec_setpoint",
160
+ "set_tec_setpoint",
161
+ "init_detector_area",
162
+ "scans_to_average",
163
+ "high_gain_mode_enable",
164
+ "save_config",
165
+ "set_vertical_binning",
166
+ "set_ingaas_correction_enable",
167
+ "set_etalon_correction_enable" ]:
168
+ process_f[fn_name] = getattr(self, fn_name)
169
+
170
+ # setting and function have different names
171
+ process_f["integration_time_ms"] = self.set_integration_time_ms
172
+ process_f["fan_enable"] = self.set_fan_enable
173
+ process_f["shutter_open"] = self.set_shutter_open
174
+ process_f["detector_tec_enable"] = self.set_detector_tec_enable
175
+ process_f["detector_tec_setpoint_degC"] = self.set_tec_setpoint
191
176
  process_f["vertical_binning"] = self.set_vertical_binning
192
177
  process_f["take_one_request"] = self.set_take_one_request
193
178
 
194
- process_f["reset_scan_averaging"] = self.not_implemented
195
- process_f["heartbeat"] = lambda x: None
196
-
197
- ##################################################################
198
- # What follows is the old init-lambdas that are squashed into process_f
199
- # Long term, the upstream requests should be changed to match the new format
200
- # This is an easy fix for the time being to make things behave
201
- ##################################################################
202
- process_f["integration_time_ms"] = lambda x: self.set_integration_time_ms(x)
203
- process_f["fan_enable"] = lambda x: self.set_fan_enable(bool(x))
204
- process_f["shutter_enable"] = lambda x: self.set_shutter_enable(bool(x))
205
- process_f["detector_tec_enable"] = lambda x: self.toggle_tec(bool(x))
206
- process_f["detector_tec_setpoint_degC"] = lambda x: self.set_tec_setpoint(int(round(x)))
207
-
208
179
  return process_f
209
180
 
210
181
  def high_gain_mode_enable(self, enabled):
@@ -219,9 +190,13 @@ class AndorDevice(InterfaceDevice):
219
190
  log.debug(f"for {enabled} setting gain to {self.gain_options[0]}")
220
191
  return
221
192
 
222
- def set_fan_enable(self, x):
223
- self.check_result(self.driver.SetFanMode(int(x)), f"Andor Fan On {x}")
224
- return SpectrometerResponse()
193
+ def set_fan_enable(self, flag):
194
+ flag = True if flag else False
195
+ try:
196
+ self.check_result(self.driver.SetFanMode(int(x)), f"Andor Fan On {x}")
197
+ except:
198
+ return SpectrometerResponse(False)
199
+ return SpectrometerResponse(True)
225
200
 
226
201
  def _get_default_data_dir(self):
227
202
  if os.name == "nt":
@@ -263,6 +238,12 @@ class AndorDevice(InterfaceDevice):
263
238
  if not spectrum.any():
264
239
  self._queue_message("marquee_error", "Andor camera is saturated")
265
240
 
241
+ if self.settings.ingaas_correction:
242
+ spectrum = self.settings.ingaas_correction.apply(spectrum)
243
+
244
+ if self.settings.etalon_correction:
245
+ spectrum = self.settings.etalon_correction.apply(spectrum)
246
+
266
247
  # log.debug(f"_get_spectrum_raw: returning spectrum {spectrum}")
267
248
  return spectrum
268
249
 
@@ -381,13 +362,29 @@ class AndorDevice(InterfaceDevice):
381
362
  return self.temperature_cache_value
382
363
 
383
364
  def _close_ex_shutter(self):
365
+ # SetShutterEx(typ, internalMode, closingTimeMS, openingTimeMS, externalMode)
366
+ # where:
367
+ # - typ:
368
+ # 0 = output TTL LOW to open shutter
369
+ # 1 = output TTL HIGH to open shutter
370
+ #
371
+ # - mode:
372
+ # 0 = Fully Auto
373
+ # 1 = Permanently Open
374
+ # 2 = Permanently Closed
375
+ # 3 = UNDEFINED
376
+ # 4 = Open for FVB series
377
+ # 5 = Open for any series
378
+
379
+ # set TTL HIGH="open", internalMode PermanentlyOpen, externalMode PermanentlyClosed
384
380
  self.check_result(self.driver.SetShutterEx(1, 1, self.SHUTTER_SPEED_MS, self.SHUTTER_SPEED_MS, 2), "SetShutterEx(2)")
385
- self.settings.state.shutter_enabled = False
381
+ self.settings.state.shutter_open = False
386
382
  return SpectrometerResponse(True)
387
383
 
388
384
  def _open_ex_shutter(self):
385
+ # set TTL HIGH="open", internalMode PermanentlyOpen, externalMode PermanentlyOpen
389
386
  self.check_result(self.driver.SetShutterEx(1, 1, self.SHUTTER_SPEED_MS, self.SHUTTER_SPEED_MS, 1), "SetShutterEx(1)")
390
- self.settings.state.shutter_enabled = True
387
+ self.settings.state.shutter_open = True
391
388
  return SpectrometerResponse(True)
392
389
 
393
390
  ###############################################################
@@ -453,8 +450,9 @@ class AndorDevice(InterfaceDevice):
453
450
  self.init_detector_speed() # step 12+13
454
451
 
455
452
  # step 14
453
+ # set internal shutter to PermanentlyOpen, external shutter to Automatic
456
454
  self.check_result(self.driver.SetShutterEx(1, 1, self.SHUTTER_SPEED_MS, self.SHUTTER_SPEED_MS, 0), "SetShutterEx(fully automatic external with internal always open)")
457
- self.settings.state.shutter_enabled = True
455
+ self.settings.state.shutter_open = True
458
456
 
459
457
  # step 15
460
458
  self.set_integration_time_ms(self.settings.eeprom.startup_integration_time_ms)
@@ -552,8 +550,9 @@ class AndorDevice(InterfaceDevice):
552
550
  """
553
551
  Loads configuration from file `self.config_file` and populates `self.settings.eeprom` with members.
554
552
  """
555
- f = open(self.config_file,)
556
- self.config_values = dict(json.load(f))
553
+ with open(self.config_file) as f:
554
+ self.config_values = json.load(f)
555
+
557
556
  log.debug(f"loaded {self.config_file}: {self.config_values}")
558
557
 
559
558
  # handle wp_ prefixes
@@ -562,6 +561,7 @@ class AndorDevice(InterfaceDevice):
562
561
  self.settings.eeprom.multi_wavelength_calibration.set(v, self.config_values[k])
563
562
 
564
563
  # same spelling
564
+ # consider just using self.settings.eeprom.fields.keys()
565
565
  for k in [ 'model',
566
566
  'stubbed',
567
567
  'detector',
@@ -589,17 +589,20 @@ class AndorDevice(InterfaceDevice):
589
589
  if 'startup_temp_degC' in self.config_values:
590
590
  self.set_tec_setpoint(self.settings.eeprom.startup_temp_degC)
591
591
 
592
+ # pick up Pixel Corrections like EtalonCorrection, InGaAsCorrection etc
593
+ self.settings.augment_from_json_data(data)
594
+
592
595
  def acquire_data(self):
593
596
  # handle TakeOneRequest.take_dark
594
597
  dark_reading = None
595
598
  tor = self.take_one_request
596
599
  log.debug(f"acquire_data: tor {tor}")
597
600
  if tor and tor.take_dark:
598
- self.set_shutter_enable(True)
601
+ self.set_shutter_open(False)
599
602
  dark_reading = self._take_one_averaged_reading()
600
- if dark is None:
603
+ if dark_reading is None:
601
604
  return SpectrometerResponse(False, error_msg="failed to collect dark")
602
- self.set_shutter_enable(False)
605
+ self.set_shutter_open(True)
603
606
 
604
607
  # get spectrum (potentially averaged)
605
608
  reading = self._take_one_averaged_reading()
@@ -639,8 +642,8 @@ class AndorDevice(InterfaceDevice):
639
642
  log.debug(f"acquire_data: reading {reading}")
640
643
  return SpectrometerResponse(data=reading)
641
644
 
642
- def set_shutter_enable(self, enable):
643
- if enable:
645
+ def set_shutter_open(self, flag):
646
+ if flag:
644
647
  return self._open_ex_shutter()
645
648
  else:
646
649
  return self._close_ex_shutter()
@@ -688,16 +691,17 @@ class AndorDevice(InterfaceDevice):
688
691
 
689
692
  return SpectrometerResponse(True)
690
693
 
691
- def toggle_tec(self, toggle_state):
692
- c_toggle = c_int(toggle_state)
693
- self.tec_enabled = c_toggle.value
694
- if self.tec_enabled:
694
+ def set_detector_tec_enable(self, flag):
695
+ flag = True if flag else False
696
+ self.tec_enabled = flag
697
+ if flag:
695
698
  self.check_result(self.driver.CoolerON(), "CoolerON")
696
699
  else:
697
700
  self.check_result(self.driver.CoolerOFF(), "CoolerOFF")
698
701
  return SpectrometerResponse(True)
699
702
 
700
703
  def set_tec_setpoint(self, set_temp):
704
+ set_temp = int(round(set_temp, 0))
701
705
  if set_temp < self.settings.eeprom.min_temp_degC or set_temp > self.settings.eeprom.max_temp_degC:
702
706
  log.error(f"requested temp of {set_temp}, but it is outside range ({self.settings.eeprom.min_temp_degC}C, {self.settings.eeprom.max_temp_degC}C)")
703
707
  return
@@ -768,6 +772,14 @@ class AndorDevice(InterfaceDevice):
768
772
  log.debug(f"set AD channel {ADnumber} with horizontal speed {HSnumber} ({STemp})")
769
773
  return SpectrometerResponse(True)
770
774
 
775
+ def set_ingaas_correction_enable(self, flag):
776
+ if self.settings.ingaas_correction:
777
+ self.settings.ingaas_correction.enable = flag
778
+
779
+ def set_etalon_correction_enable(self, flag):
780
+ if self.settings.etalon_correction:
781
+ self.settings.etalon_correction.enable = flag
782
+
771
783
  def set_scans_to_average(self, value):
772
784
  value = int(value)
773
785
 
@@ -0,0 +1,56 @@
1
+ import json
2
+ from . import utils
3
+
4
+ class AssemblyRevision(dict):
5
+ """
6
+ Making this a standalone class because might end up using from both FID (USB)
7
+ and BLEDevice.
8
+ """
9
+ def __init__(self, data=None):
10
+ dict.__init__(self) # https://stackoverflow.com/a/31207881
11
+ self.partnumber = None
12
+ self.variant = None
13
+ self.revision = None
14
+
15
+ if isinstance(data, str):
16
+ data = utils.hex_string_to_data(data)
17
+
18
+ self.parse_data(data)
19
+
20
+ def parse_data(self, data):
21
+ """
22
+ This is based on the data structure defined in ENG-0120 Rev 10.
23
+ """
24
+ if data is None or len(data) != 6:
25
+ log.error(f"parse_data: invalid length {data}")
26
+ return
27
+
28
+ self.partnumber = (data[0] << 8) + data[1] + 140_000
29
+ self.variant = (data[2] << 8) + data[3]
30
+ self.revision = (data[4] << 8) + data[5]
31
+
32
+ def serialize(self):
33
+ """ probably a simpler way to do this with struct """
34
+ buf = [ 0 ] * 6
35
+ if self.partnumber:
36
+ n = self.partnumber - 140_000
37
+ buf[0] = (n >> 8) & 0xff
38
+ buf[1] = n & 0xff
39
+ if self.variant:
40
+ n = self.variant
41
+ buf[2] = (n >> 8) & 0xff
42
+ buf[3] = n & 0xff
43
+ if self.revision:
44
+ n = self.revision
45
+ buf[4] = (n >> 8) & 0xff
46
+ buf[5] = n & 0xff
47
+
48
+ def __repr__(self):
49
+ s = ""
50
+ if self.partnumber and self.partnumber != 140_000:
51
+ s += f"{self.partnumber}"
52
+ if self.variant:
53
+ s += f"-{self.variant}"
54
+ if self.revision:
55
+ s += f" Rev{self.revision}"
56
+ return s