wasatch 2.3.6__tar.gz → 2.3.12__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.12}/.gitignore +1 -0
  2. {wasatch-2.3.6 → wasatch-2.3.12}/CHANGELOG.md +23 -0
  3. {wasatch-2.3.6 → wasatch-2.3.12}/PKG-INFO +1 -1
  4. {wasatch-2.3.6 → wasatch-2.3.12}/README_PYPI.md +6 -0
  5. wasatch-2.3.12/demo-ids-raman.py +297 -0
  6. {wasatch-2.3.6 → wasatch-2.3.12}/pyproject.toml +1 -1
  7. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/AndorDevice.py +39 -32
  8. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/AreaScanImage.py +1 -1
  9. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/EEPROM.py +83 -12
  10. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/FeatureIdentificationDevice.py +35 -26
  11. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/IDSCamera.py +124 -37
  12. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/IDSDevice.py +69 -46
  13. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/Reading.py +1 -0
  14. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/SpectrometerSettings.py +4 -0
  15. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/TCPDevice.py +24 -7
  16. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/WasatchDevice.py +44 -33
  17. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/WasatchDeviceWrapper.py +1 -0
  18. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/WrapperWorker.py +10 -6
  19. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/__init__.py +1 -1
  20. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/applog.py +1 -1
  21. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/utils.py +22 -6
  22. {wasatch-2.3.6 → wasatch-2.3.12}/.example_pypirc +0 -0
  23. {wasatch-2.3.6 → wasatch-2.3.12}/.flake8 +0 -0
  24. {wasatch-2.3.6 → wasatch-2.3.12}/.pylintrc +0 -0
  25. {wasatch-2.3.6 → wasatch-2.3.12}/Doxyfile +0 -0
  26. {wasatch-2.3.6 → wasatch-2.3.12}/LICENSE +0 -0
  27. {wasatch-2.3.6 → wasatch-2.3.12}/MANIFEST.in +0 -0
  28. {wasatch-2.3.6 → wasatch-2.3.12}/Makefile +0 -0
  29. {wasatch-2.3.6 → wasatch-2.3.12}/README.md +0 -0
  30. {wasatch-2.3.6 → wasatch-2.3.12}/README_RPI.md +0 -0
  31. {wasatch-2.3.6 → wasatch-2.3.12}/README_SETTINGS.md +0 -0
  32. {wasatch-2.3.6 → wasatch-2.3.12}/README_VIRTUAL.md +0 -0
  33. {wasatch-2.3.6 → wasatch-2.3.12}/WasatchShell/README.md +0 -0
  34. {wasatch-2.3.6 → wasatch-2.3.12}/WasatchShell/laser-test.py +0 -0
  35. {wasatch-2.3.6 → wasatch-2.3.12}/WasatchShell/load-test.py +0 -0
  36. {wasatch-2.3.6 → wasatch-2.3.12}/WasatchShell/one-shot.py +0 -0
  37. {wasatch-2.3.6 → wasatch-2.3.12}/WasatchShell/wasatch-shell.py +0 -0
  38. {wasatch-2.3.6 → wasatch-2.3.12}/demo-auto-raman.py +0 -0
  39. {wasatch-2.3.6 → wasatch-2.3.12}/demo-simple.py +0 -0
  40. {wasatch-2.3.6 → wasatch-2.3.12}/demo-virtual.py +0 -0
  41. {wasatch-2.3.6 → wasatch-2.3.12}/demo-workflow.py +0 -0
  42. {wasatch-2.3.6 → wasatch-2.3.12}/demo.py +0 -0
  43. {wasatch-2.3.6 → wasatch-2.3.12}/environments/conda-linux.yml +0 -0
  44. {wasatch-2.3.6 → wasatch-2.3.12}/environments/conda-macos.yml +0 -0
  45. {wasatch-2.3.6 → wasatch-2.3.12}/environments/conda-rpi.yml +0 -0
  46. {wasatch-2.3.6 → wasatch-2.3.12}/environments/conda-win10.yml +0 -0
  47. {wasatch-2.3.6 → wasatch-2.3.12}/screenshots/multiplatform.png +0 -0
  48. {wasatch-2.3.6 → wasatch-2.3.12}/scripts/dark-vs-integ.py +0 -0
  49. {wasatch-2.3.6 → wasatch-2.3.12}/scripts/deploy +0 -0
  50. {wasatch-2.3.6 → wasatch-2.3.12}/testSpectrometers/SiG_785/eeprom/EEPROM-EM-9c65d19f4c.json +0 -0
  51. {wasatch-2.3.6 → wasatch-2.3.12}/testSpectrometers/SiG_785/readings/enlighten-20210329-094722-799520-WP-00686.csv +0 -0
  52. {wasatch-2.3.6 → wasatch-2.3.12}/testSpectrometers/SiG_785/readings/enlighten-20210329-094755-288958-WP-00686.csv +0 -0
  53. {wasatch-2.3.6 → wasatch-2.3.12}/testSpectrometers/WP-00887/eeprom/WP-00887-mock.json +0 -0
  54. {wasatch-2.3.6 → wasatch-2.3.12}/testSpectrometers/WP-00904/eeprom/WP-00904-mock.json +0 -0
  55. {wasatch-2.3.6 → wasatch-2.3.12}/testSpectrometers/WP-00913/eeprom/WP-00913-mock.json +0 -0
  56. {wasatch-2.3.6 → wasatch-2.3.12}/tests/existing_implementations.py +0 -0
  57. {wasatch-2.3.6 → wasatch-2.3.12}/tests/noise-by-setpoint.py +0 -0
  58. {wasatch-2.3.6 → wasatch-2.3.12}/tests/queues.py +0 -0
  59. {wasatch-2.3.6 → wasatch-2.3.12}/tests/unit.py +0 -0
  60. {wasatch-2.3.6 → wasatch-2.3.12}/udev/10-wasatch.rules +0 -0
  61. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/AbstractUSBDevice.py +0 -0
  62. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/AutoRaman.py +0 -0
  63. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/AutoRamanRequest.py +0 -0
  64. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/BLEDevice.py +0 -0
  65. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/BalanceAcquisition.py +0 -0
  66. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/CSVLoader.py +0 -0
  67. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/CommandSettings.py +0 -0
  68. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/ControlObject.py +0 -0
  69. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/DetectorROI.py +0 -0
  70. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/DetectorRegions.py +0 -0
  71. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/DeviceFinderBLE.py +0 -0
  72. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/DeviceFinderTCP.py +0 -0
  73. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/DeviceFinderUSB.py +0 -0
  74. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/DeviceID.py +0 -0
  75. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/EEPROM_multical.sav +0 -0
  76. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/FPGAOptions.py +0 -0
  77. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/FirmwareRequirements.py +0 -0
  78. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/HardwareInfo.py +0 -0
  79. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/IMX385.py +0 -0
  80. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/InterfaceDevice.py +0 -0
  81. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/MockUSBDevice.py +0 -0
  82. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/OceanDevice.py +0 -0
  83. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/PollStatus.py +0 -0
  84. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/ProcessedReading.py +0 -0
  85. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/ROI.py +0 -0
  86. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/RealUSBDevice.py +0 -0
  87. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/SPIDevice.py +0 -0
  88. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/SpectrometerRequest.py +0 -0
  89. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/SpectrometerResponse.py +0 -0
  90. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/SpectrometerState.py +0 -0
  91. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/StatusMessage.py +0 -0
  92. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/TakeOneRequest.py +0 -0
  93. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/WasatchBus.py +0 -0
  94. {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/simulation_protocol.py +0 -0
@@ -111,3 +111,4 @@ wasatch_applog.txt
111
111
  .DS_Store
112
112
  .vs/*
113
113
  environment.yml
114
+ data/
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ - 2026-02-17 2.3.12
4
+ - added laser_password, startup_scans_to_average, laser_attenuator
5
+ - added [un]pack_field
6
+ - un-rotated Hamamatsu area scan 180 deg :-(
7
+ - allow IDS config files to be located by serial number prefix
8
+ - 2026-02-09 2.3.11
9
+ - rotate Hamamatsu area scan 180 deg
10
+ - 2026-01-16 2.3.10
11
+ - support 2min integrations on STARVIS
12
+ - 2026-01-15 2.3.9
13
+ - added --normalize to demo-ids-raman.py
14
+ - 2026-01-12 2.3.8
15
+ - added settings.has_detector
16
+ - improved support for Andor .json "virtual EEPROMs"
17
+ - bump internal WrapperWorker poll rate from 20Hz to 200Hz
18
+ - retain debug logging for 20sec after connection (vs previous 10sec)
19
+ - only update Andor detector temperature at 1Hz
20
+ - improved support for using 220250 boards as standalone laser drivers w/o
21
+ detectors (added Reading.keep_alive to indicate metadata but no spectra)
22
+ - 2025-12-10 2.3.7
23
+ - added EEPROM.is_oem
24
+ - fixed TCPDevice.set_detector_gain and set_vertical_roi
25
+ - disable use of eeprom.actual_pixels_horizontal during acquisition
3
26
  - 2025-11-06 2.3.6
4
27
  - resolve hotplug race condition when laser power set in percent
5
28
  - 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.12
4
4
  Summary: Application-level driver for Wasatch Photonics spectrometers
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -50,6 +50,12 @@ before uploading to the "public, global, default" pypi repository.
50
50
  When you are ready to publish run `flit publish` or `flit publish --repository pypi`
51
51
  (or just "make publish").
52
52
 
53
+ ## Common Errors
54
+
55
+ ### requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://test.pypi.org/legacy/
56
+
57
+ Ensure wasatch/__init__.py version matches pyproject.toml.
58
+
53
59
  # Installing 'wasatch' package from source
54
60
 
55
61
  If you've cloned the Wasatch.PY repo and want to just install the `wasatch`
@@ -0,0 +1,297 @@
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
+ from PIL import Image, ImageStat # for image normalization
30
+
31
+ import wasatch
32
+ from wasatch import applog
33
+ from wasatch.DeviceID import DeviceID
34
+ from wasatch.IDSDevice import IDSDevice
35
+ from wasatch.WasatchBus import WasatchBus
36
+ from wasatch.WasatchDevice import WasatchDevice
37
+
38
+ log = logging.getLogger(__name__)
39
+ demo = None
40
+
41
+ class WasatchDemo:
42
+
43
+ def __init__(self):
44
+ self.camera_device = None # a wasatch.IDSDevice wrapping a wasatch.IDSCamera (as 'camera')
45
+ self.laser_device = None # a wasatch.WasatchDevice wrapping a wasatch.FeatureIdentificationDevice (as 'hardware')
46
+ self.reading_count = 0
47
+ self.exiting = False
48
+
49
+ self.args = self.parse_args()
50
+
51
+ print(f"Wasatch.PY {wasatch.__version__} IDS Raman Demo")
52
+
53
+ def parse_args(self):
54
+ parser = argparse.ArgumentParser(description="Simple demo to acquire spectra from command-line interface")
55
+ parser.add_argument("--log-level", type=str, default="INFO", help="logging level", choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL","NEVER"])
56
+ parser.add_argument("--integration-time-ms", type=int, default=1000, help="integration time (ms, default 10)")
57
+ parser.add_argument("--take-dark", action="store_true", help="first measurement is used for dark subtraction")
58
+ parser.add_argument("--enable-laser", action="store_true", help="fire laser (after optional dark)")
59
+ parser.add_argument("--max", type=int, default=0, help="max spectra to acquire (default 0, unlimited)")
60
+ parser.add_argument("--save-spectra", action="store_true", help="save vertically-binned spectra to row-ordered CSV")
61
+ parser.add_argument("--save-png", action="store_true", help="save PNG of each image frame")
62
+ parser.add_argument("--save-data", action="store_true", help="save 2D array of each image frame")
63
+ parser.add_argument("--normalize", action="store_true", help="normalize PNG image")
64
+ parser.add_argument("--save-dir", type=str, default=".", help="directory to save files")
65
+ parser.add_argument("--prefix", type=str, default="ids-raman", help="filename prefix")
66
+
67
+ args = parser.parse_args()
68
+ applog.MainLogger(args.log_level)
69
+
70
+ return args
71
+
72
+ def connect(self):
73
+
74
+ ########################################################################
75
+ # Look for laser driver board
76
+ ########################################################################
77
+
78
+ bus = WasatchBus()
79
+ if not bus.device_ids:
80
+ print("No Wasatch USB spectrometers found.")
81
+ return
82
+
83
+ # look for laser driver on WasatchBus -- we won't see the camera here,
84
+ # because IDSPeak devices are not automatically considered for inclusion
85
+ # by WasatchBus (although they could be)
86
+ for device_id in bus.device_ids:
87
+ log.debug("connect: trying to connect to %s", device_id)
88
+ device = WasatchDevice(device_id)
89
+ ok = device.connect()
90
+ if not ok:
91
+ log.critical("connect: can't connect to %s", device_id)
92
+ continue
93
+
94
+ log.debug(f"connect: connected to {device_id} ({device})")
95
+ if device.settings is None:
96
+ log.error("can't have device without SpectrometerSettings")
97
+ continue
98
+
99
+ print(f"connected to {device.settings.full_model()} {device.settings.eeprom.serial_number} with {device.settings.pixels()} pixels " +
100
+ f"from ({device.settings.wavelengths[0]:.2f}, {device.settings.wavelengths[-1]:.2f}nm)" +
101
+ ( f" ({device.settings.wavenumbers[0]:.2f}, {device.settings.wavenumbers[-1]:.2f}cm⁻¹)" if device.settings.has_excitation() else "") +
102
+ f" (Microcontroller {device.settings.microcontroller_firmware_version}," +
103
+ f" FPGA {device.settings.fpga_firmware_version})")
104
+
105
+ self.laser_device = device
106
+ break
107
+
108
+ if self.laser_device is None:
109
+ log.critical("could not find laser driver, exitting")
110
+ return
111
+
112
+ ########################################################################
113
+ # now look for an IDS camera
114
+ ########################################################################
115
+
116
+ try:
117
+ device_id = DeviceID(label="IDSPeak")
118
+ device = IDSDevice(device_id=device_id, scratch_dir=".", consumer_deletes_area_scan_image=True)
119
+ if device.connect():
120
+ self.camera_device = device
121
+ else:
122
+ log.error("failed connecting to IDSCamera")
123
+ except:
124
+ log.critical("exception connecting to IDS camera", exc_info=1)
125
+ return
126
+
127
+ if self.camera_device is None:
128
+ log.critical("could not find IDS camera, exitting")
129
+ return
130
+
131
+ device = self.camera_device
132
+ camera = device.camera
133
+ settings = device.settings
134
+ eeprom = settings.eeprom
135
+
136
+ device.set_area_scan_enable(True)
137
+
138
+ # copy the laser excitation over to the camera "virtual EEPROM" so we can
139
+ # compute wavenumbers
140
+ eeprom.excitation_nm = self.laser_device.settings.eeprom.excitation_nm
141
+ settings.update_wavecal()
142
+ print(f"connected to IDSPeak camera {eeprom.model} {eeprom.serial_number} with {camera.width}x{camera.height} pixels " +
143
+ f"from ({settings.wavelengths[0]:.2f}, {settings.wavelengths[-1]:.2f}nm)" +
144
+ ( f" ({settings.wavenumbers[0]:.2f}, {settings.wavenumbers[-1]:.2f}cm⁻¹)" if settings.has_excitation() else ""))
145
+
146
+ return True
147
+
148
+ ############################################################################
149
+ # #
150
+ # Run-Time Loop #
151
+ # #
152
+ ############################################################################
153
+
154
+ def run(self):
155
+ if self.args.save_dir != ".":
156
+ os.makedirs(self.args.save_dir, exist_ok=True)
157
+
158
+ self.camera_device.set_integration_time_ms(self.args.integration_time_ms)
159
+
160
+ # initialize outfile if saving spectra
161
+ outfile = None
162
+ if self.args.save_spectra:
163
+ filename = os.path.join(self.args.save_dir, self.args.prefix + "-spectra.csv")
164
+ outfile = open(filename, "w")
165
+ outfile.write("pixels, 0, " + ", ".join([f"{x}" for x in range(self.camera_device.settings.pixels())]) + "\n")
166
+ outfile.write("wavelengths, 0, " + ", ".join([f"{x:.2f}" for x in self.camera_device.settings.wavelengths]) + "\n")
167
+ outfile.write("wavenumbers, 0, " + ", ".join([f"{x:.2f}" for x in self.camera_device.settings.wavenumbers]) + "\n")
168
+ outfile.write("time, count, spectrum\n")
169
+
170
+ # read spectra until user presses Control-Break
171
+ while not self.exiting and (self.args.max == 0 or self.reading_count < self.args.max):
172
+
173
+ ####################################################################
174
+ # check to fire laser
175
+ ####################################################################
176
+
177
+ # on the first reading (or the second, if taking dark), enable the
178
+ # laser if requested
179
+ if (self.args.enable_laser and (self.reading_count == 0 or (self.args.take_dark and self.reading_count == 1))):
180
+ print("enabling laser")
181
+ self.laser_device.hardware.set_laser_enable(True)
182
+
183
+ ####################################################################
184
+ # read image
185
+ ####################################################################
186
+
187
+ if self.exiting:
188
+ break
189
+
190
+ response = self.camera_device.acquire_data()
191
+ if response.keep_alive:
192
+ log.debug(f"ignoring keepalive from {device}")
193
+ continue
194
+
195
+ if isinstance(response.data, bool):
196
+ if response.data:
197
+ log.debug("received poison-pill, exiting")
198
+ break
199
+ else:
200
+ log.debug("no reading available")
201
+ continue
202
+
203
+ if response.data.failure:
204
+ log.debug("reading indicated failure")
205
+ break
206
+
207
+ if self.exiting:
208
+ break
209
+
210
+ self.reading_count += 1
211
+
212
+ reading = response.data
213
+ spectrum = reading.spectrum
214
+ settings = self.camera_device.settings
215
+ asi = reading.area_scan_image
216
+ ts = reading.timestamp.strftime("%Y%m%d-%H%M%S-%f")
217
+
218
+ # print some stats as we go
219
+ spectrum_min = np.amin(spectrum)
220
+ spectrum_max = np.amax(spectrum)
221
+ spectrum_avg = np.mean(spectrum)
222
+ spectrum_std = np.std (spectrum)
223
+ 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}")
224
+
225
+ # append to row-ordered file
226
+ if outfile:
227
+ values = ",".join(format(x, ".2f") for x in spectrum)
228
+ outfile.write(f"{reading.timestamp}, {self.reading_count}, {values}\n")
229
+
230
+ # save PNG
231
+ if self.args.save_png and asi.pathname_png:
232
+ filename = os.path.join(self.args.save_dir, f"{self.args.prefix}-{self.reading_count:04d}.png")
233
+ log.debug(f"renaming {asi.pathname_png} -> {filename}")
234
+ os.replace(asi.pathname_png, filename)
235
+ print(f"\tsaved {filename}")
236
+
237
+ # normalize PNG (just the image file, not the raw 2D data)
238
+ if self.args.normalize:
239
+ img = Image.open(filename).convert('L')
240
+ stat = ImageStat.Stat(img)
241
+ mean_brightness = stat.mean[0]
242
+ img_array = np.array(img)
243
+ normalized_img_array = img_array / mean_brightness * 100
244
+ normalized_img = Image.fromarray(np.uint8(normalized_img_array))
245
+ normalized_img.save(filename)
246
+ print(f"\tnormalized {filename}")
247
+
248
+ # save area scan data
249
+ if self.args.save_data and asi.data is not None:
250
+ filename = os.path.join(self.args.save_dir, f"{self.args.prefix}-{self.reading_count:04d}.csv")
251
+ np.savetxt(filename, asi.data, fmt='%d', delimiter=",")
252
+ print(f"\tsaved {filename}")
253
+
254
+ if self.args.take_dark and self.reading_count == 1:
255
+ print("\tsaving dark")
256
+ self.camera_device.camera.set_dark_asi(asi)
257
+
258
+ ################################################################################
259
+ # main()
260
+ ################################################################################
261
+
262
+ def signal_handler(signal, frame):
263
+ print('\rInterrupted by Ctrl-C...shutting down', end=' ')
264
+ shutdown()
265
+
266
+ def shutdown():
267
+ if demo:
268
+ demo.exiting = True
269
+ if demo.laser_device is not None:
270
+ demo.laser_device.hardware.set_laser_enable(False)
271
+ demo.laser_device.disconnect()
272
+
273
+ if demo.camera_device is not None:
274
+ demo.camera_device.disconnect()
275
+ sleep(1)
276
+
277
+ log.debug(None)
278
+ applog.explicit_log_close()
279
+ sys.exit()
280
+
281
+ if __name__ == "__main__":
282
+ # add a signal-handler to catch ctrl-C interrupts, so we can cleanly close
283
+ # down all connected devices
284
+ signal.signal(signal.SIGINT, signal_handler)
285
+
286
+ demo = WasatchDemo()
287
+ if demo.connect():
288
+ # Note that on Windows, Control-Break (SIGBREAK) differs from
289
+ # Control-C (SIGINT); see https://stackoverflow.com/a/1364199
290
+ log.debug("Press Control-Break to interrupt...")
291
+ try:
292
+ demo.run()
293
+ except:
294
+ # catch exception so shutdown will run and disable laser
295
+ log.error("Caught exception during demo", exc_info=1)
296
+
297
+ shutdown()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wasatch"
3
- version = "2.3.6"
3
+ version = "2.3.12"
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}>"