wasatch 2.3.2__py3-none-any.whl

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 (53) hide show
  1. wasatch/AbstractUSBDevice.py +29 -0
  2. wasatch/AndorDevice.py +910 -0
  3. wasatch/AreaScanImage.py +19 -0
  4. wasatch/AutoRaman.py +457 -0
  5. wasatch/AutoRamanRequest.py +96 -0
  6. wasatch/BLEDevice.py +1032 -0
  7. wasatch/BalanceAcquisition.py +136 -0
  8. wasatch/CSVLoader.py +188 -0
  9. wasatch/CommandSettings.py +95 -0
  10. wasatch/ControlObject.py +17 -0
  11. wasatch/DetectorROI.py +36 -0
  12. wasatch/DetectorRegions.py +158 -0
  13. wasatch/DeviceFinderBLE.py +107 -0
  14. wasatch/DeviceFinderTCP.py +20 -0
  15. wasatch/DeviceFinderUSB.py +236 -0
  16. wasatch/DeviceID.py +344 -0
  17. wasatch/EEPROM.py +1450 -0
  18. wasatch/EEPROM_multical.sav +48 -0
  19. wasatch/FPGAOptions.py +125 -0
  20. wasatch/FeatureIdentificationDevice.py +3961 -0
  21. wasatch/FirmwareRequirements.py +76 -0
  22. wasatch/HardwareInfo.py +36 -0
  23. wasatch/IDSCamera.py +916 -0
  24. wasatch/IDSDevice.py +329 -0
  25. wasatch/IMX385.py +179 -0
  26. wasatch/InterfaceDevice.py +40 -0
  27. wasatch/MockUSBDevice.py +353 -0
  28. wasatch/OceanDevice.py +215 -0
  29. wasatch/PollStatus.py +15 -0
  30. wasatch/ProcessedReading.py +352 -0
  31. wasatch/ROI.py +33 -0
  32. wasatch/Reading.py +80 -0
  33. wasatch/RealUSBDevice.py +64 -0
  34. wasatch/SPIDevice.py +628 -0
  35. wasatch/SpectrometerRequest.py +15 -0
  36. wasatch/SpectrometerResponse.py +38 -0
  37. wasatch/SpectrometerSettings.py +446 -0
  38. wasatch/SpectrometerState.py +255 -0
  39. wasatch/StatusMessage.py +14 -0
  40. wasatch/TCPDevice.py +409 -0
  41. wasatch/TakeOneRequest.py +43 -0
  42. wasatch/WasatchBus.py +87 -0
  43. wasatch/WasatchDevice.py +1060 -0
  44. wasatch/WasatchDeviceWrapper.py +452 -0
  45. wasatch/WrapperWorker.py +304 -0
  46. wasatch/__init__.py +4 -0
  47. wasatch/applog.py +177 -0
  48. wasatch/simulation_protocol.py +227 -0
  49. wasatch/utils.py +731 -0
  50. wasatch-2.3.2.dist-info/LICENSE +21 -0
  51. wasatch-2.3.2.dist-info/METADATA +347 -0
  52. wasatch-2.3.2.dist-info/RECORD +53 -0
  53. wasatch-2.3.2.dist-info/WHEEL +4 -0
wasatch/AndorDevice.py ADDED
@@ -0,0 +1,910 @@
1
+ import os
2
+ import usb
3
+ import json
4
+ import numpy as np
5
+ import struct
6
+ import logging
7
+ import datetime
8
+
9
+ from ctypes import *
10
+
11
+ from .SpectrometerSettings import SpectrometerSettings
12
+ from .SpectrometerResponse import SpectrometerResponse
13
+ from .InterfaceDevice import InterfaceDevice
14
+ from .StatusMessage import StatusMessage
15
+ from .DeviceID import DeviceID
16
+ from .Reading import Reading
17
+ from .ROI import ROI
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+ class AndorDevice(InterfaceDevice):
22
+ """
23
+ This is the basic implementation of our interface with Andor cameras
24
+
25
+ @todo have check_result return a SpectrometerResponse
26
+ @todo try to auto-detect whether x-axis needs inverted via DLL.GetImageFlip()
27
+
28
+ ##########################################################################
29
+ This class adopts the external device interface structure.
30
+ This involves receiving a request through the handle_request function.
31
+ A request is processed based on the key in the request.
32
+ The processing function passes the commands to the requested device.
33
+ Once it receives a response from the connected device it then passes that
34
+ back up the chain.
35
+ @verbatim
36
+ Enlighten Request
37
+ |
38
+ handle_requests
39
+ |
40
+ ------------
41
+ / / | \ \
42
+ { get_laser status, acquire, set_laser_watchdog, etc....}
43
+ \ \ | / /
44
+ ------------
45
+ |
46
+ {self.driver.some_andor_sdk_call}
47
+ @endverbatim
48
+ ############################################################################
49
+ """
50
+
51
+ SUCCESS = 20002 #!< see load_error_codes()
52
+ SHUTTER_SPEED_MS = 50 #!< allow time for mechanical shutter to stabilize
53
+
54
+ def __init__(self, device_id, message_queue=None, alert_queue=None):
55
+ # if passed a string representation of a DeviceID, deserialize it
56
+ super().__init__()
57
+ if type(device_id) is str:
58
+ device_id = DeviceID(label=device_id)
59
+
60
+ self.device_id = device_id
61
+ self.message_queue = message_queue
62
+ self.alert_queue = alert_queue
63
+
64
+ self.load_error_codes()
65
+
66
+ self.connected = False
67
+
68
+ # Receives ENLIGHTEN's 'change settings' commands in the spectrometer
69
+ # process.
70
+ self.command_queue = []
71
+
72
+ self.immediate_mode = False
73
+
74
+ # An AndorDevice has-a SpectrometerSettings, which has-a EEPROM, which
75
+ # has-a MultiWavelengthCalibration
76
+ self.settings = SpectrometerSettings(self.device_id)
77
+ self.session_reading_count = 0
78
+ self.sum_count = 0
79
+ self.take_one_request = None
80
+
81
+ self.dll_fail = True
82
+ self.tec_enabled = True
83
+ self.driver = None
84
+
85
+ self.process_id = os.getpid()
86
+ self.last_memory_check = datetime.datetime.now()
87
+ self.last_battery_percentage = 0
88
+ self.spec_index = 0
89
+ self._scan_averaging = 1
90
+ self.dark = None
91
+ self.boxcar_half_width = 0
92
+
93
+ # decide appropriate DLL filename for architecture
94
+ arch = 64 if 64 == struct.calcsize("P") * 8 else 32
95
+ filename = f"atmcd{arch}d.dll"
96
+
97
+ # Andor libraries may be found in various locations
98
+ dll_paths = [ r"C:\Program Files\Andor Driver Pack 2",
99
+ r"C:\Program Files\Andor SDK",
100
+ r"dist\Andor",
101
+ r"dist" ]
102
+
103
+ # try to find correct DLL in any known location
104
+ for path in dll_paths:
105
+ pathname = os.path.join(path, filename)
106
+ if os.path.exists(pathname):
107
+ try:
108
+ log.debug(f"attempting to load {pathname}")
109
+ self.driver = cdll.LoadLibrary(pathname)
110
+ self.dll_fail = False
111
+ except Exception as e:
112
+ log.error(f"Error loading {pathname}: {e}")
113
+
114
+ if self.driver is not None:
115
+ break
116
+
117
+ if self.driver is None:
118
+ log.error(f"could not find {filename} in search path: {dll_paths}")
119
+ # MZ: interesting that we don't return here
120
+
121
+ # "serial_number", "model" etc are ambiguous in an Andor configuration
122
+ # file -- do they refer to the camera (Andor), or the spectrometer
123
+ # (Wasatch)? Therefore, some Wasatch EEPROM fields get extra "wp_"
124
+ # prefixes in Andor configuration files to be clear.
125
+ self.config_names_to_eeprom = {
126
+ 'wp_serial_number': 'serial_number',
127
+ 'wp_model': 'model'
128
+ }
129
+
130
+ # set Andor defaults for important "EEPROM" settings
131
+ # (all but has_cooling can be overridden via config file)
132
+
133
+ # Andor API doesn't(?) seem to have access to detector info.
134
+ # Note that we use non-iDus cameras, including the Newton.
135
+ self.settings.eeprom.detector = "iDus"
136
+
137
+ self.settings.eeprom.has_cooling = True
138
+ self.settings.eeprom.startup_integration_time_ms = 10
139
+ self.settings.eeprom.startup_temp_degC = -60
140
+ self.settings.eeprom.detector_gain = 1
141
+ self.settings.eeprom.detector_gain_odd = 1
142
+
143
+ # MultiWavelengthCalibration attributes
144
+ self.settings.eeprom.multi_wavelength_calibration.set("wavelength_coeffs", [0,1,0,0,0])
145
+
146
+ self.process_f = self._init_process_funcs()
147
+
148
+ ###############################################################
149
+ # Private Methods
150
+ ###############################################################
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
+
173
+ def _init_process_funcs(self):
174
+ process_f = {}
175
+
176
+ process_f["connect"] = self.connect
177
+ process_f["acquire_data"] = self.acquire_data
178
+ process_f["set_shutter_enable"] = self.set_shutter_enable
179
+ process_f["set_integration_time_ms"] = self.set_integration_time_ms
180
+ process_f["get_serial_number"] = self.get_serial_number
181
+ process_f["init_tec_setpoint"] = self.init_tec_setpoint
182
+ process_f["set_tec_setpoint"] = self.set_tec_setpoint
183
+ process_f["init_detector_area"] = self.init_detector_area
184
+ process_f["scans_to_average"] = self.set_scans_to_average
185
+ process_f["high_gain_mode_enable"] = self.high_gain_mode_enable
186
+ process_f["save_config"] = self.save_config
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"] = self.not_implemented
192
+
193
+ ##################################################################
194
+ # What follows is the old init-lambdas that are squashed into process_f
195
+ # Long term, the upstream requests should be changed to match the new format
196
+ # This is an easy fix for the time being to make things behave
197
+ ##################################################################
198
+ process_f["integration_time_ms"] = lambda x: self.set_integration_time_ms(x)
199
+ process_f["fan_enable"] = lambda x: self.set_fan_enable(bool(x))
200
+ process_f["shutter_enable"] = lambda x: self.set_shutter_enable(bool(x))
201
+ process_f["detector_tec_enable"] = lambda x: self.toggle_tec(bool(x))
202
+ process_f["detector_tec_setpoint_degC"] = lambda x: self.set_tec_setpoint(int(round(x)))
203
+
204
+ return process_f
205
+
206
+ def high_gain_mode_enable(self, enabled):
207
+ if enabled:
208
+ result = self.driver.SetPreAmpGain(self.gain_idx[-1])
209
+ assert(self.SUCCESS == result), f"unable to set detector gain, got value of {result}"
210
+ log.debug(f"for {enabled} setting gain to {self.gain_options[-1]}")
211
+ return
212
+ else:
213
+ result = self.driver.SetPreAmpGain(self.gain_idx[0])
214
+ assert(self.SUCCESS == result), f"unable to set detector gain, got value of {result}"
215
+ log.debug(f"for {enabled} setting gain to {self.gain_options[0]}")
216
+ return
217
+
218
+ def set_fan_enable(self, x):
219
+ self.check_result(self.driver.SetFanMode(int(x)), f"Andor Fan On {x}")
220
+ return SpectrometerResponse()
221
+
222
+ def _get_default_data_dir(self):
223
+ if os.name == "nt":
224
+ return os.path.join(os.path.expanduser("~"), "Documents", "EnlightenSpectra")
225
+ return os.path.join(os.environ["HOME"], "EnlightenSpectra")
226
+
227
+ def _check_config_file(self):
228
+ self.config_dir = os.path.join(self._get_default_data_dir(), 'config')
229
+ self.config_file = os.path.join(self.config_dir, self.serial + '.json')
230
+ if not os.path.exists(self.config_dir):
231
+ os.makedirs(self.config_dir)
232
+ return os.path.isfile(self.config_file)
233
+
234
+ def _get_spectrum_raw(self):
235
+ """
236
+ @todo missing bad-pixel correction
237
+ """
238
+ spec_arr = c_long * self.pixels
239
+ spec_init_vals = [0] * self.pixels
240
+ spectrum = spec_arr(*spec_init_vals)
241
+
242
+ # ask for spectrum then collect, NOT multithreaded (though we should look into that!), blocks
243
+ self.driver.StartAcquisition()
244
+ self.driver.WaitForAcquisition()
245
+ result = self.driver.GetAcquiredData(spectrum, c_ulong(self.pixels))
246
+
247
+ if (result != self.SUCCESS):
248
+ log.error(f"_get_spectrum_raw: GetAcquiredData failed (result {result})")
249
+ return
250
+
251
+ # convert from wasatch.AndorDevice.c_long_Array_512
252
+ spectrum = np.array(spectrum, dtype=np.float32) # [x for x in spectrum]
253
+
254
+ if (self.settings.eeprom.invert_x_axis):
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")
261
+
262
+ # log.debug(f"_get_spectrum_raw: returning spectrum {spectrum}")
263
+ return spectrum
264
+
265
+ def _take_one_averaged_reading(self):
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}")
279
+ else:
280
+ scans_to_average = self.settings.state.scans_to_average
281
+ log.debug(f"take_one_averaged_reading: taking scans_to_average from SpectrometerState")
282
+
283
+ log.debug(f"take_one_averaged_reading: scans_to_average {scans_to_average}")
284
+
285
+ # either take one measurement (normal), or a bunch (blocking averaging)
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
+
303
+ try:
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))
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
372
+ else:
373
+ log.error(f"unable to read detector temperature, result was {label}")
374
+
375
+
376
+ def _close_ex_shutter(self):
377
+ self.check_result(self.driver.SetShutterEx(1, 1, self.SHUTTER_SPEED_MS, self.SHUTTER_SPEED_MS, 2), "SetShutterEx(2)")
378
+ self.settings.state.shutter_enabled = False
379
+ return SpectrometerResponse(True)
380
+
381
+ def _open_ex_shutter(self):
382
+ self.check_result(self.driver.SetShutterEx(1, 1, self.SHUTTER_SPEED_MS, self.SHUTTER_SPEED_MS, 1), "SetShutterEx(1)")
383
+ self.settings.state.shutter_enabled = True
384
+ return SpectrometerResponse(True)
385
+
386
+ ###############################################################
387
+ # Public Methods
388
+ ###############################################################
389
+
390
+ def check_result(self, result, func):
391
+ if result != self.SUCCESS:
392
+ name = self.get_error_code(result)
393
+ msg = f"error calling {func}: {result} ({name})"
394
+ log.error(msg)
395
+ raise RuntimeError(msg)
396
+ log.debug(f"successfully called {func}")
397
+
398
+ def connect(self):
399
+ if self.dll_fail:
400
+ return SpectrometerResponse(False, error_msg="can't find Andor DLL; please confirm Andor Driver Pack 2 installed")
401
+
402
+ cameraHandle = c_int()
403
+ self.check_result(self.driver.GetCameraHandle(self.spec_index, byref(cameraHandle)), "GetCameraHandle") # step 1
404
+ self.check_result(self.driver.SetCurrentCamera(cameraHandle.value), "SetCurrentCamera") # step 2
405
+
406
+ try:
407
+ path_to_ini = create_string_buffer(b'\000' * 256)
408
+ self.check_result(self.driver.Initialize(path_to_ini), "Initialize") # step 3
409
+ except:
410
+ log.error("Andor.Initialize failed", exc_info=1)
411
+ return SpectrometerResponse(False, error_msg="Andor initialization failed")
412
+
413
+ # @todo missing: step 4 capabilities
414
+
415
+ self.get_serial_number() # step 16
416
+ self.init_tec_setpoint() # step 5+6
417
+ self.init_detector_area() # step 7
418
+
419
+ if not self._check_config_file():
420
+ log.debug("stubbing Andor EEPROM")
421
+ self.settings.eeprom.stubbed = True
422
+ self.config_values = {
423
+ 'detector_serial_number': self.serial,
424
+ 'wavelength_coeffs': [0,1,0,0,0],
425
+ 'excitation_nm_float': 0,
426
+ 'raman_intensity_coeffs': [],
427
+ 'raman_intensity_calibration_order': 0,
428
+ 'invert_x_axis': False,
429
+ 'roi_horizontal_start': 0,
430
+ 'roi_horizontal_end': 0,
431
+ 'roi_vertical_region_1_start': 0,
432
+ 'roi_vertical_region_1_end': 0,
433
+ 'stubbed': True
434
+ }
435
+ log.debug(f"connect: config file not found, so defaulting to these: {self.config_values}")
436
+ self.save_config()
437
+ else:
438
+ self._load_config_values()
439
+ log.debug(f"connect: loaded config file: {self.config_values}")
440
+
441
+ self.check_result(self.driver.CoolerON(), "CoolerON") # step 8
442
+ self.check_result(self.driver.SetAcquisitionMode(1), "SetAcquisitionMode(single_scan)") # step 9
443
+ self.check_result(self.driver.SetTriggerMode(0), "SetTriggerMode") # step 10
444
+ self.check_result(self.driver.SetReadMode(0), "SetReadMode(full_vertical_binning)") # step 11
445
+
446
+ self.init_detector_speed() # step 12+13
447
+
448
+ # step 14
449
+ self.check_result(self.driver.SetShutterEx(1, 1, self.SHUTTER_SPEED_MS, self.SHUTTER_SPEED_MS, 0), "SetShutterEx(fully automatic external with internal always open)")
450
+ self.settings.state.shutter_enabled = True
451
+
452
+ # step 15
453
+ self.set_integration_time_ms(self.settings.eeprom.startup_integration_time_ms)
454
+
455
+ # step 17 (WasatchNET doesn't do this)
456
+ self._obtain_gain_info()
457
+
458
+ # step 18 (WasatchNET doesn't do this)
459
+ roi = ROI(self.settings.eeprom.roi_vertical_region_1_start,
460
+ self.settings.eeprom.roi_vertical_region_1_end)
461
+ if roi.start != 0 and roi.end != 0:
462
+ # although camera can probably support the (0, 0) case, it's
463
+ # convenient to treat as unconfigured defaults
464
+ self.set_vertical_binning(roi)
465
+
466
+ # success!
467
+ log.info("AndorDevice successfully connected")
468
+
469
+ self.connected = True
470
+ self.settings.eeprom.active_pixels_horizontal = self.pixels
471
+ self.settings.eeprom.has_cooling = True
472
+ return SpectrometerResponse(data=True)
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
+
478
+ def set_vertical_binning(self, roi):
479
+ """
480
+ Note that this follows the same (start, end) vertical ROI API as
481
+ FeatureInterfaceDevice.set_vertical_binning, and dynamically translates
482
+ that into the (middle, height) Andor API.
483
+ """
484
+
485
+ if isinstance(roi, ROI):
486
+ start, end = roi.start, roi.end
487
+ elif len(roi) == 2:
488
+ start, end = roi[0], roi[1]
489
+ else:
490
+ log.error("set_vertical_binning requires an ROI object or tuple of (start, stop) lines")
491
+ return SpectrometerResponse(data=False, error_msg="invalid start and stop lines")
492
+
493
+ if start < 0 or end < 0:
494
+ log.error("set_vertical_binning requires POSITIVE (start, stop) lines")
495
+ return SpectrometerResponse(data=False, error_msg="invalid start and stop lines")
496
+
497
+ # enforce ascending order
498
+ if start >= end:
499
+ log.error("set_vertical_binning requires ascending order (ignoring %d, %d)", start, end)
500
+ return SpectrometerResponse(data=False, error_msg="invalid start and stop lines")
501
+
502
+
503
+ height = end - start
504
+ center = int(round(height / 2, 0)) + start
505
+
506
+ log.debug(f"setting Single-Track vertical binning of ROI (start {start}, end {end}) (center {center}, height {height})")
507
+ self.check_result(self.driver.SetReadMode(3), "SetReadMode(single-track)")
508
+ self.check_result(self.driver.SetSingleTrack(center, height), "SetSingleTrack")
509
+
510
+ return SpectrometerResponse(data=True)
511
+
512
+ def save_config(self, eeprom=None):
513
+ """
514
+ The user has edited the "virtual EEPROM", for instance using ENLIGHTEN's
515
+ EEPROM Editor, and wants to save the new EEPROM. Therefore we need to
516
+ generate a fresh JSON equivalent and write it to disk.
517
+
518
+ @param eeprom: if provided, overwrite current settings with those in the
519
+ passed dict before writing to disk
520
+ """
521
+ if eeprom is not None:
522
+ self.update_config_from_eeprom(eeprom)
523
+
524
+ f = open(self.config_file, 'w')
525
+ json.dump(self.config_values, f, indent=2, sort_keys=True)
526
+ log.debug(f"saved {self.config_file}: {self.config_values}")
527
+
528
+ def update_config_from_eeprom(self, eeprom):
529
+ """
530
+ Populates a dict used to update the configuration file `self.config_file`
531
+ from `self.settings.eeprom` members.
532
+ """
533
+ # first, copy over any EEPROM fields which have a different name in
534
+ # wasatch.EEPROM vs the external JSON file
535
+ for json_name, python_name in self.config_names_to_eeprom.items():
536
+ self.config_values[json_name] = eeprom.multi_wavelength_calibration.get(python_name)
537
+
538
+ # now do all the standard attributes of the wasatch.EEPROM, adding
539
+ # their values into the same dict
540
+ for python_name in eeprom.__dict__:
541
+ if python_name in self.config_values:
542
+ self.config_values[python_name] = eeprom.multi_wavelength_calibration.get(python_name)
543
+
544
+ def _load_config_values(self):
545
+ """
546
+ Loads configuration from file `self.config_file` and populates `self.settings.eeprom` with members.
547
+ """
548
+ f = open(self.config_file,)
549
+ self.config_values = dict(json.load(f))
550
+ log.debug(f"loaded {self.config_file}: {self.config_values}")
551
+
552
+ # handle wp_ prefixes
553
+ for k, v in self.config_names_to_eeprom.items():
554
+ if k in self.config_values:
555
+ self.settings.eeprom.multi_wavelength_calibration.set(v, self.config_values[k])
556
+
557
+ # same spelling
558
+ for k in [ 'model',
559
+ 'stubbed',
560
+ 'detector',
561
+ 'serial_number',
562
+ 'invert_x_axis',
563
+ 'wavelength_coeffs',
564
+ 'excitation_nm_float',
565
+ 'roi_horizontal_end',
566
+ 'roi_horizontal_start',
567
+ 'roi_vertical_region_1_start',
568
+ 'roi_vertical_region_1_end',
569
+ 'raman_intensity_coeffs',
570
+ 'raman_intensity_calibration_order',
571
+ 'startup_temp_degC',
572
+ 'startup_integration_time_ms' ]:
573
+ if k in self.config_values:
574
+ self.settings.eeprom.multi_wavelength_calibration.set(k, self.config_values[k])
575
+
576
+ # default missing-but-obvious fields
577
+ if "raman_intensity_coeffs" in self.config_values:
578
+ if "raman_intensity_calibration_order" not in self.config_values:
579
+ self.settings.eeprom.multi_wavelength_calibration.set("raman_intensity_calibration_order", len(self.settings.eeprom.raman_intensity_coeffs) - 1)
580
+
581
+ # post-load initialization
582
+ if 'startup_temp_degC' in self.config_values:
583
+ self.set_tec_setpoint(self.settings.eeprom.startup_temp_degC)
584
+
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)
598
+ reading = self._take_one_averaged_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)
634
+
635
+ def set_shutter_enable(self, enable):
636
+ if enable:
637
+ return self._open_ex_shutter()
638
+ else:
639
+ return self._close_ex_shutter()
640
+
641
+ def set_integration_time_ms(self, ms):
642
+ self.integration_time_ms = ms
643
+ log.debug(f"setting integration time to {self.integration_time_ms}ms")
644
+
645
+ exposure = c_float()
646
+ accumulate = c_float()
647
+ kinetic = c_float()
648
+
649
+ sec = ms / 1000.0
650
+ self.check_result(self.driver.SetExposureTime(c_float(sec)), f"SetExposureTime({sec})")
651
+ self.check_result(self.driver.GetAcquisitionTimings(byref(exposure), byref(accumulate), byref(kinetic)), "GetAcquisitionTimings")
652
+ log.debug(f"read integration time of {exposure.value:.3f}sec (expected {ms}ms)")
653
+ return SpectrometerResponse(data=True)
654
+
655
+ def get_serial_number(self): # -> SpectrometerResponse
656
+ sn = c_int()
657
+ self.check_result(self.driver.GetCameraSerialNumber(byref(sn)), "GetCameraSerialNumber")
658
+ self.serial = f"CCD-{sn.value}"
659
+ self.settings.eeprom.serial_number = self.serial # temporary
660
+ self.settings.eeprom.detector_serial_number = self.serial
661
+ log.debug(f"get_serial_number: connected to {self.serial}")
662
+ return SpectrometerResponse(True)
663
+
664
+ def init_tec_setpoint(self): # -> SpectrometerResponse
665
+ minTemp = c_int()
666
+ maxTemp = c_int()
667
+ self.check_result(self.driver.GetTemperatureRange(byref(minTemp), byref(maxTemp)), "GetTemperatureRange") # step 5
668
+
669
+ self.settings.eeprom.max_temp_degC = maxTemp.value
670
+ self.settings.eeprom.min_temp_degC = minTemp.value
671
+
672
+ # commenting-out because Andor camera is reporting -120C for a device
673
+ # only rated at -60C...leaving hardcoded default for now
674
+ #
675
+ # self.settings.eeprom.startup_temp_degC = minTemp.value
676
+
677
+ # however the startup temperature was set (hardcode, JSON, clamped to min)...apply it
678
+ self.setpoint_deg_c = self.settings.eeprom.startup_temp_degC
679
+ self.check_result(self.driver.SetTemperature(self.setpoint_deg_c), f"SetTemperature({self.setpoint_deg_c})") # step 6
680
+ log.debug(f"set TEC to {self.setpoint_deg_c}°C (range {self.settings.eeprom.min_temp_degC}, {self.settings.eeprom.max_temp_degC})")
681
+
682
+ return SpectrometerResponse(True)
683
+
684
+ def toggle_tec(self, toggle_state):
685
+ c_toggle = c_int(toggle_state)
686
+ self.tec_enabled = c_toggle.value
687
+ if self.tec_enabled:
688
+ self.check_result(self.driver.CoolerON(), "CoolerON")
689
+ else:
690
+ self.check_result(self.driver.CoolerOFF(), "CoolerOFF")
691
+ return SpectrometerResponse(True)
692
+
693
+ def set_tec_setpoint(self, set_temp):
694
+ if set_temp < self.settings.eeprom.min_temp_degC or set_temp > self.settings.eeprom.max_temp_degC:
695
+ log.error(f"requested temp of {set_temp}, but it is outside range ({self.settings.eeprom.min_temp_degC}C, {self.settings.eeprom.max_temp_degC}C)")
696
+ return
697
+ if not self.tec_enabled:
698
+ log.error(f"returning because tec_enabled {self.tec_enabled}")
699
+ return
700
+ self.setpoint_deg_c = set_temp
701
+ # I don't think CoolerON should need to be called, but I'm not seeing temperature changes
702
+ # when it is not present here.
703
+ self.check_result(self.driver.CoolerON(), "CoolerON")
704
+ self.check_result(self.driver.SetTemperature(self.setpoint_deg_c), f"SetTemperature({self.setpoint_deg_c})")
705
+ return SpectrometerResponse(True)
706
+
707
+ def init_detector_area(self):
708
+ xPixels = c_int()
709
+ yPixels = c_int()
710
+ self.check_result(self.driver.GetDetector(byref(xPixels), byref(yPixels)), "GetDetector(x, y)")
711
+ self.pixels = xPixels.value
712
+ self.height = yPixels.value
713
+ log.debug(f"detector {self.pixels} width x {self.height} height")
714
+ return SpectrometerResponse(True)
715
+
716
+ def _obtain_gain_info(self):
717
+ num_gains = c_int()
718
+ result = self.driver.GetNumberPreAmpGains(byref(num_gains))
719
+ assert(self.SUCCESS == result), f"unable to get number of gains. Got result {result}"
720
+ log.debug(f"got number of gains is {num_gains.value}")
721
+ self.gain_options = []
722
+ self.gain_idx = []
723
+ spec_gain_opt = c_float()
724
+ for i in range(num_gains.value):
725
+ result = self.driver.GetPreAmpGain(i, byref(spec_gain_opt))
726
+ assert(self.SUCCESS == result), f"unable to get gains index {i}. Got result {result}"
727
+ self.gain_options.append(spec_gain_opt.value)
728
+ self.gain_idx.append(i)
729
+ self.gain_idx = self.gain_idx[::-1]
730
+ self.gain_options = self.gain_options[::-1]
731
+ log.debug(f"obtained gain options for spec, values were {self.gain_options}")
732
+
733
+ def init_detector_speed(self):
734
+ speed = c_float()
735
+
736
+ # for CCDs, set vertical to recommended
737
+ if self.height > 1:
738
+ VSnumber = c_int()
739
+ self.check_result(self.driver.GetFastestRecommendedVSSpeed(byref(VSnumber), byref(speed)), "GetFastestRecommendedVSSpeed") # step 12
740
+ self.check_result(self.driver.SetVSSpeed(VSnumber.value), f"SetVSSpeed({VSnumber.value})")
741
+ else:
742
+ log.debug("vertical speed does not apply to linear array detectors")
743
+
744
+ # set horizontal to max
745
+ nAD = c_int()
746
+ sIndex = c_int()
747
+ STemp = 0.0
748
+ HSnumber = 0
749
+ ADnumber = 0
750
+ self.check_result(self.driver.GetNumberADChannels(byref(nAD)), "GetNumberADChannels") # step 13.1
751
+ for iAD in range(nAD.value):
752
+ self.check_result(self.driver.GetNumberHSSpeeds(iAD, 0, byref(sIndex)), f"GetNumberHSSpeeds({iAD})") # step 13.2
753
+ for iSpeed in range(sIndex.value):
754
+ self.check_result(self.driver.GetHSSpeed(iAD, 0, iSpeed, byref(speed)), f"GetHSSpeed(iAD {iAD}, iSpeed {iSpeed})") # step 13.3
755
+ if speed.value > STemp:
756
+ STemp = speed.value
757
+ HSnumber = iSpeed
758
+ ADnumber = iAD
759
+ self.check_result(self.driver.SetADChannel(ADnumber), f"SetADChannel({ADnumber})") # 13.4
760
+ self.check_result(self.driver.SetHSSpeed(0, HSnumber), f"SetHSSpeed({HSnumber})") # 13.5
761
+ log.debug(f"set AD channel {ADnumber} with horizontal speed {HSnumber} ({STemp})")
762
+ return SpectrometerResponse(True)
763
+
764
+ def set_scans_to_average(self, value):
765
+ value = int(value)
766
+
767
+ # reset count on changes
768
+ self.sum_count = 0
769
+ self.summed_spectrum = None
770
+ self.settings.state.scans_to_average = value
771
+ return SpectrometerResponse(True)
772
+
773
+ def get_error_code_long(self, code):
774
+ if code in self.error_codes:
775
+ return f"{code} ({self.error_codes[code]})"
776
+ return f"{code} (UNKNOWN_ANDOR_ERROR)"
777
+
778
+ def get_error_code(self, code):
779
+ if code in self.error_codes:
780
+ return self.error_codes[code]
781
+ return "UNKNOWN_ANDOR_ERROR"
782
+
783
+ ## @see ATMCD32D.H
784
+ def load_error_codes(self):
785
+ self.error_codes = {
786
+ 20001: "DRV_ERROR_CODES",
787
+ 20002: "DRV_SUCCESS",
788
+ 20003: "DRV_VXDNOTINSTALLED",
789
+ 20004: "DRV_ERROR_SCAN",
790
+ 20005: "DRV_ERROR_CHECK_SUM",
791
+ 20006: "DRV_ERROR_FILELOAD",
792
+ 20007: "DRV_UNKNOWN_FUNCTION",
793
+ 20008: "DRV_ERROR_VXD_INIT",
794
+ 20009: "DRV_ERROR_ADDRESS",
795
+ 20010: "DRV_ERROR_PAGELOCK",
796
+ 20011: "DRV_ERROR_PAGEUNLOCK",
797
+ 20012: "DRV_ERROR_BOARDTEST",
798
+ 20013: "DRV_ERROR_ACK",
799
+ 20014: "DRV_ERROR_UP_FIFO",
800
+ 20015: "DRV_ERROR_PATTERN",
801
+ 20017: "DRV_ACQUISITION_ERRORS",
802
+ 20018: "DRV_ACQ_BUFFER",
803
+ 20019: "DRV_ACQ_DOWNFIFO_FULL",
804
+ 20020: "DRV_PROC_UNKONWN_INSTRUCTION",
805
+ 20021: "DRV_ILLEGAL_OP_CODE",
806
+ 20022: "DRV_KINETIC_TIME_NOT_MET",
807
+ 20023: "DRV_ACCUM_TIME_NOT_MET",
808
+ 20024: "DRV_NO_NEW_DATA",
809
+ 20025: "DRV_PCI_DMA_FAIL",
810
+ 20026: "DRV_SPOOLERROR",
811
+ 20027: "DRV_SPOOLSETUPERROR",
812
+ 20028: "DRV_FILESIZELIMITERROR",
813
+ 20029: "DRV_ERROR_FILESAVE",
814
+ 20033: "DRV_TEMPERATURE_CODES",
815
+ 20034: "DRV_TEMPERATURE_OFF",
816
+ 20035: "DRV_TEMPERATURE_NOT_STABILIZED",
817
+ 20036: "DRV_TEMPERATURE_STABILIZED",
818
+ 20037: "DRV_TEMPERATURE_NOT_REACHED",
819
+ 20038: "DRV_TEMPERATURE_OUT_RANGE",
820
+ 20039: "DRV_TEMPERATURE_NOT_SUPPORTED",
821
+ 20040: "DRV_TEMPERATURE_DRIFT",
822
+ 20049: "DRV_GENERAL_ERRORS",
823
+ 20050: "DRV_INVALID_AUX",
824
+ 20051: "DRV_COF_NOTLOADED",
825
+ 20052: "DRV_FPGAPROG",
826
+ 20053: "DRV_FLEXERROR",
827
+ 20054: "DRV_GPIBERROR",
828
+ 20055: "DRV_EEPROMVERSIONERROR",
829
+ 20064: "DRV_DATATYPE",
830
+ 20065: "DRV_DRIVER_ERRORS",
831
+ 20066: "DRV_P1INVALID", # param #1
832
+ 20067: "DRV_P2INVALID", # param #2
833
+ 20068: "DRV_P3INVALID", # param #3
834
+ 20069: "DRV_P4INVALID", # param #4
835
+ 20070: "DRV_INIERROR",
836
+ 20071: "DRV_COFERROR",
837
+ 20072: "DRV_ACQUIRING",
838
+ 20073: "DRV_IDLE",
839
+ 20074: "DRV_TEMPCYCLE",
840
+ 20075: "DRV_NOT_INITIALIZED",
841
+ 20076: "DRV_P5INVALID",
842
+ 20077: "DRV_P6INVALID",
843
+ 20078: "DRV_INVALID_MODE",
844
+ 20079: "DRV_INVALID_FILTER",
845
+ 20080: "DRV_I2CERRORS",
846
+ 20081: "DRV_I2CDEVNOTFOUND",
847
+ 20082: "DRV_I2CTIMEOUT",
848
+ 20083: "DRV_P7INVALID",
849
+ 20084: "DRV_P8INVALID",
850
+ 20085: "DRV_P9INVALID",
851
+ 20086: "DRV_P10INVALID",
852
+ 20087: "DRV_P11INVALID",
853
+ 20089: "DRV_USBERROR",
854
+ 20090: "DRV_IOCERROR",
855
+ 20091: "DRV_VRMVERSIONERROR",
856
+ 20092: "DRV_GATESTEPERROR",
857
+ 20093: "DRV_USB_INTERRUPT_ENDPOINT_ERROR",
858
+ 20094: "DRV_RANDOM_TRACK_ERROR",
859
+ 20095: "DRV_INVALID_TRIGGER_MODE",
860
+ 20096: "DRV_LOAD_FIRMWARE_ERROR",
861
+ 20097: "DRV_DIVIDE_BY_ZERO_ERROR",
862
+ 20098: "DRV_INVALID_RINGEXPOSURES",
863
+ 20099: "DRV_BINNING_ERROR",
864
+ 20100: "DRV_INVALID_AMPLIFIER",
865
+ 20101: "DRV_INVALID_COUNTCONVERT_MODE",
866
+ 20102: "DRV_USB_INTERRUPT_ENDPOINT_TIMEOUT",
867
+ 20115: "DRV_ERROR_MAP",
868
+ 20116: "DRV_ERROR_UNMAP",
869
+ 20117: "DRV_ERROR_MDL",
870
+ 20118: "DRV_ERROR_UNMDL",
871
+ 20119: "DRV_ERROR_BUFFSIZE",
872
+ 20121: "DRV_ERROR_NOHANDLE",
873
+ 20130: "DRV_GATING_NOT_AVAILABLE",
874
+ 20131: "DRV_FPGA_VOLTAGE_ERROR",
875
+ 20150: "DRV_OW_CMD_FAIL",
876
+ 20151: "DRV_OWMEMORY_BAD_ADDR",
877
+ 20152: "DRV_OWCMD_NOT_AVAILABLE",
878
+ 20153: "DRV_OW_NO_SLAVES",
879
+ 20154: "DRV_OW_NOT_INITIALIZED",
880
+ 20155: "DRV_OW_ERROR_SLAVE_NUM",
881
+ 20156: "DRV_MSTIMINGS_ERROR",
882
+ 20173: "DRV_OA_NULL_ERROR",
883
+ 20174: "DRV_OA_PARSE_DTD_ERROR",
884
+ 20175: "DRV_OA_DTD_VALIDATE_ERROR",
885
+ 20176: "DRV_OA_FILE_ACCESS_ERROR",
886
+ 20177: "DRV_OA_FILE_DOES_NOT_EXIST",
887
+ 20178: "DRV_OA_XML_INVALID_OR_NOT_FOUND_ERROR",
888
+ 20179: "DRV_OA_PRESET_FILE_NOT_LOADED",
889
+ 20180: "DRV_OA_USER_FILE_NOT_LOADED",
890
+ 20181: "DRV_OA_PRESET_AND_USER_FILE_NOT_LOADED",
891
+ 20182: "DRV_OA_INVALID_FILE",
892
+ 20183: "DRV_OA_FILE_HAS_BEEN_MODIFIED",
893
+ 20184: "DRV_OA_BUFFER_FULL",
894
+ 20185: "DRV_OA_INVALID_STRING_LENGTH",
895
+ 20186: "DRV_OA_INVALID_CHARS_IN_NAME",
896
+ 20187: "DRV_OA_INVALID_NAMING",
897
+ 20188: "DRV_OA_GET_CAMERA_ERROR",
898
+ 20189: "DRV_OA_MODE_ALREADY_EXISTS",
899
+ 20190: "DRV_OA_STRINGS_NOT_EQUAL",
900
+ 20191: "DRV_OA_NO_USER_DATA",
901
+ 20192: "DRV_OA_VALUE_NOT_SUPPORTED",
902
+ 20193: "DRV_OA_MODE_DOES_NOT_EXIST",
903
+ 20194: "DRV_OA_CAMERA_NOT_SUPPORTED",
904
+ 20195: "DRV_OA_FAILED_TO_GET_MODE",
905
+ 20196: "DRV_OA_CAMERA_NOT_AVAILABLE",
906
+ 20211: "DRV_PROCESSING_FAILED",
907
+ 20990: "DRV_ERROR_NOCAMERA",
908
+ 20991: "DRV_NOT_SUPPORTED",
909
+ 20992: "DRV_NOT_AVAILABLE"
910
+ }