wasatch 2.3.6__tar.gz → 2.3.8__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 (94) hide show
  1. {wasatch-2.3.6 → wasatch-2.3.8}/CHANGELOG.md +12 -0
  2. {wasatch-2.3.6 → wasatch-2.3.8}/PKG-INFO +1 -1
  3. wasatch-2.3.8/demo-ids-raman.py +281 -0
  4. {wasatch-2.3.6 → wasatch-2.3.8}/pyproject.toml +1 -1
  5. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/AndorDevice.py +39 -32
  6. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/AreaScanImage.py +1 -1
  7. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/EEPROM.py +11 -0
  8. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/FeatureIdentificationDevice.py +27 -18
  9. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/IDSCamera.py +40 -11
  10. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/IDSDevice.py +17 -11
  11. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/Reading.py +1 -0
  12. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/SpectrometerSettings.py +4 -0
  13. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/TCPDevice.py +24 -7
  14. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/WasatchDevice.py +44 -33
  15. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/WasatchDeviceWrapper.py +1 -0
  16. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/WrapperWorker.py +10 -6
  17. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/__init__.py +1 -1
  18. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/applog.py +1 -1
  19. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/utils.py +1 -1
  20. {wasatch-2.3.6 → wasatch-2.3.8}/.example_pypirc +0 -0
  21. {wasatch-2.3.6 → wasatch-2.3.8}/.flake8 +0 -0
  22. {wasatch-2.3.6 → wasatch-2.3.8}/.gitignore +0 -0
  23. {wasatch-2.3.6 → wasatch-2.3.8}/.pylintrc +0 -0
  24. {wasatch-2.3.6 → wasatch-2.3.8}/Doxyfile +0 -0
  25. {wasatch-2.3.6 → wasatch-2.3.8}/LICENSE +0 -0
  26. {wasatch-2.3.6 → wasatch-2.3.8}/MANIFEST.in +0 -0
  27. {wasatch-2.3.6 → wasatch-2.3.8}/Makefile +0 -0
  28. {wasatch-2.3.6 → wasatch-2.3.8}/README.md +0 -0
  29. {wasatch-2.3.6 → wasatch-2.3.8}/README_PYPI.md +0 -0
  30. {wasatch-2.3.6 → wasatch-2.3.8}/README_RPI.md +0 -0
  31. {wasatch-2.3.6 → wasatch-2.3.8}/README_SETTINGS.md +0 -0
  32. {wasatch-2.3.6 → wasatch-2.3.8}/README_VIRTUAL.md +0 -0
  33. {wasatch-2.3.6 → wasatch-2.3.8}/WasatchShell/README.md +0 -0
  34. {wasatch-2.3.6 → wasatch-2.3.8}/WasatchShell/laser-test.py +0 -0
  35. {wasatch-2.3.6 → wasatch-2.3.8}/WasatchShell/load-test.py +0 -0
  36. {wasatch-2.3.6 → wasatch-2.3.8}/WasatchShell/one-shot.py +0 -0
  37. {wasatch-2.3.6 → wasatch-2.3.8}/WasatchShell/wasatch-shell.py +0 -0
  38. {wasatch-2.3.6 → wasatch-2.3.8}/demo-auto-raman.py +0 -0
  39. {wasatch-2.3.6 → wasatch-2.3.8}/demo-simple.py +0 -0
  40. {wasatch-2.3.6 → wasatch-2.3.8}/demo-virtual.py +0 -0
  41. {wasatch-2.3.6 → wasatch-2.3.8}/demo-workflow.py +0 -0
  42. {wasatch-2.3.6 → wasatch-2.3.8}/demo.py +0 -0
  43. {wasatch-2.3.6 → wasatch-2.3.8}/environments/conda-linux.yml +0 -0
  44. {wasatch-2.3.6 → wasatch-2.3.8}/environments/conda-macos.yml +0 -0
  45. {wasatch-2.3.6 → wasatch-2.3.8}/environments/conda-rpi.yml +0 -0
  46. {wasatch-2.3.6 → wasatch-2.3.8}/environments/conda-win10.yml +0 -0
  47. {wasatch-2.3.6 → wasatch-2.3.8}/screenshots/multiplatform.png +0 -0
  48. {wasatch-2.3.6 → wasatch-2.3.8}/scripts/dark-vs-integ.py +0 -0
  49. {wasatch-2.3.6 → wasatch-2.3.8}/scripts/deploy +0 -0
  50. {wasatch-2.3.6 → wasatch-2.3.8}/testSpectrometers/SiG_785/eeprom/EEPROM-EM-9c65d19f4c.json +0 -0
  51. {wasatch-2.3.6 → wasatch-2.3.8}/testSpectrometers/SiG_785/readings/enlighten-20210329-094722-799520-WP-00686.csv +0 -0
  52. {wasatch-2.3.6 → wasatch-2.3.8}/testSpectrometers/SiG_785/readings/enlighten-20210329-094755-288958-WP-00686.csv +0 -0
  53. {wasatch-2.3.6 → wasatch-2.3.8}/testSpectrometers/WP-00887/eeprom/WP-00887-mock.json +0 -0
  54. {wasatch-2.3.6 → wasatch-2.3.8}/testSpectrometers/WP-00904/eeprom/WP-00904-mock.json +0 -0
  55. {wasatch-2.3.6 → wasatch-2.3.8}/testSpectrometers/WP-00913/eeprom/WP-00913-mock.json +0 -0
  56. {wasatch-2.3.6 → wasatch-2.3.8}/tests/existing_implementations.py +0 -0
  57. {wasatch-2.3.6 → wasatch-2.3.8}/tests/noise-by-setpoint.py +0 -0
  58. {wasatch-2.3.6 → wasatch-2.3.8}/tests/queues.py +0 -0
  59. {wasatch-2.3.6 → wasatch-2.3.8}/tests/unit.py +0 -0
  60. {wasatch-2.3.6 → wasatch-2.3.8}/udev/10-wasatch.rules +0 -0
  61. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/AbstractUSBDevice.py +0 -0
  62. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/AutoRaman.py +0 -0
  63. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/AutoRamanRequest.py +0 -0
  64. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/BLEDevice.py +0 -0
  65. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/BalanceAcquisition.py +0 -0
  66. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/CSVLoader.py +0 -0
  67. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/CommandSettings.py +0 -0
  68. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/ControlObject.py +0 -0
  69. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/DetectorROI.py +0 -0
  70. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/DetectorRegions.py +0 -0
  71. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/DeviceFinderBLE.py +0 -0
  72. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/DeviceFinderTCP.py +0 -0
  73. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/DeviceFinderUSB.py +0 -0
  74. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/DeviceID.py +0 -0
  75. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/EEPROM_multical.sav +0 -0
  76. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/FPGAOptions.py +0 -0
  77. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/FirmwareRequirements.py +0 -0
  78. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/HardwareInfo.py +0 -0
  79. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/IMX385.py +0 -0
  80. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/InterfaceDevice.py +0 -0
  81. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/MockUSBDevice.py +0 -0
  82. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/OceanDevice.py +0 -0
  83. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/PollStatus.py +0 -0
  84. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/ProcessedReading.py +0 -0
  85. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/ROI.py +0 -0
  86. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/RealUSBDevice.py +0 -0
  87. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/SPIDevice.py +0 -0
  88. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/SpectrometerRequest.py +0 -0
  89. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/SpectrometerResponse.py +0 -0
  90. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/SpectrometerState.py +0 -0
  91. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/StatusMessage.py +0 -0
  92. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/TakeOneRequest.py +0 -0
  93. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/WasatchBus.py +0 -0
  94. {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/simulation_protocol.py +0 -0
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ - 2026-01-12 2.3.8
4
+ - added settings.has_detector
5
+ - improved support for Andor .json "virtual EEPROMs"
6
+ - bump internal WrapperWorker poll rate from 20Hz to 200Hz
7
+ - retain debug logging for 20sec after connection (vs previous 10sec)
8
+ - only update Andor detector temperature at 1Hz
9
+ - improved support for using 220250 boards as standalone laser drivers w/o
10
+ detectors (added Reading.keep_alive to indicate metadata but no spectra)
11
+ - 2025-12-10 2.3.7
12
+ - added EEPROM.is_oem
13
+ - fixed TCPDevice.set_detector_gain and set_vertical_roi
14
+ - disable use of eeprom.actual_pixels_horizontal during acquisition
3
15
  - 2025-11-06 2.3.6
4
16
  - resolve hotplug race condition when laser power set in percent
5
17
  - cache certain device getters to avoid I2C overruns
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wasatch
3
- Version: 2.3.6
3
+ Version: 2.3.8
4
4
  Summary: Application-level driver for Wasatch Photonics spectrometers
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env python
2
+ ################################################################################
3
+ # demo-ids-raman.py #
4
+ ################################################################################
5
+ # #
6
+ # DESCRIPTION: Simple cmd-line demo to control an XS spectrometer using IDS #
7
+ # detector and 220250 Rev4 laser driver. #
8
+ # #
9
+ # NOTES: This is for the fairly unusual case of having an XS bench built with #
10
+ # an IDS camera (providing its own USB3 port for comms and control), #
11
+ # plus a 220250 Rev4 PCB for use as a standalone laser driver (but w/o #
12
+ # an IMX385 detector connected). #
13
+ # #
14
+ ################################################################################
15
+
16
+ # DEPENDENCIES: pywin32, psutil, seabreeze, pyftdi, crcmod, bleak
17
+
18
+ import os
19
+ import re
20
+ import sys
21
+ import time
22
+ import numpy as np
23
+ import signal
24
+ import logging
25
+ import argparse
26
+
27
+ from datetime import datetime
28
+ from time import sleep
29
+
30
+ import wasatch
31
+ from wasatch import applog
32
+ from wasatch.DeviceID import DeviceID
33
+ from wasatch.IDSDevice import IDSDevice
34
+ from wasatch.WasatchBus import WasatchBus
35
+ from wasatch.WasatchDevice import WasatchDevice
36
+
37
+ log = logging.getLogger(__name__)
38
+ demo = None
39
+
40
+ class WasatchDemo:
41
+
42
+ def __init__(self):
43
+ self.camera_device = None # a wasatch.IDSDevice wrapping a wasatch.IDSCamera (as 'camera')
44
+ self.laser_device = None # a wasatch.WasatchDevice wrapping a wasatch.FeatureIdentificationDevice (as 'hardware')
45
+ self.reading_count = 0
46
+ self.exiting = False
47
+
48
+ self.args = self.parse_args()
49
+
50
+ print(f"Wasatch.PY {wasatch.__version__} IDS Raman Demo")
51
+
52
+ def parse_args(self):
53
+ parser = argparse.ArgumentParser(description="Simple demo to acquire spectra from command-line interface")
54
+ parser.add_argument("--log-level", type=str, default="INFO", help="logging level", choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL","NEVER"])
55
+ parser.add_argument("--integration-time-ms", type=int, default=1000, help="integration time (ms, default 10)")
56
+ parser.add_argument("--take-dark", action="store_true", help="first measurement is used for dark subtraction")
57
+ parser.add_argument("--enable-laser", action="store_true", help="fire laser (after optional dark)")
58
+ parser.add_argument("--max", type=int, default=0, help="max spectra to acquire (default 0, unlimited)")
59
+ parser.add_argument("--save-spectra", action="store_true", help="save vertically-binned spectra to row-ordered CSV")
60
+ parser.add_argument("--save-png", action="store_true", help="save PNG of each image frame")
61
+ parser.add_argument("--save-data", action="store_true", help="save 2D array of each image frame")
62
+ parser.add_argument("--save-dir", type=str, default=".", help="directory to save files")
63
+ parser.add_argument("--prefix", type=str, default="ids-raman", help="filename prefix")
64
+
65
+ args = parser.parse_args()
66
+ applog.MainLogger(args.log_level)
67
+
68
+ return args
69
+
70
+ def connect(self):
71
+
72
+ ########################################################################
73
+ # Look for laser driver board
74
+ ########################################################################
75
+
76
+ bus = WasatchBus()
77
+ if not bus.device_ids:
78
+ print("No Wasatch USB spectrometers found.")
79
+ return
80
+
81
+ # look for laser driver on WasatchBus -- we won't see the camera here,
82
+ # because IDSPeak devices are not automatically considered for inclusion
83
+ # by WasatchBus (although they could be)
84
+ for device_id in bus.device_ids:
85
+ log.debug("connect: trying to connect to %s", device_id)
86
+ device = WasatchDevice(device_id)
87
+ ok = device.connect()
88
+ if not ok:
89
+ log.critical("connect: can't connect to %s", device_id)
90
+ continue
91
+
92
+ log.debug(f"connect: connected to {device_id} ({device})")
93
+ if device.settings is None:
94
+ log.error("can't have device without SpectrometerSettings")
95
+ continue
96
+
97
+ print(f"connected to {device.settings.full_model()} {device.settings.eeprom.serial_number} with {device.settings.pixels()} pixels " +
98
+ f"from ({device.settings.wavelengths[0]:.2f}, {device.settings.wavelengths[-1]:.2f}nm)" +
99
+ ( f" ({device.settings.wavenumbers[0]:.2f}, {device.settings.wavenumbers[-1]:.2f}cm⁻¹)" if device.settings.has_excitation() else "") +
100
+ f" (Microcontroller {device.settings.microcontroller_firmware_version}," +
101
+ f" FPGA {device.settings.fpga_firmware_version})")
102
+
103
+ self.laser_device = device
104
+ break
105
+
106
+ if self.laser_device is None:
107
+ log.critical("could not find laser driver, exitting")
108
+ return
109
+
110
+ ########################################################################
111
+ # now look for an IDS camera
112
+ ########################################################################
113
+
114
+ try:
115
+ device_id = DeviceID(label="IDSPeak")
116
+ device = IDSDevice(device_id=device_id, scratch_dir=".", consumer_deletes_area_scan_image=True)
117
+ if device.connect():
118
+ self.camera_device = device
119
+ else:
120
+ log.error("failed connecting to IDSCamera")
121
+ except:
122
+ log.critical("exception connecting to IDS camera", exc_info=1)
123
+ return
124
+
125
+ if self.camera_device is None:
126
+ log.critical("could not find IDS camera, exitting")
127
+ return
128
+
129
+ device = self.camera_device
130
+ camera = device.camera
131
+ settings = device.settings
132
+ eeprom = settings.eeprom
133
+
134
+ device.set_area_scan_enable(True)
135
+
136
+ # copy the laser excitation over to the camera "virtual EEPROM" so we can
137
+ # compute wavenumbers
138
+ eeprom.excitation_nm = self.laser_device.settings.eeprom.excitation_nm
139
+ settings.update_wavecal()
140
+ print(f"connected to IDSPeak camera {eeprom.model} {eeprom.serial_number} with {camera.width}x{camera.height} pixels " +
141
+ f"from ({settings.wavelengths[0]:.2f}, {settings.wavelengths[-1]:.2f}nm)" +
142
+ ( f" ({settings.wavenumbers[0]:.2f}, {settings.wavenumbers[-1]:.2f}cm⁻¹)" if settings.has_excitation() else ""))
143
+
144
+ return True
145
+
146
+ ############################################################################
147
+ # #
148
+ # Run-Time Loop #
149
+ # #
150
+ ############################################################################
151
+
152
+ def run(self):
153
+ self.camera_device.set_integration_time_ms(self.args.integration_time_ms)
154
+
155
+ # initialize outfile if saving spectra
156
+ outfile = None
157
+ if self.args.save_spectra:
158
+ filename = os.path.join(self.args.save_dir, self.args.prefix + "-spectra.csv")
159
+ outfile = open(filename, "w")
160
+ outfile.write("pixels, 0, " + ", ".join([f"{x}" for x in range(self.camera_device.settings.pixels())]) + "\n")
161
+ outfile.write("wavelengths, 0, " + ", ".join([f"{x:.2f}" for x in self.camera_device.settings.wavelengths]) + "\n")
162
+ outfile.write("wavenumbers, 0, " + ", ".join([f"{x:.2f}" for x in self.camera_device.settings.wavenumbers]) + "\n")
163
+ outfile.write("time, count, spectrum\n")
164
+
165
+ # read spectra until user presses Control-Break
166
+ while not self.exiting and (self.args.max == 0 or self.reading_count < self.args.max):
167
+
168
+ ####################################################################
169
+ # check to fire laser
170
+ ####################################################################
171
+
172
+ # on the first reading (or the second, if taking dark), enable the
173
+ # laser if requested
174
+ if (self.args.enable_laser and (self.reading_count == 0 or (self.args.take_dark and self.reading_count == 1))):
175
+ print("enabling laser")
176
+ self.laser_device.hardware.set_laser_enable(True)
177
+
178
+ ####################################################################
179
+ # read image
180
+ ####################################################################
181
+
182
+ if self.exiting:
183
+ break
184
+
185
+ response = self.camera_device.acquire_data()
186
+ if response.keep_alive:
187
+ log.debug(f"ignoring keepalive from {device}")
188
+ continue
189
+
190
+ if isinstance(response.data, bool):
191
+ if response.data:
192
+ log.debug("received poison-pill, exiting")
193
+ break
194
+ else:
195
+ log.debug("no reading available")
196
+ continue
197
+
198
+ if response.data.failure:
199
+ log.debug("reading indicated failure")
200
+ break
201
+
202
+ if self.exiting:
203
+ break
204
+
205
+ self.reading_count += 1
206
+
207
+ reading = response.data
208
+ spectrum = reading.spectrum
209
+ settings = self.camera_device.settings
210
+ asi = reading.area_scan_image
211
+ ts = reading.timestamp.strftime("%Y%m%d-%H%M%S-%f")
212
+
213
+ # print some stats as we go
214
+ spectrum_min = np.amin(spectrum)
215
+ spectrum_max = np.amax(spectrum)
216
+ spectrum_avg = np.mean(spectrum)
217
+ spectrum_std = np.std (spectrum)
218
+ print(f"{ts}: {self.reading_count:4d} : min {spectrum_min:8.2f}, max {spectrum_max:8.2f}, avg {spectrum_avg:8.2f}, stdev {spectrum_std:8.2f}")
219
+
220
+ # append to row-ordered file
221
+ if outfile:
222
+ values = ",".join(format(x, ".2f") for x in spectrum)
223
+ outfile.write(f"{reading.timestamp}, {self.reading_count}, {values}\n")
224
+
225
+ # save PNG
226
+ if self.args.save_png and asi.pathname_png:
227
+ filename = os.path.join(self.args.save_dir, f"{self.args.prefix}-{self.reading_count:04d}.png")
228
+ log.debug(f"renaming {asi.pathname_png} -> {filename}")
229
+ os.replace(asi.pathname_png, filename)
230
+ print(f"\tsaved {filename}")
231
+
232
+ # save area scan data
233
+ if self.args.save_data and asi.data is not None:
234
+ filename = os.path.join(self.args.save_dir, f"{self.args.prefix}-{self.reading_count:04d}.csv")
235
+ np.savetxt(filename, asi.data, fmt='%d', delimiter=",")
236
+ print(f"\tsaved {filename}")
237
+
238
+ if self.args.take_dark and self.reading_count == 1:
239
+ print("\tsaving dark")
240
+ self.camera_device.camera.set_dark_asi(asi)
241
+
242
+ ################################################################################
243
+ # main()
244
+ ################################################################################
245
+
246
+ def signal_handler(signal, frame):
247
+ print('\rInterrupted by Ctrl-C...shutting down', end=' ')
248
+ shutdown()
249
+
250
+ def shutdown():
251
+ if demo:
252
+ demo.exiting = True
253
+ if demo.laser_device is not None:
254
+ demo.laser_device.hardware.set_laser_enable(False)
255
+ demo.laser_device.disconnect()
256
+
257
+ if demo.camera_device is not None:
258
+ demo.camera_device.disconnect()
259
+ sleep(1)
260
+
261
+ log.debug(None)
262
+ applog.explicit_log_close()
263
+ sys.exit()
264
+
265
+ if __name__ == "__main__":
266
+ # add a signal-handler to catch ctrl-C interrupts, so we can cleanly close
267
+ # down all connected devices
268
+ signal.signal(signal.SIGINT, signal_handler)
269
+
270
+ demo = WasatchDemo()
271
+ if demo.connect():
272
+ # Note that on Windows, Control-Break (SIGBREAK) differs from
273
+ # Control-C (SIGINT); see https://stackoverflow.com/a/1364199
274
+ log.debug("Press Control-Break to interrupt...")
275
+ try:
276
+ demo.run()
277
+ except:
278
+ # catch exception so shutdown will run and disable laser
279
+ log.error("Caught exception during demo", exc_info=1)
280
+
281
+ shutdown()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wasatch"
3
- version = "2.3.6"
3
+ version = "2.3.8"
4
4
  description= "Application-level driver for Wasatch Photonics spectrometers"
5
5
  requires-python = ">=3.9"
6
6
  readme = "README.md"
@@ -4,7 +4,7 @@ import json
4
4
  import numpy as np
5
5
  import struct
6
6
  import logging
7
- import datetime
7
+ from datetime import datetime
8
8
 
9
9
  from ctypes import *
10
10
 
@@ -37,11 +37,11 @@ class AndorDevice(InterfaceDevice):
37
37
  |
38
38
  handle_requests
39
39
  |
40
- ------------
41
- / / | \ \
40
+ -----------
41
+ | | | | |
42
42
  { get_laser status, acquire, set_laser_watchdog, etc....}
43
- \ \ | / /
44
- ------------
43
+ | | | | |
44
+ -----------
45
45
  |
46
46
  {self.driver.some_andor_sdk_call}
47
47
  @endverbatim
@@ -50,6 +50,7 @@ class AndorDevice(InterfaceDevice):
50
50
 
51
51
  SUCCESS = 20002 #!< see load_error_codes()
52
52
  SHUTTER_SPEED_MS = 50 #!< allow time for mechanical shutter to stabilize
53
+ TEMPERATURE_CACHE_SEC = 1.0
53
54
 
54
55
  def __init__(self, device_id, message_queue=None, alert_queue=None):
55
56
  # if passed a string representation of a DeviceID, deserialize it
@@ -83,13 +84,16 @@ class AndorDevice(InterfaceDevice):
83
84
  self.driver = None
84
85
 
85
86
  self.process_id = os.getpid()
86
- self.last_memory_check = datetime.datetime.now()
87
+ self.last_memory_check = datetime.now()
87
88
  self.last_battery_percentage = 0
88
89
  self.spec_index = 0
89
90
  self._scan_averaging = 1
90
91
  self.dark = None
91
92
  self.boxcar_half_width = 0
92
93
 
94
+ self.temperature_cache_value = None
95
+ self.temperature_cache_timestamp = None
96
+
93
97
  # decide appropriate DLL filename for architecture
94
98
  arch = 64 if 64 == struct.calcsize("P") * 8 else 32
95
99
  filename = f"atmcd{arch}d.dll"
@@ -344,34 +348,37 @@ class AndorDevice(InterfaceDevice):
344
348
 
345
349
  def get_detector_temperature_degC(self):
346
350
  if self.tec_enabled:
347
- log.debug("TEC enabled, so reading temperature")
348
-
349
- use_float = True # seems to work on 785XL (WP-01635 and WP-01491)
350
- if use_float:
351
- c_temp = c_float()
352
- result = self.driver.GetTemperatureF(byref(c_temp))
353
- else:
354
- c_temp = c_int()
355
- result = self.driver.GetTemperature(byref(c_temp))
356
-
357
- # MZ: why were we trying to use GetTemperatureF(float) here, when above it was GetTemperature(int)?
358
- # temperature = c_float()
359
- # temp_success = self.driver.GetTemperatureF(byref(temperature))
360
- # reading.detector_temperature_degC = temperature.value
361
-
362
- label = self.get_error_code(result)
363
-
364
- if label in [ "DRV_SUCCESS",
365
- "DRV_TEMPERATURE_DRIFT",
366
- "DRV_TEMPERATURE_STABILIZED",
367
- "DRV_TEMPERATURE_NOT_REACHED",
368
- "DRV_TEMPERATURE_NOT_STABILIZED" ]:
369
- detector_temperature_degC = c_temp.value
370
- log.debug(f"Andor temperature {detector_temperature_degC:.2f} ({label})")
371
- return detector_temperature_degC
351
+ log.debug("TEC enabled, so including temperature")
352
+
353
+ now = datetime.now()
354
+ if self.temperature_cache_timestamp is None or self.temperature_cache_value is None or (now - self.temperature_cache_timestamp).total_seconds() >= self.TEMPERATURE_CACHE_SEC:
355
+ # read new temperature
356
+ use_float = True # seems to work on 785XL (WP-01635 and WP-01491)
357
+ if use_float:
358
+ c_temp = c_float()
359
+ result = self.driver.GetTemperatureF(byref(c_temp))
360
+ else:
361
+ c_temp = c_int()
362
+ result = self.driver.GetTemperature(byref(c_temp))
363
+
364
+ label = self.get_error_code(result)
365
+ if label in [ "DRV_SUCCESS",
366
+ "DRV_TEMPERATURE_DRIFT",
367
+ "DRV_TEMPERATURE_STABILIZED",
368
+ "DRV_TEMPERATURE_NOT_REACHED",
369
+ "DRV_TEMPERATURE_NOT_STABILIZED" ]:
370
+ self.temperature_cache_value = c_temp.value
371
+ self.temperature_cache_timestamp = now
372
+ log.debug(f"Andor temperature {self.temperature_cache_value:.2f} ({label})")
373
+ else:
374
+ log.error(f"unable to read detector temperature, result was {label}")
375
+ self.temperature_cache_value = None
376
+ self.temperature_cache_timestamp = None
372
377
  else:
373
- log.error(f"unable to read detector temperature, result was {label}")
378
+ # use cached value
379
+ pass
374
380
 
381
+ return self.temperature_cache_value
375
382
 
376
383
  def _close_ex_shutter(self):
377
384
  self.check_result(self.driver.SetShutterEx(1, 1, self.SHUTTER_SPEED_MS, self.SHUTTER_SPEED_MS, 2), "SetShutterEx(2)")
@@ -16,4 +16,4 @@ class AreaScanImage:
16
16
  AreaScanImage.FRAME_COUNT += 1
17
17
 
18
18
  def __repr__(self):
19
- return f"AreaScanImage<frame {self.frame_count}, line_index {line_index}, width {self.width} (orig {self.width_orig}), height {self.height} (orig {self.height_orig}), format_name {self.format_name}, pathname_png {self.pathname_png}>"
19
+ return f"AreaScanImage<frame {self.frame_count}, line_index {self.line_index}, width {self.width} (orig {self.width_orig}), height {self.height} (orig {self.height_orig}), format_name {self.format_name}, pathname_png {self.pathname_png}>"
@@ -123,6 +123,8 @@ class EEPROM:
123
123
  self.sig_laser_tec = False # better: sig_sml_laser_tec
124
124
  self.has_interlock_feedback = False
125
125
  self.laser_interlock_excluded = False # True if the unit has a laser, but the normal safety interlock has been removed or otherwise disabled (OEM-only)
126
+ self.laser_timeout_after_count = False
127
+ self.is_oem = False
126
128
  self.has_shutter = False
127
129
  self.disable_ble_power = False
128
130
  self.disable_laser_armed_indicator = False
@@ -506,6 +508,8 @@ class EEPROM:
506
508
  self.disable_ble_power = 0 != self.feature_mask & 0x0100
507
509
  self.disable_laser_armed_indicator = 0 != self.feature_mask & 0x0200
508
510
  self.laser_interlock_excluded = 0 != self.feature_mask & 0x0400
511
+ self.laser_timeout_after_count = 0 != self.feature_mask & 0x0800
512
+ self.is_oem = 0 != self.feature_mask & 0x1000
509
513
  else:
510
514
  self.invert_x_axis = False
511
515
  self.horiz_binning_enabled = False
@@ -517,6 +521,9 @@ class EEPROM:
517
521
  self.has_shutter = False
518
522
  self.disable_ble_power = False
519
523
  self.disable_laser_armed_indicator = False
524
+ self.laser_interlock_excluded = False
525
+ self.laser_timeout_after_count = False
526
+ self.is_oem = False
520
527
 
521
528
  # ######################################################################
522
529
  # sanity checks
@@ -562,6 +569,8 @@ class EEPROM:
562
569
  mask |= 0x0100 if self.disable_ble_power else 0
563
570
  mask |= 0x0200 if self.disable_laser_armed_indicator else 0
564
571
  mask |= 0x0400 if self.laser_interlock_excluded else 0
572
+ mask |= 0x0800 if self.laser_timeout_after_count else 0
573
+ mask |= 0x1000 if self.is_oem else 0
565
574
  return mask
566
575
 
567
576
  ##
@@ -860,6 +869,8 @@ class EEPROM:
860
869
  else:
861
870
  buf[start_byte + i] = 0
862
871
  else:
872
+ if data_type == "f":
873
+ value = float(value)
863
874
  struct.pack_into(data_type, buf, start_byte, value)
864
875
 
865
876
  if False:
@@ -66,11 +66,11 @@ class FeatureIdentificationDevice(InterfaceDevice):
66
66
  |
67
67
  handle_requests
68
68
  |
69
- ------------
70
- / / | \ \
69
+ -----------
70
+ | | | | |
71
71
  { get_laser status, acquire, set_laser_watchdog, etc....}
72
- \ \ | / /
73
- ------------
72
+ | | | | |
73
+ -----------
74
74
  |
75
75
  _send_code
76
76
  @endverbatim
@@ -185,14 +185,14 @@ class FeatureIdentificationDevice(InterfaceDevice):
185
185
  # it doesn't know what PIDs it might be looking for. We know, so just
186
186
  # narrow down the search to those devices.
187
187
 
188
- log.info(f"FID.connect: asked to connect to device_type {self.device_type}")
189
- log.info(f"FID.connect: calling device_type.find, looking for VID 0x{self.device_id.vid:04x} and PID 0x{self.device_id.pid:04x}")
188
+ log.debug(f"FID.connect: asked to connect to device_type {self.device_type}")
189
+ log.debug(f"FID.connect: calling device_type.find, looking for VID 0x{self.device_id.vid:04x} and PID 0x{self.device_id.pid:04x}")
190
190
  devices = self.device_type.find(find_all=True, idVendor=self.device_id.vid, idProduct=self.device_id.pid)
191
- log.info(f"FID.connect: found devices {devices}")
191
+ log.debug(f"FID.connect: found devices {devices}")
192
192
  dev_list = list(devices) # convert from array
193
193
 
194
194
  device = None
195
- log.info(f"searching for specified device in dev_list {dev_list}")
195
+ log.debug(f"searching for specified device in dev_list {dev_list}")
196
196
  for dev in dev_list:
197
197
  if dev.bus != self.device_id.bus:
198
198
  log.debug("FID.connect: rejecting device (bus %d != requested %d)", dev.bus, self.device_id.bus)
@@ -475,7 +475,7 @@ class FeatureIdentificationDevice(InterfaceDevice):
475
475
 
476
476
  self.connected = False
477
477
 
478
- log.critical("fid.disconnect: releasing interface")
478
+ log.debug("fid.disconnect: releasing interface")
479
479
  try:
480
480
  #result = self.device_type.release_interface(self.device, 0)
481
481
  try:
@@ -520,7 +520,7 @@ class FeatureIdentificationDevice(InterfaceDevice):
520
520
  # I had to look at the full configuration string and saw that the way to get the instance id was the following
521
521
  # using _try_get_string https://github.com/pyusb/pyusb/blob/master/usb/core.py#L1221
522
522
  # so techincally we shouldn't do this by the _ meaning it should be private, but it's the only way I see
523
- device_instance_id = f'USB\VID_{self.device_id.vid:04X}&PID_{self.device_id.pid:04X}\{usb.core._try_get_string(pyusb_devices[0], pyusb_devices[0].iSerialNumber)}'
523
+ device_instance_id = f'USB/VID_{self.device_id.vid:04X}&PID_{self.device_id.pid:04X}/{usb.core._try_get_string(pyusb_devices[0], pyusb_devices[0].iSerialNumber)}'
524
524
  log.debug(f"In reset and restart trying to reset instance id {device_instance_id}")
525
525
  subprocess.run(["pnputil", r"/reboot", r"/disable-device", device_instance_id])
526
526
  subprocess.run(["pnputil", r"/reboot", r"/enable-device", device_instance_id])
@@ -1316,6 +1316,10 @@ class FeatureIdentificationDevice(InterfaceDevice):
1316
1316
  when NOT in external-triggered mode
1317
1317
  @throws exception on timeout (unless external triggering enabled)
1318
1318
  """
1319
+
1320
+ if not self.settings.has_detector():
1321
+ return SpectrometerResponse(keep_alive=True)
1322
+
1319
1323
  response = SpectrometerResponse()
1320
1324
 
1321
1325
  if not self.is_sensor_stable():
@@ -1361,7 +1365,7 @@ class FeatureIdentificationDevice(InterfaceDevice):
1361
1365
  ########################################################################
1362
1366
 
1363
1367
  # this is the number of pixels we expect to read-out over USB
1364
- if self.settings.is_imx385():
1368
+ if False and self.settings.is_imx385():
1365
1369
  # Currently, only the IMX385 uses horizontal binning, so for now
1366
1370
  # limit code changes to that detector (even though they haven't been
1367
1371
  # shown to break other hardware)
@@ -1413,13 +1417,14 @@ class FeatureIdentificationDevice(InterfaceDevice):
1413
1417
  pass
1414
1418
  else:
1415
1419
  errors += 1
1416
- log.error(f"Encountered error on read of {exc}", exc_info=1)
1420
+ log.error(f"Encountered error {errors} on read of {exc}", exc_info=1)
1417
1421
 
1418
1422
  # Don't loop on errors on XS, so we can experimentally
1419
1423
  # use an XS board as a detector-less laser/TEC driver
1420
- # board. (A better solution would be to add FeatureMask.
1421
- # has_detector or similar, this works for now.)
1422
- if errors < 3 and not self.settings.is_xs():
1424
+ # board.
1425
+ if not self.settings.has_detector():
1426
+ pass
1427
+ elif errors < 3:
1423
1428
  log.error(f"ignoring error number {errors}")
1424
1429
  else:
1425
1430
  response.error_msg = "Encountered error on read"
@@ -1621,7 +1626,7 @@ class FeatureIdentificationDevice(InterfaceDevice):
1621
1626
  return response
1622
1627
 
1623
1628
  def require_throwaway(self, flag):
1624
- if flag and self.settings.is_xs() and self.remaining_throwaways < 1:
1629
+ if flag and self.settings.is_xs() and self.remaining_throwaways < 1 and self.settings.has_detector():
1625
1630
  log.debug("queuing throwaways")
1626
1631
  self.remaining_throwaways += 1
1627
1632
 
@@ -1634,6 +1639,10 @@ class FeatureIdentificationDevice(InterfaceDevice):
1634
1639
  if not self.settings.is_xs():
1635
1640
  return SpectrometerResponse(False)
1636
1641
 
1642
+ # MZ: Kludge, disabling while testing
1643
+ log.debug("disabling internal averaging")
1644
+ return SpectrometerResponse(False)
1645
+
1637
1646
  retval = self._send_code(0xff, 0x62, n, label="SET_ONBOARD_SCANS_TO_AVERAGE")
1638
1647
  self.settings.state.scans_to_average = n
1639
1648
  self.settings.state.onboard_averaging = n > 1
@@ -2999,13 +3008,13 @@ class FeatureIdentificationDevice(InterfaceDevice):
2999
3008
  log.debug(f"set_vertical_roi: start {start}, end {end}")
3000
3009
 
3001
3010
  if start < 0 or end < 0:
3002
- log.error("set_vertical_roi: requires POSITIVE (start, stop) lines")
3011
+ log.debug("set_vertical_roi: requires POSITIVE (start, stop) lines")
3003
3012
  return SpectrometerResponse(data=False, error_msg="invalid start and stop lines")
3004
3013
 
3005
3014
  # enforce ascending order (also, note that stop line is "last line binned + 1", so stop must be > start)
3006
3015
  if start >= end:
3007
3016
  # (start, end) = (end, start)
3008
- log.error("set_vertical_roi: requires ascending order (ignoring %d, %d)", start, end)
3017
+ log.debug("set_vertical_roi: requires ascending order (ignoring %d, %d)", start, end)
3009
3018
  return SpectrometerResponse(data=False, error_msg="invalid start and stop lines")
3010
3019
 
3011
3020
  if self.settings.is_xs():