wasatch 2.3.2__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.2 → wasatch-2.3.8}/CHANGELOG.md +22 -0
  2. {wasatch-2.3.2 → wasatch-2.3.8}/Makefile +4 -0
  3. {wasatch-2.3.2 → wasatch-2.3.8}/PKG-INFO +3 -2
  4. wasatch-2.3.8/demo-ids-raman.py +281 -0
  5. {wasatch-2.3.2 → wasatch-2.3.8}/pyproject.toml +1 -1
  6. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/AndorDevice.py +40 -33
  7. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/AreaScanImage.py +1 -1
  8. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/EEPROM.py +14 -0
  9. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/FeatureIdentificationDevice.py +118 -37
  10. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/IDSCamera.py +40 -11
  11. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/IDSDevice.py +17 -11
  12. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/Reading.py +1 -0
  13. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/SpectrometerSettings.py +6 -2
  14. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/SpectrometerState.py +3 -0
  15. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/TCPDevice.py +24 -7
  16. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/WasatchDevice.py +44 -33
  17. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/WasatchDeviceWrapper.py +2 -1
  18. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/WrapperWorker.py +10 -6
  19. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/__init__.py +1 -1
  20. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/applog.py +1 -1
  21. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/utils.py +1 -1
  22. {wasatch-2.3.2 → wasatch-2.3.8}/.example_pypirc +0 -0
  23. {wasatch-2.3.2 → wasatch-2.3.8}/.flake8 +0 -0
  24. {wasatch-2.3.2 → wasatch-2.3.8}/.gitignore +0 -0
  25. {wasatch-2.3.2 → wasatch-2.3.8}/.pylintrc +0 -0
  26. {wasatch-2.3.2 → wasatch-2.3.8}/Doxyfile +0 -0
  27. {wasatch-2.3.2 → wasatch-2.3.8}/LICENSE +0 -0
  28. {wasatch-2.3.2 → wasatch-2.3.8}/MANIFEST.in +0 -0
  29. {wasatch-2.3.2 → wasatch-2.3.8}/README.md +0 -0
  30. {wasatch-2.3.2 → wasatch-2.3.8}/README_PYPI.md +0 -0
  31. {wasatch-2.3.2 → wasatch-2.3.8}/README_RPI.md +0 -0
  32. {wasatch-2.3.2 → wasatch-2.3.8}/README_SETTINGS.md +0 -0
  33. {wasatch-2.3.2 → wasatch-2.3.8}/README_VIRTUAL.md +0 -0
  34. {wasatch-2.3.2 → wasatch-2.3.8}/WasatchShell/README.md +0 -0
  35. {wasatch-2.3.2 → wasatch-2.3.8}/WasatchShell/laser-test.py +0 -0
  36. {wasatch-2.3.2 → wasatch-2.3.8}/WasatchShell/load-test.py +0 -0
  37. {wasatch-2.3.2 → wasatch-2.3.8}/WasatchShell/one-shot.py +0 -0
  38. {wasatch-2.3.2 → wasatch-2.3.8}/WasatchShell/wasatch-shell.py +0 -0
  39. {wasatch-2.3.2 → wasatch-2.3.8}/demo-auto-raman.py +0 -0
  40. {wasatch-2.3.2 → wasatch-2.3.8}/demo-simple.py +0 -0
  41. {wasatch-2.3.2 → wasatch-2.3.8}/demo-virtual.py +0 -0
  42. {wasatch-2.3.2 → wasatch-2.3.8}/demo-workflow.py +0 -0
  43. {wasatch-2.3.2 → wasatch-2.3.8}/demo.py +0 -0
  44. {wasatch-2.3.2 → wasatch-2.3.8}/environments/conda-linux.yml +0 -0
  45. {wasatch-2.3.2 → wasatch-2.3.8}/environments/conda-macos.yml +0 -0
  46. {wasatch-2.3.2 → wasatch-2.3.8}/environments/conda-rpi.yml +0 -0
  47. {wasatch-2.3.2 → wasatch-2.3.8}/environments/conda-win10.yml +0 -0
  48. {wasatch-2.3.2 → wasatch-2.3.8}/screenshots/multiplatform.png +0 -0
  49. {wasatch-2.3.2 → wasatch-2.3.8}/scripts/dark-vs-integ.py +0 -0
  50. {wasatch-2.3.2 → wasatch-2.3.8}/scripts/deploy +0 -0
  51. {wasatch-2.3.2 → wasatch-2.3.8}/testSpectrometers/SiG_785/eeprom/EEPROM-EM-9c65d19f4c.json +0 -0
  52. {wasatch-2.3.2 → wasatch-2.3.8}/testSpectrometers/SiG_785/readings/enlighten-20210329-094722-799520-WP-00686.csv +0 -0
  53. {wasatch-2.3.2 → wasatch-2.3.8}/testSpectrometers/SiG_785/readings/enlighten-20210329-094755-288958-WP-00686.csv +0 -0
  54. {wasatch-2.3.2 → wasatch-2.3.8}/testSpectrometers/WP-00887/eeprom/WP-00887-mock.json +0 -0
  55. {wasatch-2.3.2 → wasatch-2.3.8}/testSpectrometers/WP-00904/eeprom/WP-00904-mock.json +0 -0
  56. {wasatch-2.3.2 → wasatch-2.3.8}/testSpectrometers/WP-00913/eeprom/WP-00913-mock.json +0 -0
  57. {wasatch-2.3.2 → wasatch-2.3.8}/tests/existing_implementations.py +0 -0
  58. {wasatch-2.3.2 → wasatch-2.3.8}/tests/noise-by-setpoint.py +0 -0
  59. {wasatch-2.3.2 → wasatch-2.3.8}/tests/queues.py +0 -0
  60. {wasatch-2.3.2 → wasatch-2.3.8}/tests/unit.py +0 -0
  61. {wasatch-2.3.2 → wasatch-2.3.8}/udev/10-wasatch.rules +0 -0
  62. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/AbstractUSBDevice.py +0 -0
  63. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/AutoRaman.py +0 -0
  64. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/AutoRamanRequest.py +0 -0
  65. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/BLEDevice.py +0 -0
  66. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/BalanceAcquisition.py +0 -0
  67. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/CSVLoader.py +0 -0
  68. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/CommandSettings.py +0 -0
  69. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/ControlObject.py +0 -0
  70. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/DetectorROI.py +0 -0
  71. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/DetectorRegions.py +0 -0
  72. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/DeviceFinderBLE.py +0 -0
  73. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/DeviceFinderTCP.py +0 -0
  74. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/DeviceFinderUSB.py +0 -0
  75. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/DeviceID.py +0 -0
  76. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/EEPROM_multical.sav +0 -0
  77. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/FPGAOptions.py +0 -0
  78. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/FirmwareRequirements.py +0 -0
  79. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/HardwareInfo.py +0 -0
  80. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/IMX385.py +0 -0
  81. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/InterfaceDevice.py +0 -0
  82. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/MockUSBDevice.py +0 -0
  83. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/OceanDevice.py +0 -0
  84. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/PollStatus.py +0 -0
  85. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/ProcessedReading.py +0 -0
  86. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/ROI.py +0 -0
  87. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/RealUSBDevice.py +0 -0
  88. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/SPIDevice.py +0 -0
  89. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/SpectrometerRequest.py +0 -0
  90. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/SpectrometerResponse.py +0 -0
  91. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/StatusMessage.py +0 -0
  92. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/TakeOneRequest.py +0 -0
  93. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/WasatchBus.py +0 -0
  94. {wasatch-2.3.2 → wasatch-2.3.8}/wasatch/simulation_protocol.py +0 -0
@@ -1,5 +1,27 @@
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
15
+ - 2025-11-06 2.3.6
16
+ - resolve hotplug race condition when laser power set in percent
17
+ - cache certain device getters to avoid I2C overruns
18
+ - stop trying to read laser temperature on FX2, it's never worked right
19
+ - 2025-10-22 2.3.5
20
+ - don't loop on spectral read errors on XS
21
+ - 2025-10-22 2.3.4
22
+ - ignore heartbeat on Andor
23
+ - 2025-06-30 2.3.3
24
+ - Hamamatsu 2048px area scan
3
25
  - 2025-06-18 2.3.2
4
26
  - XS area scan
5
27
  - fixed off-by-one stop line
@@ -28,7 +28,11 @@ pip-install-local:
28
28
  pip install $$PWD
29
29
 
30
30
  publish-test:
31
+ @echo REMINDER to keep .toml in sync with __init__.py!
32
+ @echo
31
33
  flit publish --repository testpypi
32
34
 
33
35
  publish:
36
+ @echo REMINDER to keep .toml in sync with __init__.py!
37
+ @echo
34
38
  flit publish
@@ -1,9 +1,10 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: wasatch
3
- Version: 2.3.2
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
7
+ License-File: LICENSE
7
8
  Requires-Dist: numpy
8
9
  Requires-Dist: six
9
10
  Requires-Dist: psutil
@@ -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.2"
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"
@@ -188,7 +192,7 @@ class AndorDevice(InterfaceDevice):
188
192
  process_f["take_one_request"] = self.set_take_one_request
189
193
 
190
194
  process_f["reset_scan_averaging"] = self.not_implemented
191
- process_f["heartbeat"] = self.not_implemented
195
+ process_f["heartbeat"] = lambda x: None
192
196
 
193
197
  ##################################################################
194
198
  # What follows is the old init-lambdas that are squashed into process_f
@@ -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:
@@ -1446,5 +1457,8 @@ class EEPROMField:
1446
1457
  self.offset = pos[1]
1447
1458
  self.length = pos[2]
1448
1459
 
1460
+ def __repr__(self):
1461
+ return str(self.__dict__)
1462
+
1449
1463
  def toJSON(self):
1450
1464
  return str(self.__dict__)