wasatch 2.2.17__tar.gz → 2.3.6__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 (98) hide show
  1. {wasatch-2.2.17 → wasatch-2.3.6}/CHANGELOG.md +38 -0
  2. wasatch-2.3.6/Makefile +38 -0
  3. {wasatch-2.2.17 → wasatch-2.3.6}/PKG-INFO +21 -6
  4. wasatch-2.3.6/pyproject.toml +36 -0
  5. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/AndorDevice.py +201 -115
  6. wasatch-2.3.6/wasatch/AreaScanImage.py +19 -0
  7. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/AutoRaman.py +6 -0
  8. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/AutoRamanRequest.py +2 -2
  9. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/CSVLoader.py +7 -3
  10. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/DeviceFinderUSB.py +33 -25
  11. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/DeviceID.py +8 -2
  12. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/EEPROM.py +149 -22
  13. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/FeatureIdentificationDevice.py +579 -158
  14. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/FirmwareRequirements.py +23 -6
  15. wasatch-2.3.6/wasatch/IDSCamera.py +916 -0
  16. wasatch-2.3.6/wasatch/IDSDevice.py +329 -0
  17. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/InterfaceDevice.py +8 -0
  18. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/Reading.py +10 -11
  19. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/SpectrometerResponse.py +1 -1
  20. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/SpectrometerSettings.py +5 -2
  21. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/SpectrometerState.py +8 -0
  22. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/StatusMessage.py +3 -0
  23. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/TCPDevice.py +0 -1
  24. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/WasatchDevice.py +161 -172
  25. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/WasatchDeviceWrapper.py +3 -1
  26. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/WrapperWorker.py +13 -2
  27. wasatch-2.3.6/wasatch/__init__.py +4 -0
  28. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/utils.py +32 -4
  29. wasatch-2.2.17/Makefile +0 -37
  30. wasatch-2.2.17/pyproject.toml +0 -13
  31. wasatch-2.2.17/requirements.txt +0 -11
  32. wasatch-2.2.17/setup.cfg +0 -24
  33. wasatch-2.2.17/wasatch/__init__.py +0 -4
  34. {wasatch-2.2.17 → wasatch-2.3.6}/.example_pypirc +0 -0
  35. {wasatch-2.2.17 → wasatch-2.3.6}/.flake8 +0 -0
  36. {wasatch-2.2.17 → wasatch-2.3.6}/.gitignore +0 -0
  37. {wasatch-2.2.17 → wasatch-2.3.6}/.pylintrc +0 -0
  38. {wasatch-2.2.17 → wasatch-2.3.6}/Doxyfile +0 -0
  39. {wasatch-2.2.17 → wasatch-2.3.6}/LICENSE +0 -0
  40. {wasatch-2.2.17 → wasatch-2.3.6}/MANIFEST.in +0 -0
  41. {wasatch-2.2.17 → wasatch-2.3.6}/README.md +0 -0
  42. {wasatch-2.2.17 → wasatch-2.3.6}/README_PYPI.md +0 -0
  43. {wasatch-2.2.17 → wasatch-2.3.6}/README_RPI.md +0 -0
  44. {wasatch-2.2.17 → wasatch-2.3.6}/README_SETTINGS.md +0 -0
  45. {wasatch-2.2.17 → wasatch-2.3.6}/README_VIRTUAL.md +0 -0
  46. {wasatch-2.2.17 → wasatch-2.3.6}/WasatchShell/README.md +0 -0
  47. {wasatch-2.2.17 → wasatch-2.3.6}/WasatchShell/laser-test.py +0 -0
  48. {wasatch-2.2.17 → wasatch-2.3.6}/WasatchShell/load-test.py +0 -0
  49. {wasatch-2.2.17 → wasatch-2.3.6}/WasatchShell/one-shot.py +0 -0
  50. {wasatch-2.2.17 → wasatch-2.3.6}/WasatchShell/wasatch-shell.py +0 -0
  51. {wasatch-2.2.17 → wasatch-2.3.6}/demo-auto-raman.py +0 -0
  52. {wasatch-2.2.17 → wasatch-2.3.6}/demo-simple.py +0 -0
  53. {wasatch-2.2.17 → wasatch-2.3.6}/demo-virtual.py +0 -0
  54. {wasatch-2.2.17 → wasatch-2.3.6}/demo-workflow.py +0 -0
  55. {wasatch-2.2.17 → wasatch-2.3.6}/demo.py +0 -0
  56. {wasatch-2.2.17 → wasatch-2.3.6}/environments/conda-linux.yml +0 -0
  57. {wasatch-2.2.17 → wasatch-2.3.6}/environments/conda-macos.yml +0 -0
  58. {wasatch-2.2.17 → wasatch-2.3.6}/environments/conda-rpi.yml +0 -0
  59. {wasatch-2.2.17 → wasatch-2.3.6}/environments/conda-win10.yml +0 -0
  60. {wasatch-2.2.17 → wasatch-2.3.6}/screenshots/multiplatform.png +0 -0
  61. {wasatch-2.2.17 → wasatch-2.3.6}/scripts/dark-vs-integ.py +0 -0
  62. {wasatch-2.2.17 → wasatch-2.3.6}/scripts/deploy +0 -0
  63. {wasatch-2.2.17 → wasatch-2.3.6}/testSpectrometers/SiG_785/eeprom/EEPROM-EM-9c65d19f4c.json +0 -0
  64. {wasatch-2.2.17 → wasatch-2.3.6}/testSpectrometers/SiG_785/readings/enlighten-20210329-094722-799520-WP-00686.csv +0 -0
  65. {wasatch-2.2.17 → wasatch-2.3.6}/testSpectrometers/SiG_785/readings/enlighten-20210329-094755-288958-WP-00686.csv +0 -0
  66. {wasatch-2.2.17 → wasatch-2.3.6}/testSpectrometers/WP-00887/eeprom/WP-00887-mock.json +0 -0
  67. {wasatch-2.2.17 → wasatch-2.3.6}/testSpectrometers/WP-00904/eeprom/WP-00904-mock.json +0 -0
  68. {wasatch-2.2.17 → wasatch-2.3.6}/testSpectrometers/WP-00913/eeprom/WP-00913-mock.json +0 -0
  69. {wasatch-2.2.17 → wasatch-2.3.6}/tests/existing_implementations.py +0 -0
  70. {wasatch-2.2.17 → wasatch-2.3.6}/tests/noise-by-setpoint.py +0 -0
  71. {wasatch-2.2.17 → wasatch-2.3.6}/tests/queues.py +0 -0
  72. {wasatch-2.2.17 → wasatch-2.3.6}/tests/unit.py +0 -0
  73. {wasatch-2.2.17 → wasatch-2.3.6}/udev/10-wasatch.rules +0 -0
  74. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/AbstractUSBDevice.py +0 -0
  75. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/BLEDevice.py +0 -0
  76. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/BalanceAcquisition.py +0 -0
  77. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/CommandSettings.py +0 -0
  78. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/ControlObject.py +0 -0
  79. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/DetectorROI.py +0 -0
  80. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/DetectorRegions.py +0 -0
  81. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/DeviceFinderBLE.py +0 -0
  82. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/DeviceFinderTCP.py +0 -0
  83. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/EEPROM_multical.sav +0 -0
  84. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/FPGAOptions.py +0 -0
  85. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/HardwareInfo.py +0 -0
  86. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/IMX385.py +0 -0
  87. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/MockUSBDevice.py +0 -0
  88. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/OceanDevice.py +0 -0
  89. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/PollStatus.py +0 -0
  90. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/ProcessedReading.py +0 -0
  91. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/ROI.py +0 -0
  92. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/RealUSBDevice.py +0 -0
  93. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/SPIDevice.py +0 -0
  94. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/SpectrometerRequest.py +0 -0
  95. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/TakeOneRequest.py +0 -0
  96. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/WasatchBus.py +0 -0
  97. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/applog.py +0 -0
  98. {wasatch-2.2.17 → wasatch-2.3.6}/wasatch/simulation_protocol.py +0 -0
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ - 2025-11-06 2.3.6
4
+ - resolve hotplug race condition when laser power set in percent
5
+ - cache certain device getters to avoid I2C overruns
6
+ - stop trying to read laser temperature on FX2, it's never worked right
7
+ - 2025-10-22 2.3.5
8
+ - don't loop on spectral read errors on XS
9
+ - 2025-10-22 2.3.4
10
+ - ignore heartbeat on Andor
11
+ - 2025-06-30 2.3.3
12
+ - Hamamatsu 2048px area scan
13
+ - 2025-06-18 2.3.2
14
+ - XS area scan
15
+ - fixed off-by-one stop line
16
+ - fixed xs_area_scan_offset_kludge on newer FPGAs
17
+ - added "max" to utils.vercmp
18
+ - 2025-05-30 2.3.1
19
+ - enforce signed and unsigned integers in MultiWavecalCalibration (resolve XL
20
+ config files with floating-point horizontal ROI)
21
+ - update BatchCollection and Scan Averaging for AndorDevice
22
+ - added EEPROM.laser_interlock_excluded
23
+ - area scan mostly works for XS
24
+ - packaging
25
+ - remove legacy package install files
26
+ - add all dependencies to pyproject.toml
27
+ - 2025-04-08 2.3.0
28
+ - IDS cameras
29
+ - add IDSDevice, IDSCamera
30
+ - area scan
31
+ - add AreaScanImage
32
+ - expect start-of-frame marker at spectrum[0]
33
+ - move row index to spectrum[1]
34
+ - redesigned for Hamamatsu
35
+ - vertical ROI
36
+ - support vertical ROI in Hamamatsu
37
+ - firmware logging
38
+ - allow ENLIGHTEN to display firmware log
39
+ - 2025-02-26 2.2.18
40
+ - add laser state to Reading from AutoRaman
3
41
  - 2025-02-11 2.2.17
4
42
  - [#133] merged in Fast BatchCollection fixes, part of ENLIGHTEN [#516]
5
43
  - 2025-01-20 2.2.16
wasatch-2.3.6/Makefile ADDED
@@ -0,0 +1,38 @@
1
+ help:
2
+ @echo "Supported targets:"
3
+ @echo " doc (render Doxygen)"
4
+ @echo " clean (delete artifacts)"
5
+ @echo " cloc (count SLOC)"
6
+ @echo " publish (flit --> pypi)"
7
+ @echo " publish-test (flit --> testpypi)"
8
+ @echo " pip-install-local"
9
+
10
+ .PHONY: doc docs clean cloc publish publish-test pip-install-local
11
+
12
+ cloc:
13
+ @cloc --include-lang=Python .
14
+
15
+ doc docs:
16
+ @echo "Rendering Doxygen..."
17
+ @mkdir -p doxygen
18
+ @doxygen 1>doxygen.out 2>doxygen.err
19
+ @cat doxygen.err
20
+
21
+ clean:
22
+ @rm -rf doxygen \
23
+ doxygen.out \
24
+ doxygen.err
25
+ @find . -name \*.pyc -exec rm {} \;
26
+
27
+ pip-install-local:
28
+ pip install $$PWD
29
+
30
+ publish-test:
31
+ @echo REMINDER to keep .toml in sync with __init__.py!
32
+ @echo
33
+ flit publish --repository testpypi
34
+
35
+ publish:
36
+ @echo REMINDER to keep .toml in sync with __init__.py!
37
+ @echo
38
+ flit publish
@@ -1,11 +1,26 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: wasatch
3
- Version: 2.2.17
4
- Summary: This package is a driver for Wasatch Photonics spectrometers
5
- Author-email: Wasatch Photonics <edort@wasatchphotonics.com>
3
+ Version: 2.3.6
4
+ Summary: Application-level driver for Wasatch Photonics spectrometers
5
+ Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
7
- Classifier: License :: OSI Approved :: MIT License
8
- Project-URL: Home, https://github.com/WasatchPhotonics/Wasatch.PY
7
+ License-File: LICENSE
8
+ Requires-Dist: numpy
9
+ Requires-Dist: six
10
+ Requires-Dist: psutil
11
+ Requires-Dist: future
12
+ Requires-Dist: pygtail
13
+ Requires-Dist: pyusb
14
+ Requires-Dist: requests
15
+ Requires-Dist: pexpect
16
+ Requires-Dist: seabreeze
17
+ Requires-Dist: pyudev
18
+ Requires-Dist: pywin32; platform_system=='Windows'
19
+ Requires-Dist: pyftdi
20
+ Requires-Dist: crcmod
21
+ Requires-Dist: bleak
22
+ Requires-Dist: ids_peak; platform_system=='Windows'
23
+ Project-URL: Homepage, https://github.com/WasatchPhotonics/wasatch.py
9
24
 
10
25
  ![Sample Console Views](https://github.com/WasatchPhotonics/Wasatch.PY/raw/master/screenshots/multiplatform.png)
11
26
 
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "wasatch"
3
+ version = "2.3.6"
4
+ description= "Application-level driver for Wasatch Photonics spectrometers"
5
+ requires-python = ">=3.9"
6
+ readme = "README.md"
7
+ dependencies = [
8
+ #"mkl",
9
+ "numpy",
10
+ "six",
11
+ "psutil",
12
+ "future",
13
+ "pygtail",
14
+ "pyusb",
15
+ "requests",
16
+ "pexpect",
17
+ "seabreeze",
18
+ "pyudev",
19
+ "pywin32; platform_system=='Windows'",
20
+ "pyftdi",
21
+ "crcmod",
22
+ "bleak",
23
+ "ids_peak; platform_system=='Windows'",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/WasatchPhotonics/wasatch.py"
28
+
29
+ [build-system]
30
+ requires=["setuptools"]
31
+ build-backend="setuptools.build_meta"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where=["."]
35
+ include=["wasatch"]
36
+ exclude=[]
@@ -1,14 +1,17 @@
1
1
  import os
2
2
  import usb
3
3
  import json
4
+ import numpy as np
4
5
  import struct
5
6
  import logging
6
7
  import datetime
8
+
7
9
  from ctypes import *
8
10
 
9
11
  from .SpectrometerSettings import SpectrometerSettings
10
12
  from .SpectrometerResponse import SpectrometerResponse
11
13
  from .InterfaceDevice import InterfaceDevice
14
+ from .StatusMessage import StatusMessage
12
15
  from .DeviceID import DeviceID
13
16
  from .Reading import Reading
14
17
  from .ROI import ROI
@@ -71,11 +74,10 @@ class AndorDevice(InterfaceDevice):
71
74
  # An AndorDevice has-a SpectrometerSettings, which has-a EEPROM, which
72
75
  # has-a MultiWavelengthCalibration
73
76
  self.settings = SpectrometerSettings(self.device_id)
74
- self.summed_spectra = None
75
- self.sum_count = 0
76
77
  self.session_reading_count = 0
77
- self.take_one = False
78
- self.failure_count = 0
78
+ self.sum_count = 0
79
+ self.take_one_request = None
80
+
79
81
  self.dll_fail = True
80
82
  self.tec_enabled = True
81
83
  self.driver = None
@@ -147,6 +149,27 @@ class AndorDevice(InterfaceDevice):
147
149
  # Private Methods
148
150
  ###############################################################
149
151
 
152
+ def _queue_message(self, setting, value):
153
+ """
154
+ If an upstream queue is defined, send the name-value pair. Does nothing
155
+ if the caller hasn't provided a queue.
156
+
157
+ "setting" is application (caller) dependent, but ENLIGHTEN currently uses
158
+ "marquee_info" and "marquee_error".
159
+ """
160
+ if self.message_queue is None:
161
+ return
162
+
163
+ msg = StatusMessage(setting, value)
164
+ try:
165
+ log.debug(f"queue_message: msg {msg}")
166
+ self.message_queue.put(msg)
167
+ except:
168
+ log.error(f"failed to enqueue StatusMessage {msg}", exc_info=1)
169
+
170
+ def not_implemented(self):
171
+ pass
172
+
150
173
  def _init_process_funcs(self):
151
174
  process_f = {}
152
175
 
@@ -158,10 +181,14 @@ class AndorDevice(InterfaceDevice):
158
181
  process_f["init_tec_setpoint"] = self.init_tec_setpoint
159
182
  process_f["set_tec_setpoint"] = self.set_tec_setpoint
160
183
  process_f["init_detector_area"] = self.init_detector_area
161
- process_f["scans_to_average"] = self.scans_to_average
184
+ process_f["scans_to_average"] = self.set_scans_to_average
162
185
  process_f["high_gain_mode_enable"] = self.high_gain_mode_enable
163
186
  process_f["save_config"] = self.save_config
164
187
  process_f["vertical_binning"] = self.set_vertical_binning
188
+ process_f["take_one_request"] = self.set_take_one_request
189
+
190
+ process_f["reset_scan_averaging"] = self.not_implemented
191
+ process_f["heartbeat"] = lambda x: None
165
192
 
166
193
  ##################################################################
167
194
  # What follows is the old init-lambdas that are squashed into process_f
@@ -204,142 +231,147 @@ class AndorDevice(InterfaceDevice):
204
231
  os.makedirs(self.config_dir)
205
232
  return os.path.isfile(self.config_file)
206
233
 
207
- def _get_spectrum_raw(self): # -> list[float]
234
+ def _get_spectrum_raw(self):
208
235
  """
209
236
  @todo missing bad-pixel correction
210
237
  """
211
238
  spec_arr = c_long * self.pixels
212
239
  spec_init_vals = [0] * self.pixels
213
- spec = spec_arr(*spec_init_vals)
240
+ spectrum = spec_arr(*spec_init_vals)
214
241
 
215
242
  # ask for spectrum then collect, NOT multithreaded (though we should look into that!), blocks
216
243
  self.driver.StartAcquisition()
217
244
  self.driver.WaitForAcquisition()
218
- success = self.driver.GetAcquiredData(spec, c_ulong(self.pixels))
245
+ result = self.driver.GetAcquiredData(spectrum, c_ulong(self.pixels))
219
246
 
220
- if (success != self.SUCCESS):
221
- log.debug(f"getting spectra did not succeed. Received code of {success}. Returning")
247
+ if (result != self.SUCCESS):
248
+ log.error(f"_get_spectrum_raw: GetAcquiredData failed (result {result})")
222
249
  return
223
250
 
224
251
  # convert from wasatch.AndorDevice.c_long_Array_512
225
- convertedSpec = [x for x in spec]
252
+ spectrum = np.array(spectrum, dtype=np.float32) # [x for x in spectrum]
226
253
 
227
254
  if (self.settings.eeprom.invert_x_axis):
228
- convertedSpec.reverse()
255
+ # spectrum.reverse()
256
+ spectrum = spectrum[::-1]
257
+
258
+ # Andor cameras can return all zeros when saturated
259
+ if not spectrum.any():
260
+ self._queue_message("marquee_error", "Andor camera is saturated")
229
261
 
230
- return convertedSpec
262
+ # log.debug(f"_get_spectrum_raw: returning spectrum {spectrum}")
263
+ return spectrum
231
264
 
232
265
  def _take_one_averaged_reading(self):
233
- averaging_enabled = (self.settings.state.scans_to_average > 1)
234
-
235
- if averaging_enabled:
236
- # collect the entire averaged spectrum at once (added for
237
- # BatchCollection with laser delay)
238
- #
239
- # So: we're NOT in "free-running" mode, so we're basically being
240
- # slaved to parent process and doing exactly what is requested
241
- # "on command." That means we can perform a big, heavy blocking
242
- # scan average all at once, because they requested it.
243
- self.sum_count = 0
244
- loop_count = self.settings.state.scans_to_average
266
+ """
267
+ @note this may be collecting a dark spectrum requested through TakeOneRequest.take_dark
268
+ @returns Reading
269
+ """
270
+
271
+ ########################################################################
272
+ # take the averaged spectrum
273
+ ########################################################################
274
+
275
+ tor = self.take_one_request
276
+ if tor is not None:
277
+ scans_to_average = tor.scans_to_average
278
+ log.debug(f"take_one_averaged_reading: taking scans_to_average from tor {tor}")
245
279
  else:
246
- # we're in free-running mode
247
- loop_count = 1
280
+ scans_to_average = self.settings.state.scans_to_average
281
+ log.debug(f"take_one_averaged_reading: taking scans_to_average from SpectrometerState")
248
282
 
249
- log.debug("take_one_averaged_reading: loop_count = %d", loop_count)
283
+ log.debug(f"take_one_averaged_reading: scans_to_average {scans_to_average}")
250
284
 
251
285
  # either take one measurement (normal), or a bunch (blocking averaging)
252
- reading = None
253
- for _ in range(0, loop_count):
254
-
255
- # start a new reading
256
- # NOTE: reading.timestamp is when reading STARTED, not FINISHED!
257
- reading = Reading(self.device_id)
258
-
259
- if self.tec_enabled:
260
- log.debug("TEC enabled, so reading temperature")
261
-
262
- use_float = True # seems to work on 785XL (WP-01635 and WP-01491)
263
-
264
- if use_float:
265
- c_temp = c_float()
266
- result = self.driver.GetTemperatureF(byref(c_temp))
267
- else:
268
- c_temp = c_int()
269
- result = self.driver.GetTemperature(byref(c_temp))
270
-
271
- label = self.get_error_code(result)
272
-
273
- if label in [ "DRV_SUCCESS",
274
- "DRV_TEMPERATURE_DRIFT",
275
- "DRV_TEMPERATURE_STABILIZED",
276
- "DRV_TEMPERATURE_NOT_REACHED",
277
- "DRV_TEMPERATURE_NOT_STABILIZED" ]:
278
- reading.detector_temperature_degC = c_temp.value
279
- log.debug(f"Andor temperature {reading.detector_temperature_degC:.2f} ({label})")
280
- else:
281
- log.error(f"unable to read detector temperature, result was {label}")
282
-
286
+ reading = Reading(self.device_id) # reading.timestamp is when reading STARTED, not FINISHED!
287
+ self.sum_count = 0
288
+ failure_count = 0
289
+ self.summed_spectrum = None
290
+ MAX_FAILURES = 3
291
+
292
+ # this loop can DELIBERATELY be changed (shortened, reset or lengthened)
293
+ # by external changes to self.sum_count and self.settings.state.scans_to_average
294
+ while self.sum_count < scans_to_average:
295
+
296
+ # monitor for external changes (but NOT new TakeOneRequest...that will appear in due process)
297
+ if scans_to_average != self.settings.state.scans_to_average:
298
+ log.debug(f"detected change in scans_to_average from {scans_to_average} to {self.settings.state.scans_to_average}")
299
+ self.scan_count = 0
300
+ scans_to_average = self.settings.state.scans_to_average
301
+ self.summed_spectrum = None
302
+
283
303
  try:
284
- reading.integration_time_ms = self.settings.state.integration_time_ms
285
- reading.laser_power_perc = self.settings.state.laser_power_perc
286
- reading.laser_power_mW = self.settings.state.laser_power_mW
287
- reading.laser_enabled = self.settings.state.laser_enabled
288
- reading.spectrum = self._get_spectrum_raw()
304
+ log.debug(f"take_one_averaged_reading: taking spectrum {self.sum_count+1} of {scans_to_average}")
305
+ spectrum = self._get_spectrum_raw()
306
+ except usb.USBError:
307
+ failure_count += 1
308
+ log.error(f"failure_count {failure_count}, encountered USB error in reading for device {self.device}", exc_info=1)
309
+ if failure_count < MAX_FAILURES:
310
+ continue
311
+
312
+ if spectrum is None or spectrum == []:
313
+ failure_count += 1
314
+ log.error(f"failure_count {failure_count}, received empty spectrum {reading.spectrum}")
315
+ if failure_count < MAX_FAILURES:
316
+ log.error(f"failure_count {failure_count}, trying again")
317
+ continue
318
+
319
+ if failure_count >= MAX_FAILURES:
320
+ msg = f"exceeded MAX_FAILURES {MAX_FAILURES} while trying to read an averaged spectrum"
321
+ log.error(msg)
322
+ return
323
+
324
+ log.debug("successfully read one spectrum")
325
+ self.sum_count += 1
326
+ if scans_to_average > 1 and self.sum_count < scans_to_average:
327
+ # MZ: why aren't we following this pattern in WasatchDevice / FeatureInterfaceDevice?
328
+ log.debug(f"floating message upstream because sum_count {self.sum_count}, scans_to_average {scans_to_average}")
329
+ self._queue_message("scan_averaging", (self.device_id, self.sum_count))
330
+
331
+ if self.summed_spectrum is None:
332
+ self.summed_spectrum = spectrum
333
+ else:
334
+ self.summed_spectrum += spectrum
335
+ log.debug(f"take_one_averaged_reading: sum_count {self.sum_count}, summed_spectrum : {self.summed_spectrum[0:9]}")
336
+
337
+ # have we completed the averaged reading?
338
+ reading.spectrum = self.summed_spectrum / self.sum_count if self.sum_count > 1 else spectrum
339
+
340
+ log.debug(f"take_one_averaged_reading: {'averaged' if self.sum_count > 1 else 'non-averaged'} spectrum : %s ...", reading.spectrum[0:9])
341
+ reading.averaged = True
342
+ reading.sum_count = self.sum_count
343
+ return reading
344
+
345
+ def get_detector_temperature_degC(self):
346
+ 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))
289
356
 
290
357
  # MZ: why were we trying to use GetTemperatureF(float) here, when above it was GetTemperature(int)?
291
358
  # temperature = c_float()
292
359
  # temp_success = self.driver.GetTemperatureF(byref(temperature))
293
360
  # reading.detector_temperature_degC = temperature.value
294
- except usb.USBError:
295
- self.failure_count += 1
296
- log.error(f"Andor Device: encountered USB error in reading for device {self.device}")
297
-
298
- if reading.spectrum is None or reading.spectrum == []:
299
- if self.failure_count > 3:
300
- return SpectrometerResponse(data=False,error_msg="exceeded failure for readings")
301
-
302
- if not reading.failure:
303
- if averaging_enabled:
304
- if self.sum_count == 0:
305
- self.summed_spectra = [float(i) for i in reading.spectrum]
306
- else:
307
- log.debug("device.take_one_averaged_reading: summing spectra")
308
- for i in range(len(self.summed_spectra)):
309
- self.summed_spectra[i] += reading.spectrum[i]
310
- self.sum_count += 1
311
- log.debug("device.take_one_averaged_reading: summed_spectra : %s ...", self.summed_spectra[0:9])
312
-
313
- # count spectra
314
- self.session_reading_count += 1
315
- reading.session_count = self.session_reading_count
316
- reading.sum_count = self.sum_count
317
-
318
- # have we completed the averaged reading?
319
- if averaging_enabled:
320
- if self.sum_count >= self.settings.state.scans_to_average:
321
- reading.spectrum = [ x / self.sum_count for x in self.summed_spectra ]
322
- log.debug("device.take_one_averaged_reading: averaged_spectrum : %s ...", reading.spectrum[0:9])
323
- reading.averaged = True
324
-
325
- # reset for next average
326
- self.summed_spectra = None
327
- self.sum_count = 0
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
328
372
  else:
329
- # if averaging isn't enabled...then a single reading is the
330
- # "averaged" final measurement (check reading.sum_count to confirm)
331
- reading.averaged = True
332
-
333
- # were we told to only take one (potentially averaged) measurement?
334
- if self.take_one and reading.averaged:
335
- log.debug("completed take_one")
336
- self.change_setting("cancel_take_one", True)
337
-
338
- log.debug("device.take_one_averaged_reading: returning %s", reading)
339
- if reading.spectrum is not None and reading.spectrum != []:
340
- self.failure_count = 0
341
- # reading.dump_area_scan()
342
- return SpectrometerResponse(data=reading)
373
+ log.error(f"unable to read detector temperature, result was {label}")
374
+
343
375
 
344
376
  def _close_ex_shutter(self):
345
377
  self.check_result(self.driver.SetShutterEx(1, 1, self.SHUTTER_SPEED_MS, self.SHUTTER_SPEED_MS, 2), "SetShutterEx(2)")
@@ -439,6 +471,10 @@ class AndorDevice(InterfaceDevice):
439
471
  self.settings.eeprom.has_cooling = True
440
472
  return SpectrometerResponse(data=True)
441
473
 
474
+ def set_take_one_request(self, tor):
475
+ log.debug(f"set_take_one_request: storing {tor}")
476
+ self.take_one_request = tor
477
+
442
478
  def set_vertical_binning(self, roi):
443
479
  """
444
480
  Note that this follows the same (start, end) vertical ROI API as
@@ -547,8 +583,54 @@ class AndorDevice(InterfaceDevice):
547
583
  self.set_tec_setpoint(self.settings.eeprom.startup_temp_degC)
548
584
 
549
585
  def acquire_data(self):
586
+ # handle TakeOneRequest.take_dark
587
+ dark_reading = None
588
+ tor = self.take_one_request
589
+ log.debug(f"acquire_data: tor {tor}")
590
+ if tor and tor.take_dark:
591
+ self.set_shutter_enable(True)
592
+ dark_reading = self._take_one_averaged_reading()
593
+ if dark is None:
594
+ return SpectrometerResponse(False, error_msg="failed to collect dark")
595
+ self.set_shutter_enable(False)
596
+
597
+ # get spectrum (potentially averaged)
550
598
  reading = self._take_one_averaged_reading()
551
- return reading
599
+ if reading is None:
600
+ return SpectrometerResponse(False, error_msg="failed to collect reading")
601
+
602
+ # fill-out metadata
603
+ if dark_reading:
604
+ reading.dark = dark_reading.spectrum
605
+ reading.integration_time_ms = self.settings.state.integration_time_ms
606
+ reading.detector_temperature_degC = self.get_detector_temperature_degC()
607
+
608
+ # only bump session_count by one for a dark-corrected averaged reading
609
+ # (regardless of how many spectra were collected to generate it)
610
+ self.session_reading_count += 1
611
+ reading.session_count = self.session_reading_count
612
+
613
+ # update or close TakeOneRequest
614
+ if tor is not None:
615
+ log.debug(f"acquire_data: adding to to reading: {tor}")
616
+ reading.take_one_request = tor
617
+
618
+ # XL units don't provide laser control
619
+ if tor.laser_warmup_ms: log.error("TakeOneRequest.laser_warmup_ms not supported")
620
+ if tor.auto_raman_request: log.error("TakeOneRequest.auto_raman_request not supported")
621
+ if tor.enable_laser_before: log.error("TakeOneRequest.enable_laser_before not supported")
622
+ if tor.disable_laser_after: log.error("TakeOneRequest.disable_laser_after not supported")
623
+
624
+ # readings_current/target are for "fast (streaming) BatchCollection"
625
+ if tor.readings_target:
626
+ tor.readings_current += 1
627
+
628
+ if not tor.readings_target or tor.readings_current >= tor.readings_target:
629
+ log.debug(f"completed {tor}")
630
+ self.take_one_request = None
631
+
632
+ log.debug(f"acquire_data: reading {reading}")
633
+ return SpectrometerResponse(data=reading)
552
634
 
553
635
  def set_shutter_enable(self, enable):
554
636
  if enable:
@@ -679,9 +761,13 @@ class AndorDevice(InterfaceDevice):
679
761
  log.debug(f"set AD channel {ADnumber} with horizontal speed {HSnumber} ({STemp})")
680
762
  return SpectrometerResponse(True)
681
763
 
682
- def scans_to_average(self, value):
764
+ def set_scans_to_average(self, value):
765
+ value = int(value)
766
+
767
+ # reset count on changes
683
768
  self.sum_count = 0
684
- self.settings.state.scans_to_average = int(value)
769
+ self.summed_spectrum = None
770
+ self.settings.state.scans_to_average = value
685
771
  return SpectrometerResponse(True)
686
772
 
687
773
  def get_error_code_long(self, code):
@@ -0,0 +1,19 @@
1
+ class AreaScanImage:
2
+
3
+ FRAME_COUNT = 0
4
+
5
+ def __init__(self, data=None, width=None, height=None, format_name=None, width_orig=None, height_orig=None, pathname_png=None, line_index=None):
6
+ self.data = data
7
+ self.width = width
8
+ self.height = height
9
+ self.width_orig = width_orig
10
+ self.height_orig = height_orig
11
+ self.format_name = format_name
12
+ self.pathname_png = pathname_png
13
+ self.line_index = line_index
14
+
15
+ self.frame_count = AreaScanImage.FRAME_COUNT
16
+ AreaScanImage.FRAME_COUNT += 1
17
+
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}>"
@@ -105,6 +105,9 @@ class AutoRaman:
105
105
  reading.sum_count = 1 # todo: get_scans_to_average()
106
106
  reading.new_integration_time_ms = None # todo: get_integration_time_ms()
107
107
  reading.new_gain_db = None # todo: get_detector_gain()
108
+ reading.laser_enabled = True
109
+ reading.laser_power_perc = hardware.settings.state.laser_power_perc
110
+ reading.laser_power_mW = hardware.settings.state.laser_power_mW
108
111
 
109
112
  return SpectrometerResponse(data=reading)
110
113
 
@@ -415,6 +418,9 @@ class AutoRaman:
415
418
  reading.sum_count = num_avg
416
419
  reading.new_integration_time_ms = int_time
417
420
  reading.new_gain_db = gain_db
421
+ reading.laser_enabled = True
422
+ reading.laser_power_perc = self.hardware.settings.state.laser_power_perc
423
+ reading.laser_power_mW = self.hardware.settings.state.laser_power_mW
418
424
 
419
425
  log.debug("done")
420
426
  return reading
@@ -5,7 +5,7 @@ class AutoRamanRequest:
5
5
  START_GAIN_DB = 0
6
6
  MAX_INTEG_MS = 2000
7
7
  MIN_INTEG_MS = 10
8
- MAX_GAIN_DB = 32
8
+ MAX_GAIN_DB = 30
9
9
  MIN_GAIN_DB = 0
10
10
  TARGET_COUNTS = 45000
11
11
  MAX_COUNTS = 50000
@@ -82,7 +82,7 @@ if __name__ == "__main__":
82
82
  start_gain_db = 0,
83
83
  max_integ_ms = 2000,
84
84
  min_integ_ms = 10,
85
- max_gain_db = 32,
85
+ max_gain_db = 30,
86
86
  min_gain_db = 0,
87
87
  target_counts = 45000,
88
88
  max_counts = 50000,
@@ -41,11 +41,10 @@ class CSVLoader:
41
41
  # default
42
42
  self.timestamp = datetime.datetime.now()
43
43
 
44
- # temporarily store these if no wavecal is provided
44
+ # define buckets into which we will load the data in load_data()
45
45
  self.metadata = {
46
46
  "pixel": []
47
47
  }
48
-
49
48
  self.headers = []
50
49
  self.processed_reading = ProcessedReading()
51
50
  self.processed_reading.reading = Reading(device_id = "LOAD:" + pathname)
@@ -104,7 +103,7 @@ class CSVLoader:
104
103
  # check for end of metadata
105
104
  looks_like_header = False
106
105
  for tok in [ part.strip().lower() for part in line ]:
107
- if tok in ["pixel", "wavelength", "wavenumber", "processed", "intensity"]:
106
+ if tok in ["pixel", "wavelength", "wavenumber", "processed", "intensity", "raw"]:
108
107
  looks_like_header = True
109
108
  break
110
109
 
@@ -181,4 +180,9 @@ class CSVLoader:
181
180
  # clear any arrays we ended up not filling
182
181
  self.processed_reading.post_load_cleanup()
183
182
 
183
+ log.debug("returning processed_reading:")
184
+ self.processed_reading.dump()
185
+
186
+ log.debug(f"returning metadata {self.metadata}")
187
+
184
188
  return self.processed_reading, self.metadata