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.
- wasatch/AbstractUSBDevice.py +29 -0
- wasatch/AndorDevice.py +910 -0
- wasatch/AreaScanImage.py +19 -0
- wasatch/AutoRaman.py +457 -0
- wasatch/AutoRamanRequest.py +96 -0
- wasatch/BLEDevice.py +1032 -0
- wasatch/BalanceAcquisition.py +136 -0
- wasatch/CSVLoader.py +188 -0
- wasatch/CommandSettings.py +95 -0
- wasatch/ControlObject.py +17 -0
- wasatch/DetectorROI.py +36 -0
- wasatch/DetectorRegions.py +158 -0
- wasatch/DeviceFinderBLE.py +107 -0
- wasatch/DeviceFinderTCP.py +20 -0
- wasatch/DeviceFinderUSB.py +236 -0
- wasatch/DeviceID.py +344 -0
- wasatch/EEPROM.py +1450 -0
- wasatch/EEPROM_multical.sav +48 -0
- wasatch/FPGAOptions.py +125 -0
- wasatch/FeatureIdentificationDevice.py +3961 -0
- wasatch/FirmwareRequirements.py +76 -0
- wasatch/HardwareInfo.py +36 -0
- wasatch/IDSCamera.py +916 -0
- wasatch/IDSDevice.py +329 -0
- wasatch/IMX385.py +179 -0
- wasatch/InterfaceDevice.py +40 -0
- wasatch/MockUSBDevice.py +353 -0
- wasatch/OceanDevice.py +215 -0
- wasatch/PollStatus.py +15 -0
- wasatch/ProcessedReading.py +352 -0
- wasatch/ROI.py +33 -0
- wasatch/Reading.py +80 -0
- wasatch/RealUSBDevice.py +64 -0
- wasatch/SPIDevice.py +628 -0
- wasatch/SpectrometerRequest.py +15 -0
- wasatch/SpectrometerResponse.py +38 -0
- wasatch/SpectrometerSettings.py +446 -0
- wasatch/SpectrometerState.py +255 -0
- wasatch/StatusMessage.py +14 -0
- wasatch/TCPDevice.py +409 -0
- wasatch/TakeOneRequest.py +43 -0
- wasatch/WasatchBus.py +87 -0
- wasatch/WasatchDevice.py +1060 -0
- wasatch/WasatchDeviceWrapper.py +452 -0
- wasatch/WrapperWorker.py +304 -0
- wasatch/__init__.py +4 -0
- wasatch/applog.py +177 -0
- wasatch/simulation_protocol.py +227 -0
- wasatch/utils.py +731 -0
- wasatch-2.3.2.dist-info/LICENSE +21 -0
- wasatch-2.3.2.dist-info/METADATA +347 -0
- wasatch-2.3.2.dist-info/RECORD +53 -0
- 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
|
+
}
|