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/WasatchDevice.py
ADDED
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import numpy as np
|
|
5
|
+
import psutil
|
|
6
|
+
import logging
|
|
7
|
+
import datetime
|
|
8
|
+
import threading
|
|
9
|
+
from queue import Queue
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .FeatureIdentificationDevice import FeatureIdentificationDevice
|
|
13
|
+
from .SpectrometerSettings import SpectrometerSettings
|
|
14
|
+
from .SpectrometerResponse import SpectrometerResponse
|
|
15
|
+
from .SpectrometerRequest import SpectrometerRequest
|
|
16
|
+
from .SpectrometerResponse import ErrorLevel
|
|
17
|
+
from .InterfaceDevice import InterfaceDevice
|
|
18
|
+
from .BalanceAcquisition import BalanceAcquisition
|
|
19
|
+
from .SpectrometerState import SpectrometerState
|
|
20
|
+
from .ControlObject import ControlObject
|
|
21
|
+
from .AutoRaman import AutoRaman
|
|
22
|
+
from .DeviceID import DeviceID
|
|
23
|
+
from .Reading import Reading
|
|
24
|
+
|
|
25
|
+
log = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
class WasatchDevice(InterfaceDevice):
|
|
28
|
+
"""
|
|
29
|
+
This is the top-level interface for controlling and communicating with
|
|
30
|
+
Wasatch Photonics USB 2.0 spectrometers using the FeatureInterfaceDevice
|
|
31
|
+
(FID) protocol as defined in ENG-0001.
|
|
32
|
+
|
|
33
|
+
This class is essentially a clunky wrapper over FeatureInterfaceDevice,
|
|
34
|
+
providing the higher-level InterfaceDevice interface making WasatchDevice a
|
|
35
|
+
peer to AndorDevice, OceanDevice, TCPDevice, IDSDevice etc.
|
|
36
|
+
|
|
37
|
+
Several points will stand out:
|
|
38
|
+
|
|
39
|
+
1. WasatchDevice should probably be renamed to FIDDevice, because Wasatch
|
|
40
|
+
Photonics is also the manufacturer for spectrometers using AndorDevice,
|
|
41
|
+
TCPDevice, IDSDevice etc.
|
|
42
|
+
|
|
43
|
+
2. This class could be simplified considerably, and possibly merged with
|
|
44
|
+
FeatureIdentificationDevice.
|
|
45
|
+
|
|
46
|
+
ENLIGHTEN does not instantiate WasatchDevices directly, but instead uses
|
|
47
|
+
a WasatchDeviceWrapper to access a single WasatchDevice in a dedicated child
|
|
48
|
+
thread. Other users of Wasatch.PY may of course instantiate a WasatchDevice
|
|
49
|
+
directly, or go straight to a FeatureInterfaceDevice.
|
|
50
|
+
|
|
51
|
+
Main things this "wrapper" provides:
|
|
52
|
+
|
|
53
|
+
- Implements software-based, library-based scan averaging (badly). Although
|
|
54
|
+
ARM-based spectrometers can do this in firmware, FX2-based models have to
|
|
55
|
+
do it in software. Between Wasatch.PY and ENLIGHTEN, it seems preferable
|
|
56
|
+
to have this in the Wasatch.PY library, where it is easier for customers
|
|
57
|
+
to use.
|
|
58
|
+
|
|
59
|
+
- Distinguishes between normal, averaged, AutoRaman and AreaScan
|
|
60
|
+
acquisitions.
|
|
61
|
+
|
|
62
|
+
@par History
|
|
63
|
+
|
|
64
|
+
This class exists because it used to wrap TWO types of spectrometers:
|
|
65
|
+
|
|
66
|
+
- FeatureIdentificationDevices, which follow the ENG-0001 API and
|
|
67
|
+
have EEPROMs
|
|
68
|
+
- StrokerDevices, which didn't have EEPROMs and didn't obey ENG-0001.
|
|
69
|
+
|
|
70
|
+
I don't know if we have an ENG document specifying the protocol that the old
|
|
71
|
+
Stroker electronics used. It was already deprecated when I started, and was
|
|
72
|
+
rapidly removed from Wasatch.PY. Which kind of made this class superfluous.
|
|
73
|
+
|
|
74
|
+
This class (and all who inherit InterfaceDevice) use a somewhat clunky
|
|
75
|
+
data-passing and error-handling mechanism via SpectrometerRequest and
|
|
76
|
+
SpectrometerResponse objects. That could probably be streamlined.
|
|
77
|
+
|
|
78
|
+
Part of the legacy "ugly-isms" in this class date back to when ENLIGHTEN
|
|
79
|
+
was multi-process rather than multi-threaded, and all data flows between
|
|
80
|
+
Wasatch.PY and ENLIGHTEN were via pickled queues :-(
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, device_id, message_queue=None, alert_queue=None):
|
|
84
|
+
"""
|
|
85
|
+
@param device_id a DeviceID instance OR string label thereof
|
|
86
|
+
@param message_queue if provided, used to send status back to caller
|
|
87
|
+
@param alert_queue if provided, used to receive hints and realtime interrupts from caller
|
|
88
|
+
"""
|
|
89
|
+
# if passed a string representation of a DeviceID, deserialize it
|
|
90
|
+
if type(device_id) is str:
|
|
91
|
+
device_id = DeviceID(label=device_id)
|
|
92
|
+
|
|
93
|
+
self.device_id = device_id
|
|
94
|
+
self.message_queue = message_queue # outgoing notifications to ENLIGHTEN
|
|
95
|
+
self.alert_queue = alert_queue # incoming alerts from ENLIGHTEN
|
|
96
|
+
|
|
97
|
+
self.lock = threading.Lock()
|
|
98
|
+
|
|
99
|
+
self.connected = False
|
|
100
|
+
self.hardware = None # FeatureIdentificationDevice
|
|
101
|
+
|
|
102
|
+
# Receives ENLIGHTEN's 'change settings' commands in the spectrometer
|
|
103
|
+
# process. Although a logical queue, has nothing to do with multiprocessing.
|
|
104
|
+
self.command_queue = []
|
|
105
|
+
|
|
106
|
+
# Enable for "immediate mode" by clients like WasatchShell (by default,
|
|
107
|
+
# inbound commands are queued and executed at beginning of next acquire_data;
|
|
108
|
+
# this runs them as they arrive).
|
|
109
|
+
self.immediate_mode = False
|
|
110
|
+
|
|
111
|
+
self.settings = SpectrometerSettings()
|
|
112
|
+
|
|
113
|
+
# Any particular reason these aren't in FeatureIdentificationDevice?
|
|
114
|
+
self.summed_spectra = None
|
|
115
|
+
self.sum_count = 0
|
|
116
|
+
self.session_reading_count = 0
|
|
117
|
+
self.take_one_request = None
|
|
118
|
+
self.last_complete_acquisition = None
|
|
119
|
+
|
|
120
|
+
self.process_id = os.getpid()
|
|
121
|
+
self.last_memory_check = datetime.datetime.now()
|
|
122
|
+
self.last_battery_percentage = 0
|
|
123
|
+
|
|
124
|
+
self.process_f = self._init_process_funcs()
|
|
125
|
+
|
|
126
|
+
self.auto_raman = AutoRaman(self)
|
|
127
|
+
|
|
128
|
+
# ######################################################################## #
|
|
129
|
+
# #
|
|
130
|
+
# Connection #
|
|
131
|
+
# #
|
|
132
|
+
# ######################################################################## #
|
|
133
|
+
|
|
134
|
+
def connect(self):
|
|
135
|
+
""" Attempt low level connection to the specified DeviceID """
|
|
136
|
+
if self.device_id.is_usb() or self.device_id.is_mock():
|
|
137
|
+
log.debug(f"trying to connect to {'USB' if self.device_id.is_usb() else 'Mock'}")
|
|
138
|
+
result = self.connect_feature_identification()
|
|
139
|
+
if result.data:
|
|
140
|
+
log.debug("Connected to FeatureIdentificationDevice")
|
|
141
|
+
self.connected = True
|
|
142
|
+
self.initialize_settings()
|
|
143
|
+
return SpectrometerResponse(True)
|
|
144
|
+
else:
|
|
145
|
+
log.debug("Failed to connect to FeatureIdentificationDevice")
|
|
146
|
+
return result
|
|
147
|
+
else:
|
|
148
|
+
log.critical("unsupported DeviceID protocol: %s", self.device_id)
|
|
149
|
+
log.debug("Can't connect to %s", self.device_id)
|
|
150
|
+
return SpectrometerResponse(False)
|
|
151
|
+
|
|
152
|
+
def disconnect(self):
|
|
153
|
+
log.debug("WasatchDevice.disconnect: calling hardware disconnect")
|
|
154
|
+
try:
|
|
155
|
+
req = SpectrometerRequest("disconnect")
|
|
156
|
+
self.hardware.handle_requests([req])
|
|
157
|
+
except:
|
|
158
|
+
log.critical("Issue disconnecting hardware", exc_info=1)
|
|
159
|
+
|
|
160
|
+
time.sleep(0.1)
|
|
161
|
+
|
|
162
|
+
self.connected = False
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
## Given a specified universal identifier, attempt to connect to the device using FID protocol.
|
|
166
|
+
# @todo merge with the hardcoded list in DeviceFinderUSB
|
|
167
|
+
def connect_feature_identification(self):
|
|
168
|
+
# check to see if valid FID PID
|
|
169
|
+
pid_hex = self.device_id.get_pid_hex()
|
|
170
|
+
if pid_hex not in ["1000", "2000", "4000"]:
|
|
171
|
+
log.debug(f"connect_feature_identification: device_id {self.device_id} has invalid PID {pid_hex}")
|
|
172
|
+
return SpectrometerResponse(False)
|
|
173
|
+
|
|
174
|
+
dev = None
|
|
175
|
+
try:
|
|
176
|
+
log.debug(f"connect_fid: instantiating FID with device_id {self.device_id}, pid {pid_hex}")
|
|
177
|
+
dev = FeatureIdentificationDevice(device_id=self.device_id, message_queue=self.message_queue, alert_queue=self.alert_queue)
|
|
178
|
+
log.debug("connect_fid: instantiated")
|
|
179
|
+
|
|
180
|
+
log.debug("connect_fid: calling dev.connect")
|
|
181
|
+
response = dev.connect()
|
|
182
|
+
log.debug("connect_fid: back from dev.connect")
|
|
183
|
+
|
|
184
|
+
if not response.data:
|
|
185
|
+
log.critical("Low level failure in device connect")
|
|
186
|
+
return response
|
|
187
|
+
|
|
188
|
+
self.hardware = dev
|
|
189
|
+
except:
|
|
190
|
+
log.critical(f"Problem connecting to {self.device_id}", exc_info=1)
|
|
191
|
+
return SpectrometerResponse(False)
|
|
192
|
+
|
|
193
|
+
log.debug(f"Connected to FeatureIdentificationDevice {self.device_id}")
|
|
194
|
+
return SpectrometerResponse(True)
|
|
195
|
+
|
|
196
|
+
def initialize_settings(self):
|
|
197
|
+
if not self.connected:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# WasatchDevice and FID share same SpectrometerSettings
|
|
201
|
+
self.settings = self.hardware.settings
|
|
202
|
+
|
|
203
|
+
req_int = SpectrometerRequest('get_integration_time_ms')
|
|
204
|
+
req_gain = SpectrometerRequest('get_detector_gain')
|
|
205
|
+
reqs = [req_int, req_gain]
|
|
206
|
+
self.hardware.handle_requests(reqs)
|
|
207
|
+
|
|
208
|
+
# could read the defaults for these ss.state volatiles from FID too:
|
|
209
|
+
#
|
|
210
|
+
# self.tec_setpoint_degC
|
|
211
|
+
# self.high_gain_mode_enabled
|
|
212
|
+
# self.triggering_enabled
|
|
213
|
+
# self.laser_enabled
|
|
214
|
+
# self.laser_power
|
|
215
|
+
# self.ccd_offset
|
|
216
|
+
|
|
217
|
+
self.settings.update_wavecal()
|
|
218
|
+
self.settings.update_raman_intensity_factors()
|
|
219
|
+
self.settings.dump()
|
|
220
|
+
|
|
221
|
+
# ######################################################################## #
|
|
222
|
+
# #
|
|
223
|
+
# Acquisition #
|
|
224
|
+
# #
|
|
225
|
+
# ######################################################################## #
|
|
226
|
+
|
|
227
|
+
def acquire_data(self):
|
|
228
|
+
"""
|
|
229
|
+
Process all enqueued settings, then read actual data (spectrum and
|
|
230
|
+
temperatures) from the device.
|
|
231
|
+
|
|
232
|
+
ENLIGHTEN calls this function via WrapperWorker.run().
|
|
233
|
+
|
|
234
|
+
@see Controller.acquire_reading
|
|
235
|
+
"""
|
|
236
|
+
log.debug("acquire_data: start")
|
|
237
|
+
|
|
238
|
+
if self.hardware.shutdown_requested:
|
|
239
|
+
log.critical("acquire_data: hardware shutdown requested")
|
|
240
|
+
return SpectrometerResponse(False, poison_pill=True)
|
|
241
|
+
|
|
242
|
+
# process queued commands, and find out if we've been asked to read a
|
|
243
|
+
# spectrum
|
|
244
|
+
self.process_commands()
|
|
245
|
+
|
|
246
|
+
# if we don't yet have an integration time, nothing to do
|
|
247
|
+
if self.settings.state.integration_time_ms <= 0:
|
|
248
|
+
log.debug("skipping acquire_data because no integration_time_ms")
|
|
249
|
+
return SpectrometerResponse(None)
|
|
250
|
+
|
|
251
|
+
# if not self.hardware.is_sensor_stable():
|
|
252
|
+
# # technically, we could do all the other stuff in acquire_spectrum
|
|
253
|
+
# # (read battery, laser temperature, ambient temperature, detector
|
|
254
|
+
# # temperature etc), not bothering for now
|
|
255
|
+
# log.debug("declining to read spectra while stabilizing")
|
|
256
|
+
# return SpectrometerResponse(None)
|
|
257
|
+
|
|
258
|
+
# note that right now, all we return are Readings (encapsulating both
|
|
259
|
+
# spectra and temperatures). If we disable spectra, ENLIGHTEN stops
|
|
260
|
+
# receiving temperatures as well. In the future perhaps we should return
|
|
261
|
+
# multiple object types (Acquisitions, Temperatures, etc)
|
|
262
|
+
|
|
263
|
+
if self.settings.state.area_scan_enabled:
|
|
264
|
+
return self.acquire_area_scan()
|
|
265
|
+
else:
|
|
266
|
+
return self.acquire_spectrum()
|
|
267
|
+
|
|
268
|
+
##
|
|
269
|
+
# Generate one Reading from the spectrometer, including one
|
|
270
|
+
# optionally-averaged spectrum, device temperatures and other hardware
|
|
271
|
+
# status.
|
|
272
|
+
#
|
|
273
|
+
# This is normally called by self.acquire_data when that function decides it
|
|
274
|
+
# is time to perform an acquisition.
|
|
275
|
+
#
|
|
276
|
+
# @par Scan Averaging
|
|
277
|
+
#
|
|
278
|
+
# IF the driver is in free-running mode, AND performing scan averaging,
|
|
279
|
+
# THEN scan averaging is NOT encapsulated within a single call to this
|
|
280
|
+
# function. Instead, we let ths spectrometer run in free-running mode,
|
|
281
|
+
# collecting individual spectra as per normal, and returning each "partial"
|
|
282
|
+
# readings while "building up" to the final averaged measurement.
|
|
283
|
+
#
|
|
284
|
+
# That is to say, if scan averaging is set to 10, then this function will
|
|
285
|
+
# get called 10 times, as ticked by the regular free-running timers, before
|
|
286
|
+
# the fully averaged spectrum is returned. A total of 10 (not 11) spectra
|
|
287
|
+
# will be generated and sent upstream: the first 9 "partial" (unaveraged)
|
|
288
|
+
# reads, and the final 10th spectrum which will contain the average of all
|
|
289
|
+
# 10 measurements.
|
|
290
|
+
#
|
|
291
|
+
# This gives the user-facing GUI an opportunity to update the "collected
|
|
292
|
+
# X-of-Y" readout on-screen, and potentially even graph the traces of
|
|
293
|
+
# in-process partial readings.
|
|
294
|
+
#
|
|
295
|
+
# HOWEVER, this doesn't make as much sense if we're not in free-running mode,
|
|
296
|
+
# i.e. the subprocess has been slaved to explicit control by the Controller
|
|
297
|
+
# (likely a feature object like BatchCollection), and is collecting exactly
|
|
298
|
+
# those measurements we're being commanded, as they're commanded.
|
|
299
|
+
#
|
|
300
|
+
# THEREFORE, if the driver IS NOT in free-running mode, then we ONLY return
|
|
301
|
+
# the final averaged spectrum as one atomic operation.
|
|
302
|
+
#
|
|
303
|
+
# In this case, if scan averaging is set to 10, then A SINGLE CALL to this
|
|
304
|
+
# function will "block" while the full 10 measurements are made, and then
|
|
305
|
+
# a single, fully-averaged spectrum will be returned upstream.
|
|
306
|
+
#
|
|
307
|
+
# @return a Reading wrapped in a SpectrometerResponse
|
|
308
|
+
#
|
|
309
|
+
def acquire_spectrum(self):
|
|
310
|
+
"""
|
|
311
|
+
@returns a SpectrometerResponse(data=Reading)
|
|
312
|
+
"""
|
|
313
|
+
if self.take_one_request and self.take_one_request.auto_raman_request:
|
|
314
|
+
return self.acquire_spectrum_auto_raman()
|
|
315
|
+
else:
|
|
316
|
+
return self.acquire_spectrum_standard()
|
|
317
|
+
|
|
318
|
+
def acquire_spectrum_auto_raman(self):
|
|
319
|
+
"""
|
|
320
|
+
@returns a SpectrometerResponse(data=Reading)
|
|
321
|
+
@todo fold-in a lot of the post-reading sensor measurements
|
|
322
|
+
(temperature, interlock etc) provided by acquire_spectrum_standard
|
|
323
|
+
"""
|
|
324
|
+
log.debug("WasatchDevice.acquire_spectrum_auto_raman: calling AutoRaman.measure")
|
|
325
|
+
spectrometer_response = self.auto_raman.measure(self.take_one_request.auto_raman_request)
|
|
326
|
+
reading = spectrometer_response.data
|
|
327
|
+
log.debug(f"WasatchDevice.acquire_spectrum_auto_raman: received {reading}")
|
|
328
|
+
|
|
329
|
+
# return the completed TakeOneRequest and clear our internal handle
|
|
330
|
+
#
|
|
331
|
+
# Note that Auto-Raman doesn't currently support "fast BatchCollection"
|
|
332
|
+
# with TakeOneRequest.readings_target. I think that's okay, because that's
|
|
333
|
+
# not really what Auto-Raman is for.
|
|
334
|
+
reading.take_one_request = self.take_one_request
|
|
335
|
+
self.take_one_request = None
|
|
336
|
+
|
|
337
|
+
return spectrometer_response
|
|
338
|
+
|
|
339
|
+
def acquire_spectrum_standard(self):
|
|
340
|
+
"""
|
|
341
|
+
@returns a SpectrometerResponse(data=Reading)
|
|
342
|
+
"""
|
|
343
|
+
acquire_response = SpectrometerResponse()
|
|
344
|
+
|
|
345
|
+
tor = self.take_one_request
|
|
346
|
+
|
|
347
|
+
self.perform_optional_throwaways()
|
|
348
|
+
|
|
349
|
+
########################################################################
|
|
350
|
+
# Batch Collection silliness
|
|
351
|
+
########################################################################
|
|
352
|
+
|
|
353
|
+
# Note that BatchCollection's "auto-enable laser" is not nearly as
|
|
354
|
+
# involved as the Auto-Raman performed by acquire_spectrum_auto_raman.
|
|
355
|
+
# BatchCollection uses TakeOneRequest.enable_laser_before,
|
|
356
|
+
# .disable_laser_after, .take_dark, .laser_warmup_ms and .scans_to_average
|
|
357
|
+
# to take an averaged, dark-corrected Raman measurement WITH FIXED
|
|
358
|
+
# ACQUISITION PARAMETERS (integration time, gain). It does not perform
|
|
359
|
+
# any optimization of integration time or gain.
|
|
360
|
+
#
|
|
361
|
+
# By implication then, we should simplify this by changing
|
|
362
|
+
# BatchCollection to use AutoRamanRequest, adding a checkbox to
|
|
363
|
+
# BatchCollection to allow # optimization; if unchecked, min/max integ
|
|
364
|
+
# and gain will equal start integ/gain, and no optimization will occur.
|
|
365
|
+
# Then we can remove all the auto laser stuff from the following code.
|
|
366
|
+
#
|
|
367
|
+
# This has been captured in https://github.com/WasatchPhotonics/ENLIGHTEN/issues/474
|
|
368
|
+
|
|
369
|
+
auto_enable_laser = tor is not None and tor.enable_laser_before
|
|
370
|
+
# log.debug("acquire_spectrum: auto_enable_laser = %s", auto_enable_laser)
|
|
371
|
+
|
|
372
|
+
dark_reading = SpectrometerResponse()
|
|
373
|
+
if auto_enable_laser and tor.take_dark:
|
|
374
|
+
log.debug(f"AUTO-RAMAN ==> taking internal dark")
|
|
375
|
+
|
|
376
|
+
# disable laser if it was on
|
|
377
|
+
if self.settings.state.laser_enabled:
|
|
378
|
+
log.debug("AUTO-RAMAN ==> disabling laser for internal dark")
|
|
379
|
+
self.hardware.handle_requests([SpectrometerRequest('set_laser_enable', args=[False])])
|
|
380
|
+
time.sleep(1)
|
|
381
|
+
|
|
382
|
+
dark_reading = self.take_one_averaged_reading(label="internal dark")
|
|
383
|
+
if dark_reading.poison_pill or dark_reading.error_msg:
|
|
384
|
+
log.debug(f"internal dark error {dark_reading}")
|
|
385
|
+
acquire_response.poison_pill = True
|
|
386
|
+
return acquire_response
|
|
387
|
+
|
|
388
|
+
if auto_enable_laser:
|
|
389
|
+
log.debug(f"AUTO-RAMAN ==> acquire_spectum: enabling laser")
|
|
390
|
+
req = SpectrometerRequest('set_laser_enable', args=[True])
|
|
391
|
+
self.hardware.handle_requests([req])
|
|
392
|
+
if self.hardware.shutdown_requested:
|
|
393
|
+
log.debug(f"auto_enable_laser shutdown requested")
|
|
394
|
+
acquire_response.poison_pill = True
|
|
395
|
+
return acquire_response
|
|
396
|
+
|
|
397
|
+
if tor:
|
|
398
|
+
log.debug(f"AUTO-RAMAN ==> acquire_spectum: sleeping {tor.laser_warmup_ms}ms for laser to warmup")
|
|
399
|
+
time.sleep(tor.laser_warmup_ms / 1000.0)
|
|
400
|
+
|
|
401
|
+
self.perform_optional_throwaways()
|
|
402
|
+
|
|
403
|
+
########################################################################
|
|
404
|
+
# Take a Reading (possibly averaged)
|
|
405
|
+
########################################################################
|
|
406
|
+
|
|
407
|
+
log.debug("taking averaged reading")
|
|
408
|
+
take_one_response = self.take_one_averaged_reading(label="sample (possibly Raman)")
|
|
409
|
+
reading = take_one_response.data
|
|
410
|
+
if take_one_response.poison_pill:
|
|
411
|
+
log.debug(f"floating up take_one_averaged_reading poison pill {take_one_response}")
|
|
412
|
+
return take_one_response
|
|
413
|
+
if take_one_response.keep_alive:
|
|
414
|
+
log.debug(f"floating up keep alive")
|
|
415
|
+
return take_one_response
|
|
416
|
+
if take_one_response.data is None:
|
|
417
|
+
log.debug(f"Received a none reading, floating it up {take_one_response}")
|
|
418
|
+
return take_one_response
|
|
419
|
+
|
|
420
|
+
# don't perform dark subtraction, but pass the dark measurement along
|
|
421
|
+
# with the averaged reading
|
|
422
|
+
if dark_reading.data is not None:
|
|
423
|
+
reading.dark = dark_reading.data.spectrum
|
|
424
|
+
log.debug(f"attaching dark to reading (mean {np.mean(reading.dark)}, min {min(reading.dark)}, max {max(reading.dark)})")
|
|
425
|
+
log.debug(f"reading spectrum (mean {np.mean(reading.spectrum)}, min {min(reading.spectrum)}, max {max(reading.spectrum)})")
|
|
426
|
+
|
|
427
|
+
########################################################################
|
|
428
|
+
# provide early exit-ramp if we've been asked to return bare Readings
|
|
429
|
+
# (just averaged spectra with corrected bad pixels, no metadata)
|
|
430
|
+
########################################################################
|
|
431
|
+
|
|
432
|
+
def disable_laser(shutdown=False, label=None):
|
|
433
|
+
if shutdown or auto_enable_laser:
|
|
434
|
+
log.debug(f"acquire_spectrum.disable_laser: shutdown {shutdown}, auto_enable_laser {auto_enable_laser}, label {label}")
|
|
435
|
+
req = SpectrometerRequest('set_laser_enable', args=[False])
|
|
436
|
+
self.hardware.handle_requests([req])
|
|
437
|
+
acquire_response.poison_pill = shutdown
|
|
438
|
+
return acquire_response # for convenience
|
|
439
|
+
|
|
440
|
+
########################################################################
|
|
441
|
+
# We're done with the (possibly-averaged) spectrum, so we'd like to now
|
|
442
|
+
# disable the automatically-enabled laser, if it was engaged; but before
|
|
443
|
+
# we can do that that, we should take any requested measurements of the
|
|
444
|
+
# laser temperature and photodiode, as those would obviously be invalid-
|
|
445
|
+
# ated if we took them AFTER the laser was off.
|
|
446
|
+
########################################################################
|
|
447
|
+
|
|
448
|
+
# only read laser temperature if we have a laser
|
|
449
|
+
if self.settings.eeprom.has_laser:
|
|
450
|
+
try:
|
|
451
|
+
# MZ: we might want to do these for auto_enable_laser as well...
|
|
452
|
+
func_attr = [ ('get_laser_enabled', 'laser_enabled'),
|
|
453
|
+
('can_laser_fire', 'laser_can_fire'),
|
|
454
|
+
('is_laser_firing', 'laser_is_firing') ]
|
|
455
|
+
if self.settings.is_xs() and self.settings.eeprom.sig_laser_tec:
|
|
456
|
+
func_attr.append( ('get_laser_tec_mode', 'laser_tec_enabled') )
|
|
457
|
+
func_attr.append( ('get_ambient_temperature_degC', 'ambient_temperature_degC') )
|
|
458
|
+
|
|
459
|
+
for (func, attr) in func_attr:
|
|
460
|
+
req = SpectrometerRequest(func)
|
|
461
|
+
res = self.hardware.handle_requests([req])[0]
|
|
462
|
+
if res is None:
|
|
463
|
+
log.debug(f"WasatchDevice.acquire_spectrum: ignoring None {func} response")
|
|
464
|
+
else:
|
|
465
|
+
if res.error_msg != '':
|
|
466
|
+
return res
|
|
467
|
+
value = res.data
|
|
468
|
+
# log.debug(f"WasatchDevice.acquire_spectrum: storing {attr} = {value}")
|
|
469
|
+
setattr(reading, attr, value)
|
|
470
|
+
|
|
471
|
+
if self.hardware.shutdown_requested:
|
|
472
|
+
return disable_laser(shutdown=True, label=f"loading laser attributes")
|
|
473
|
+
|
|
474
|
+
# laser temperature
|
|
475
|
+
count = 2 if self.settings.state.secondary_adc_enabled else 1
|
|
476
|
+
for throwaway in range(count):
|
|
477
|
+
req = SpectrometerRequest('get_laser_temperature_raw')
|
|
478
|
+
res = self.hardware.handle_requests([req])[0]
|
|
479
|
+
if res.error_msg != '':
|
|
480
|
+
return res
|
|
481
|
+
reading.laser_temperature_raw = res.data
|
|
482
|
+
if self.hardware.shutdown_requested:
|
|
483
|
+
return disable_laser(shuttdown=True, label=f"reading laser temperature (throwaway {throwaway} of {count})")
|
|
484
|
+
|
|
485
|
+
req = SpectrometerRequest('get_laser_temperature_degC', args=[reading.laser_temperature_raw])
|
|
486
|
+
res = self.hardware.handle_requests([req])[0]
|
|
487
|
+
if res.error_msg != '':
|
|
488
|
+
return res
|
|
489
|
+
reading.laser_temperature_degC = res.data
|
|
490
|
+
if self.hardware.shutdown_requested:
|
|
491
|
+
return disable_laser(shutdown=True, label=f"reading laser temperature")
|
|
492
|
+
|
|
493
|
+
except:
|
|
494
|
+
log.debug("Error reading laser temperature", exc_info=1)
|
|
495
|
+
|
|
496
|
+
# read secondary ADC if requested
|
|
497
|
+
if self.settings.state.secondary_adc_enabled:
|
|
498
|
+
try:
|
|
499
|
+
req = SpectrometerRequest("select_adc", args=[1])
|
|
500
|
+
self.hardware.handle_requests([req])
|
|
501
|
+
if self.hardware.shutdown_requested:
|
|
502
|
+
return disable_laser(shutdown=True, label="select_adc[1]")
|
|
503
|
+
|
|
504
|
+
for throwaway in range(2):
|
|
505
|
+
req = SpectrometerRequest("get_secondary_adc_raw")
|
|
506
|
+
res = self.hardware.handle_requests([req])[0]
|
|
507
|
+
if res.error_msg != '':
|
|
508
|
+
return res
|
|
509
|
+
reading.secondary_adc_raw = res.data
|
|
510
|
+
if self.hardware.shutdown_requested:
|
|
511
|
+
return disable_laser(shutdown=True, label="get_secondary_adc_raw")
|
|
512
|
+
|
|
513
|
+
req = SpectrometerRequest("get_secondary_adc_calibrated", args =[reading.secondary_adc_raw])
|
|
514
|
+
res = self.hardware.handle_requests([req])[0]
|
|
515
|
+
if res.error_msg != '':
|
|
516
|
+
return res
|
|
517
|
+
reading.secondary_adc_calibrated = res.data
|
|
518
|
+
req = SpectrometerRequest("select_adc", args=[0])
|
|
519
|
+
res = self.hardware.handle_requests([req])[0]
|
|
520
|
+
if res.error_msg != '':
|
|
521
|
+
return res
|
|
522
|
+
if self.hardware.shutdown_requested:
|
|
523
|
+
return disable_laser(shutdown=True, label="select_adc[0]")
|
|
524
|
+
|
|
525
|
+
except:
|
|
526
|
+
log.debug("Error reading secondary ADC", exc_info=1)
|
|
527
|
+
|
|
528
|
+
########################################################################
|
|
529
|
+
# we've read the laser temperature and photodiode, so we can now safely
|
|
530
|
+
# disable the laser (if we're the one who enabled it)
|
|
531
|
+
########################################################################
|
|
532
|
+
|
|
533
|
+
disable_laser(label="clean exit")
|
|
534
|
+
|
|
535
|
+
########################################################################
|
|
536
|
+
# finish collecting any metadata that doesn't require the laser
|
|
537
|
+
########################################################################
|
|
538
|
+
|
|
539
|
+
# read detector temperature if applicable
|
|
540
|
+
if self.settings.eeprom.has_cooling:
|
|
541
|
+
try:
|
|
542
|
+
req = SpectrometerRequest("get_detector_temperature_raw")
|
|
543
|
+
res = self.hardware.handle_requests([req])[0]
|
|
544
|
+
if res.error_msg != '':
|
|
545
|
+
return res
|
|
546
|
+
reading.detector_temperature_raw = res.data
|
|
547
|
+
if self.hardware.shutdown_requested:
|
|
548
|
+
log.debug("detector_temperature_raw shutdown")
|
|
549
|
+
acquire_response.poison_pill = True
|
|
550
|
+
return acquire_response
|
|
551
|
+
|
|
552
|
+
req = SpectrometerRequest("get_detector_temperature_degC", args=[reading.detector_temperature_raw])
|
|
553
|
+
res = self.hardware.handle_requests([req])[0]
|
|
554
|
+
if res.error_msg != '':
|
|
555
|
+
return res
|
|
556
|
+
reading.detector_temperature_degC = res.data
|
|
557
|
+
if self.hardware.shutdown_requested:
|
|
558
|
+
log.debug("detector_temperature_degC shutdown")
|
|
559
|
+
acquire_response.poison_pill = True
|
|
560
|
+
return acquire_response
|
|
561
|
+
|
|
562
|
+
except:
|
|
563
|
+
log.debug("Error reading detector temperature", exc_info=1)
|
|
564
|
+
|
|
565
|
+
# read ambient temperature if applicable
|
|
566
|
+
if self.settings.is_gen15():
|
|
567
|
+
try:
|
|
568
|
+
# reading.ambient_temperature_degC = self.hardware.get_ambient_temperature_degC()
|
|
569
|
+
pass
|
|
570
|
+
except:
|
|
571
|
+
log.debug("Error reading ambient temperature", exc_info=1)
|
|
572
|
+
|
|
573
|
+
# read battery every 5sec
|
|
574
|
+
if self.settings.eeprom.has_battery:
|
|
575
|
+
if self.settings.state.battery_timestamp is None or (datetime.datetime.now() - self.settings.state.battery_timestamp).total_seconds() > 5:
|
|
576
|
+
|
|
577
|
+
# note that the following 3 requests should actually only generate
|
|
578
|
+
# one USB transaction as raw is cached internally
|
|
579
|
+
req = SpectrometerRequest("get_battery_state_raw")
|
|
580
|
+
res = self.hardware.handle_requests([req])[0]
|
|
581
|
+
if res.error_msg != '':
|
|
582
|
+
return res
|
|
583
|
+
reading.battery_raw = res.data
|
|
584
|
+
if self.hardware.shutdown_requested:
|
|
585
|
+
log.debug("battery_raw shutdown")
|
|
586
|
+
acquire_response.poison_pill = True
|
|
587
|
+
return acquire_response
|
|
588
|
+
|
|
589
|
+
req = SpectrometerRequest("get_battery_percentage")
|
|
590
|
+
res = self.hardware.handle_requests([req])[0]
|
|
591
|
+
if res.error_msg != '':
|
|
592
|
+
return res
|
|
593
|
+
reading.battery_percentage = res.data
|
|
594
|
+
if self.hardware.shutdown_requested:
|
|
595
|
+
log.debug("battery_perc shutdown")
|
|
596
|
+
acquire_response.poison_pill = True
|
|
597
|
+
return acquire_response
|
|
598
|
+
self.last_battery_percentage = reading.battery_percentage
|
|
599
|
+
|
|
600
|
+
req = SpectrometerRequest("get_battery_percentage")
|
|
601
|
+
res = self.hardware.handle_requests([req])[0]
|
|
602
|
+
if res.error_msg != '':
|
|
603
|
+
return res
|
|
604
|
+
reading.battery_charging = res.data
|
|
605
|
+
if self.hardware.shutdown_requested:
|
|
606
|
+
log.debug("battery_charging shutdown")
|
|
607
|
+
acquire_response.poison_pill = True
|
|
608
|
+
return acquire_response
|
|
609
|
+
|
|
610
|
+
log.debug("battery: %.2f%% (%s)", reading.battery_percentage, "charging" if reading.battery_charging else "discharging")
|
|
611
|
+
else:
|
|
612
|
+
if reading is not None:
|
|
613
|
+
reading.battery_percentage = self.last_battery_percentage
|
|
614
|
+
|
|
615
|
+
self.hardware.update_firmware_log()
|
|
616
|
+
|
|
617
|
+
if auto_enable_laser:
|
|
618
|
+
log.debug(f"AUTO-RAMAN ==> done")
|
|
619
|
+
|
|
620
|
+
if tor:
|
|
621
|
+
reading.take_one_request = tor
|
|
622
|
+
|
|
623
|
+
# this was for a "fast BatchCollection" with a streaming multi-reading TakeOneRequest
|
|
624
|
+
if tor.readings_target:
|
|
625
|
+
tor.readings_current += 1
|
|
626
|
+
|
|
627
|
+
if not tor.readings_target or tor.readings_current >= tor.readings_target:
|
|
628
|
+
log.debug(f"completed {tor}")
|
|
629
|
+
self.take_one_request = None
|
|
630
|
+
|
|
631
|
+
log.debug("device.acquire_spectrum: returning %s", reading)
|
|
632
|
+
acquire_response.data = reading
|
|
633
|
+
self.last_complete_acquisition = datetime.datetime.now()
|
|
634
|
+
return acquire_response
|
|
635
|
+
|
|
636
|
+
##
|
|
637
|
+
# It's unclear how many throwaways are really needed for a stable Raman
|
|
638
|
+
# spectrum, and whether they're really based on number of integrations
|
|
639
|
+
# (sensor stabilization) or time (laser warmup); I suspect both. Also note
|
|
640
|
+
# the potential need for sensor warm-up, but I think that's handled inside FW.
|
|
641
|
+
#
|
|
642
|
+
# Optimal would probably be something like "As many integrations as it takes
|
|
643
|
+
# to span 2sec, but not fewer than two."
|
|
644
|
+
#
|
|
645
|
+
# These are NOT the same throwaways added to smooth spectra over changes to
|
|
646
|
+
# integration time and gain. These are separate throwaways potentially
|
|
647
|
+
# required when waking the sensor from sleep. However, I'm using the same
|
|
648
|
+
# mechanism for tracking (self.remaining_throwaways) for commonality.
|
|
649
|
+
#
|
|
650
|
+
# @todo This shouldn't be required at all if we're in free-running mode, or
|
|
651
|
+
# if it's been less than a second since the last acquisition.
|
|
652
|
+
# @returns SpectrometerResponse IFF error occurred (normally None)
|
|
653
|
+
def perform_optional_throwaways(self):
|
|
654
|
+
if self.settings.is_micro() and self.take_one_request:
|
|
655
|
+
count = 2
|
|
656
|
+
readout_ms = 5
|
|
657
|
+
|
|
658
|
+
# Assume that if we FINISHED the last measurement less than a second
|
|
659
|
+
# ago, the sensor probably has not gone to sleep and doesn't need SW-
|
|
660
|
+
# driven warmups.
|
|
661
|
+
elapsed_sec_since_last_acquisition = (datetime.datetime.now() - self.last_complete_acquisition).total_seconds()
|
|
662
|
+
if self.last_complete_acquisition is None or elapsed_sec_since_last_acquisition > 1:
|
|
663
|
+
# for now, default to 2sec worth of acquisitions
|
|
664
|
+
while count * (self.settings.state.integration_time_ms + readout_ms) < 2000:
|
|
665
|
+
count += 1
|
|
666
|
+
|
|
667
|
+
self.hardware.remaining_throwaways = count
|
|
668
|
+
while self.hardware.remaining_throwaways > 0:
|
|
669
|
+
log.debug(f"more than a second since last measurement, so performing wake-up throwaways ({self.hardware.remaining_throwaways - 1} remaining)")
|
|
670
|
+
req = SpectrometerRequest("get_spectrum")
|
|
671
|
+
res = self.hardware.handle_requests([req])[0]
|
|
672
|
+
if res.error_msg != '':
|
|
673
|
+
return res
|
|
674
|
+
|
|
675
|
+
def take_one_averaged_reading(self, label=None):
|
|
676
|
+
"""
|
|
677
|
+
Okay, let's talk about averaging. We only perform averaging as a
|
|
678
|
+
blocking call in Wasatch.PY when given a TakeOneRequest with non-zero
|
|
679
|
+
scans_to_average, typically from ENLIGHTEN's TakeOneFeature (perhaps via
|
|
680
|
+
VCRControls.step, possibly with AutoRaman, or BatchCollection).
|
|
681
|
+
|
|
682
|
+
Otherwise, normally this thread (the background thread owned by a
|
|
683
|
+
WasatchDeviceWrapper object and running WrapperWorker.run) is in "free-
|
|
684
|
+
running" mode, taking spectra just as fast as it can in an endless loop,
|
|
685
|
+
feeding them back to the consumer (ENLIGHTEN) over a queue. To keep that
|
|
686
|
+
pipeline "moving," generally we don't do heavy blocking operations down
|
|
687
|
+
here in the background thread.
|
|
688
|
+
|
|
689
|
+
The new AutoRaman feature makes increased using of scan averaging,
|
|
690
|
+
and again kind of wants to be encapsulated down here. And by implication,
|
|
691
|
+
all TakeOneRequests should really support encapsulated, atomic averaging.
|
|
692
|
+
So the current design is that ATOMIC scans_to_average comes from
|
|
693
|
+
TakeOneRequest, ENLIGHTEN-based averaging in SpectrometerState.
|
|
694
|
+
|
|
695
|
+
@returns Reading on success, true or false on "stop processing" conditions
|
|
696
|
+
"""
|
|
697
|
+
take_one_response = SpectrometerResponse()
|
|
698
|
+
|
|
699
|
+
if self.take_one_request:
|
|
700
|
+
scans_to_average = self.take_one_request.scans_to_average # how many (for completion test / division)
|
|
701
|
+
loop_count = scans_to_average # how many to take NOW
|
|
702
|
+
sum_locally = scans_to_average > 1 # whether we should be summing at all
|
|
703
|
+
self.sum_count = 0 # whether we should reset previous sums
|
|
704
|
+
else:
|
|
705
|
+
scans_to_average = self.settings.state.scans_to_average # how many (for completion test / division)
|
|
706
|
+
sum_locally = scans_to_average > 1 # whether we should be summing at all
|
|
707
|
+
loop_count = 1 # how many to take NOW
|
|
708
|
+
|
|
709
|
+
if False:
|
|
710
|
+
log.debug(f"take_one_averaged_reading[{label}]:")
|
|
711
|
+
log.debug(f" self.sum_count = {self.sum_count}")
|
|
712
|
+
log.debug(f" scans_to_average = {scans_to_average}")
|
|
713
|
+
log.debug(f" loop_count = {loop_count}")
|
|
714
|
+
log.debug(f" sum_locally = {sum_locally}")
|
|
715
|
+
log.debug(f" remaining_throwaways = {self.hardware.remaining_throwaways}")
|
|
716
|
+
|
|
717
|
+
# clear any pending throwaways
|
|
718
|
+
while self.hardware.remaining_throwaways > 0:
|
|
719
|
+
log.debug(f"clearing stabilization throwaway ({self.hardware.remaining_throwaways - 1} remaining)")
|
|
720
|
+
req = SpectrometerRequest("get_spectrum")
|
|
721
|
+
res = self.hardware.handle_requests([req])[0]
|
|
722
|
+
if res.error_msg != '':
|
|
723
|
+
return res
|
|
724
|
+
|
|
725
|
+
# either take one measurement (normal), or a bunch (sum_locally)
|
|
726
|
+
reading = None
|
|
727
|
+
for loop_index in range(loop_count):
|
|
728
|
+
|
|
729
|
+
# log.debug(f"take_one_averaged_reading: loop_index {loop_index+1} of {loop_count}")
|
|
730
|
+
|
|
731
|
+
# start a new reading
|
|
732
|
+
# NOTE: reading.timestamp is when reading STARTED, not FINISHED!
|
|
733
|
+
reading = Reading(self.device_id)
|
|
734
|
+
|
|
735
|
+
# TODO...just include a copy of SpectrometerState? something to think
|
|
736
|
+
# about. That would actually provide a reason to roll all the
|
|
737
|
+
# temperature etc readouts into the SpectrometerState class...
|
|
738
|
+
reading.integration_time_ms = self.settings.state.integration_time_ms
|
|
739
|
+
reading.laser_power_perc = self.settings.state.laser_power_perc
|
|
740
|
+
reading.laser_power_mW = self.settings.state.laser_power_mW
|
|
741
|
+
reading.laser_enabled = self.settings.state.laser_enabled
|
|
742
|
+
|
|
743
|
+
# collect ONE spectrum (it's in a while loop because we may have
|
|
744
|
+
# to wait for an external trigger, and must wait through a series
|
|
745
|
+
# of timeouts)
|
|
746
|
+
externally_triggered = self.settings.state.trigger_source == SpectrometerState.TRIGGER_SOURCE_EXTERNAL
|
|
747
|
+
try:
|
|
748
|
+
while True:
|
|
749
|
+
req = SpectrometerRequest("get_spectrum")
|
|
750
|
+
res = self.hardware.handle_requests([req])[0]
|
|
751
|
+
if res.error_msg != '':
|
|
752
|
+
return res
|
|
753
|
+
|
|
754
|
+
# @todo get rid of spectrum_and_row...get_spectrum() can go back to only returning spectrum
|
|
755
|
+
spectrum_and_row = res.data
|
|
756
|
+
if res.poison_pill:
|
|
757
|
+
# float up poison
|
|
758
|
+
take_one_response.transfer_response(res)
|
|
759
|
+
return take_one_response
|
|
760
|
+
if res.keep_alive:
|
|
761
|
+
# float up keep alive
|
|
762
|
+
take_one_response.transfer_response(res)
|
|
763
|
+
return take_one_response
|
|
764
|
+
if isinstance(spectrum_and_row, bool):
|
|
765
|
+
# get_spectrum returned a poison-pill, so flow it upstream
|
|
766
|
+
take_one_response.poison_pill = True
|
|
767
|
+
return take_one_response
|
|
768
|
+
|
|
769
|
+
if self.hardware.shutdown_requested:
|
|
770
|
+
take_one_response.poison_pill = True
|
|
771
|
+
return take_one_response
|
|
772
|
+
|
|
773
|
+
if spectrum_and_row is None or spectrum_and_row.spectrum is None:
|
|
774
|
+
# FeatureIdentificationDevice can return None when waiting
|
|
775
|
+
# on an external trigger.
|
|
776
|
+
log.debug("take_one_averaged_reading: get_spectrum None, sending keepalive for now")
|
|
777
|
+
take_one_response.transfer_response(res)
|
|
778
|
+
return take_one_response
|
|
779
|
+
else:
|
|
780
|
+
break
|
|
781
|
+
|
|
782
|
+
reading.spectrum = spectrum_and_row.spectrum
|
|
783
|
+
reading.timestamp_complete = datetime.datetime.now()
|
|
784
|
+
|
|
785
|
+
log.debug(f"take_one_averaged_reading: got {reading.spectrum[0:9]}")
|
|
786
|
+
except Exception as exc:
|
|
787
|
+
# if we got the timeout after switching from externally triggered back to internal, let it ride
|
|
788
|
+
take_one_response.error_msg = exc
|
|
789
|
+
take_one_response.error_lvl = ErrorLevel.medium
|
|
790
|
+
take_one_response.keep_alive = True
|
|
791
|
+
if externally_triggered:
|
|
792
|
+
log.debug("caught exception from get_spectrum while externally triggered...sending keepalive")
|
|
793
|
+
return take_one_response
|
|
794
|
+
|
|
795
|
+
log.critical("Error reading hardware data", exc_info=1)
|
|
796
|
+
reading.spectrum = None
|
|
797
|
+
reading.failure = str(exc)
|
|
798
|
+
|
|
799
|
+
####################################################################
|
|
800
|
+
# Aggregate scan averaging
|
|
801
|
+
####################################################################
|
|
802
|
+
|
|
803
|
+
# It's important to note here that the scan averaging "sum" buffer
|
|
804
|
+
# here (self.summed_spectra) and counter (self.sum_count) are
|
|
805
|
+
# attributes of this WasatchDevice object: they are not part of the
|
|
806
|
+
# Reading being sent back to the caller.
|
|
807
|
+
#
|
|
808
|
+
# The current architecture in Wasatch.PY, which is a little different
|
|
809
|
+
# than other Wasatch drivers, is that even when scan averaging is
|
|
810
|
+
# enabled, EVERY SPECTRUM will be flowed back to the caller.
|
|
811
|
+
#
|
|
812
|
+
# This is because early versions of ENLIGHTEN showed the "individual"
|
|
813
|
+
# (pre-averaged) spectra as a faint grey trace in the background,
|
|
814
|
+
# updating with each integration, showing the higher noise level of
|
|
815
|
+
# "raw" spectra while an on-screen counter showed the incrementing
|
|
816
|
+
# tally of summed spectra in the buffer.
|
|
817
|
+
#
|
|
818
|
+
# Then, when the FINAL raw spectrum had been read and the complete
|
|
819
|
+
# average could be computed, that averaged spectrum was returned
|
|
820
|
+
# with a special flag in the Reading object to indicate "fully
|
|
821
|
+
# averaged."
|
|
822
|
+
#
|
|
823
|
+
# We no longer show the "background" spectral trace in ENLIGHTEN,
|
|
824
|
+
# so this is kind of wasting some intra-process bandwidth, but we
|
|
825
|
+
# do still increment the on-screen tally as feedback while the
|
|
826
|
+
# averaging is taking place, so the in-process Reading messages
|
|
827
|
+
# sent during an averaged collection are not entirely wasted.
|
|
828
|
+
#
|
|
829
|
+
# Still, this is not the way we would have designed a Python driver
|
|
830
|
+
# "from a blank sheet," and our other driver architectures show that.
|
|
831
|
+
#
|
|
832
|
+
# @todo: since we're actually passing every pre-averaged spectrum
|
|
833
|
+
# back to ENLIGHTEN, for non-TakeOneRequest averaging, I feel like
|
|
834
|
+
# we should probably perform the summation in ENLIGHTEN too (for
|
|
835
|
+
# non-TakeOneRequest averages).
|
|
836
|
+
|
|
837
|
+
if not reading.failure:
|
|
838
|
+
# log.debug("take_one_averaged_reading: not failure")
|
|
839
|
+
if sum_locally:
|
|
840
|
+
log.debug("take_one_averaged_reading: summing locally")
|
|
841
|
+
if self.sum_count == 0:
|
|
842
|
+
log.debug("take_one_averaged_reading: first spectrum (initializing)")
|
|
843
|
+
self.summed_spectra = [float(i) for i in reading.spectrum]
|
|
844
|
+
else:
|
|
845
|
+
log.debug("take_one_averaged_reading: adding to previous")
|
|
846
|
+
for i in range(len(self.summed_spectra)):
|
|
847
|
+
self.summed_spectra[i] += reading.spectrum[i]
|
|
848
|
+
self.sum_count += 1
|
|
849
|
+
log.debug("take_one_averaged_reading: summed_spectra : %s ...", self.summed_spectra[0:9])
|
|
850
|
+
|
|
851
|
+
# count spectra
|
|
852
|
+
self.session_reading_count += 1
|
|
853
|
+
reading.session_count = self.session_reading_count
|
|
854
|
+
reading.sum_count = self.sum_count
|
|
855
|
+
log.debug(f"take_one_averaged_reading: reading.sum_count now {reading.sum_count}, session_count {reading.session_count}")
|
|
856
|
+
|
|
857
|
+
# have we completed the averaged reading?
|
|
858
|
+
if sum_locally:
|
|
859
|
+
log.debug(f"take_one_averaged_reading: checking for completion self.sum_count {self.sum_count} >?= scans_to_average {scans_to_average}")
|
|
860
|
+
if self.sum_count >= scans_to_average:
|
|
861
|
+
reading.spectrum = [ x / self.sum_count for x in self.summed_spectra ]
|
|
862
|
+
log.debug("take_one_averaged_reading: averaged_spectrum : %s ...", reading.spectrum[0:9])
|
|
863
|
+
reading.averaged = True
|
|
864
|
+
|
|
865
|
+
# reset for next average
|
|
866
|
+
self.summed_spectra = None
|
|
867
|
+
self.sum_count = 0
|
|
868
|
+
else:
|
|
869
|
+
# if averaging isn't enabled...then a single reading is the
|
|
870
|
+
# "averaged" final measurement (check reading.sum_count to confirm)
|
|
871
|
+
reading.averaged = True
|
|
872
|
+
|
|
873
|
+
log.debug("device.take_one_averaged_reading: returning %s", reading)
|
|
874
|
+
take_one_response.data = reading
|
|
875
|
+
return take_one_response
|
|
876
|
+
|
|
877
|
+
def monitor_memory(self):
|
|
878
|
+
now = datetime.datetime.now()
|
|
879
|
+
if (now - self.last_memory_check).total_seconds() < 5:
|
|
880
|
+
return
|
|
881
|
+
|
|
882
|
+
self.last_memory_check = now
|
|
883
|
+
size_in_bytes = psutil.Process(self.process_id).memory_info().rss
|
|
884
|
+
log.info("monitor_memory: PID %d memory = %d bytes", self.process_id, size_in_bytes)
|
|
885
|
+
|
|
886
|
+
##
|
|
887
|
+
# Process every entry on the incoming command (settings) queue, writing each
|
|
888
|
+
# to the device.
|
|
889
|
+
#
|
|
890
|
+
# Essentially this iterates through all the (setting, value) pairs we've
|
|
891
|
+
# received through change_setting() which have not yet been processed, and <-- MZ: incomplete sentence
|
|
892
|
+
#
|
|
893
|
+
#
|
|
894
|
+
# Note that WrapperWorker.run "de-dupes" commands on
|
|
895
|
+
# receipt from ENLIGHTEN, so the command stream arising from that source
|
|
896
|
+
# should already be optimized and minimal. Commands injected manually by
|
|
897
|
+
# calling WasatchDevice.change_setting() do not receive this treatment.
|
|
898
|
+
#
|
|
899
|
+
# In the normal multithreaded (ENLIGHTEN) workflow, this function is called
|
|
900
|
+
# at the beginning of acquire_data, itself ticked regularly by
|
|
901
|
+
# WrapperWorker.run.
|
|
902
|
+
def process_commands(self):
|
|
903
|
+
control_object = "throwaway"
|
|
904
|
+
retval = False
|
|
905
|
+
log.debug("process_commands: processing")
|
|
906
|
+
while len(self.command_queue) > 0:
|
|
907
|
+
control_object = self.command_queue.pop(0)
|
|
908
|
+
log.debug("process_commands: %s", control_object)
|
|
909
|
+
|
|
910
|
+
# is this a command used by WasatchDevice itself, and not
|
|
911
|
+
# passed down to FeatureIdentificationDevice?
|
|
912
|
+
if control_object.setting.lower() == "acquire":
|
|
913
|
+
# MZ: does this ever happen? It looks like the current
|
|
914
|
+
# request sent down from WrapperWorker is "acquire_data"
|
|
915
|
+
# not "acquire"...
|
|
916
|
+
log.debug("process_commands: acquire found")
|
|
917
|
+
retval = True
|
|
918
|
+
else:
|
|
919
|
+
# send setting downstream to be processed by the spectrometer HAL
|
|
920
|
+
# (probably FeatureIdentificationDevice)
|
|
921
|
+
req = SpectrometerRequest(control_object.setting, args=[control_object.value])
|
|
922
|
+
self.hardware.handle_requests([req])
|
|
923
|
+
|
|
924
|
+
return retval
|
|
925
|
+
|
|
926
|
+
def _init_process_funcs(self): # -> dict[str, Callable[..., Any]]
|
|
927
|
+
process_f = {}
|
|
928
|
+
|
|
929
|
+
process_f["connect"] = self.connect
|
|
930
|
+
process_f["disconnect"] = self.disconnect
|
|
931
|
+
process_f["acquire_data"] = self.acquire_data
|
|
932
|
+
|
|
933
|
+
return process_f
|
|
934
|
+
|
|
935
|
+
# ######################################################################## #
|
|
936
|
+
# #
|
|
937
|
+
# Area Scan #
|
|
938
|
+
# #
|
|
939
|
+
# ######################################################################## #
|
|
940
|
+
|
|
941
|
+
def acquire_area_scan(self):
|
|
942
|
+
"""
|
|
943
|
+
FeatureIdentificationDevice.get_area_scan returns a Reading because it
|
|
944
|
+
really needs to return both the AreaScanImage (whatever form that may
|
|
945
|
+
take) and the vertically-binned spectrum (for live display).
|
|
946
|
+
|
|
947
|
+
@returns a SpectrometerResponse(data=Reading)
|
|
948
|
+
"""
|
|
949
|
+
reading = self.hardware.get_area_scan()
|
|
950
|
+
self.session_reading_count += 1
|
|
951
|
+
reading.session_count = self.session_reading_count
|
|
952
|
+
return SpectrometerResponse(data=reading)
|
|
953
|
+
|
|
954
|
+
# ######################################################################## #
|
|
955
|
+
# #
|
|
956
|
+
# BalanceAcquisition #
|
|
957
|
+
# #
|
|
958
|
+
# ######################################################################## #
|
|
959
|
+
|
|
960
|
+
def balance_acquisition(self,
|
|
961
|
+
device = None,
|
|
962
|
+
mode = None,
|
|
963
|
+
intensity = 45000,
|
|
964
|
+
threshold = 2500,
|
|
965
|
+
pixel = None,
|
|
966
|
+
max_integration_time_ms = 5000,
|
|
967
|
+
max_tries = 20):
|
|
968
|
+
balancer = BalanceAcquisition(
|
|
969
|
+
device = self,
|
|
970
|
+
mode = mode,
|
|
971
|
+
intensity = intensity,
|
|
972
|
+
threshold = threshold,
|
|
973
|
+
pixel = pixel,
|
|
974
|
+
max_integration_time_ms = max_integration_time_ms,
|
|
975
|
+
max_tries = max_tries)
|
|
976
|
+
return balancer.balance()
|
|
977
|
+
|
|
978
|
+
# ######################################################################## #
|
|
979
|
+
# #
|
|
980
|
+
# Hardware Control #
|
|
981
|
+
# #
|
|
982
|
+
# ######################################################################## #
|
|
983
|
+
|
|
984
|
+
##
|
|
985
|
+
# Processes an incoming (setting, value) pair.
|
|
986
|
+
#
|
|
987
|
+
# Some settings are processed internally within this function, if the
|
|
988
|
+
# functionality they are controlling is implemented by WasatchDevice.
|
|
989
|
+
# This includes scan averaging, and anything related to scan averaging
|
|
990
|
+
# (such as "take one" behavior).
|
|
991
|
+
#
|
|
992
|
+
# Most tuples are queued to be sent downstream to the connected hardware
|
|
993
|
+
# (usually FeatureIdentificationDevice) at the start of the next
|
|
994
|
+
# acquisition.
|
|
995
|
+
#
|
|
996
|
+
# Some hardware settings (those involving triggering or the laser) are
|
|
997
|
+
# sent downstream immediately, rather than waiting for the next "scheduled"
|
|
998
|
+
# settings update.
|
|
999
|
+
#
|
|
1000
|
+
# ENLIGHTEN commands to WasatchDeviceWrapper are sent here by
|
|
1001
|
+
# WasatchDeviceWrapper.continuous_poll.
|
|
1002
|
+
#
|
|
1003
|
+
# @param setting (Input) which setting to change
|
|
1004
|
+
# @param value (Input) the new value of the setting (required, but can
|
|
1005
|
+
# be None or "anything" for commands like "acquire" which
|
|
1006
|
+
# don't use the argument).
|
|
1007
|
+
# @param allow_immediate
|
|
1008
|
+
def change_setting(self, setting: str, value: Any, allow_immediate: bool = True): # -> None
|
|
1009
|
+
log.debug(f"WasatchDevice.change_setting: {setting} -> {value}")
|
|
1010
|
+
|
|
1011
|
+
# Since scan averaging lives in WasatchDevice, handle commands which affect
|
|
1012
|
+
# averaging at this level
|
|
1013
|
+
if setting == "scans_to_average":
|
|
1014
|
+
self.sum_count = 0
|
|
1015
|
+
self.settings.state.scans_to_average = int(value)
|
|
1016
|
+
return
|
|
1017
|
+
elif setting == "reset_scan_averaging":
|
|
1018
|
+
self.sum_count = 0
|
|
1019
|
+
return
|
|
1020
|
+
elif setting == "take_one_request":
|
|
1021
|
+
self.sum_count = 0
|
|
1022
|
+
self.take_one_request = value
|
|
1023
|
+
return
|
|
1024
|
+
elif setting == "cancel_take_one":
|
|
1025
|
+
self.sum_count = 0
|
|
1026
|
+
self.take_one_request = None
|
|
1027
|
+
return
|
|
1028
|
+
|
|
1029
|
+
control_object = ControlObject(setting, value)
|
|
1030
|
+
self.command_queue.append(control_object)
|
|
1031
|
+
log.debug("change_setting: queued %s", control_object)
|
|
1032
|
+
|
|
1033
|
+
# always process trigger_source commands promptly (can't wait for end of
|
|
1034
|
+
# acquisition which may never come)
|
|
1035
|
+
if (allow_immediate and self.immediate_mode) or re.search(r"trigger|laser", setting):
|
|
1036
|
+
log.debug("immediately processing %s", control_object)
|
|
1037
|
+
self.process_commands()
|
|
1038
|
+
|
|
1039
|
+
# override handle_requests. I didn't see a good way to overwrite change_setting without
|
|
1040
|
+
# having to redo that pass through.
|
|
1041
|
+
def handle_requests(self, requests: list[SpectrometerRequest]): # -> list[SpectrometerResponse]
|
|
1042
|
+
responses = []
|
|
1043
|
+
for request in requests:
|
|
1044
|
+
try:
|
|
1045
|
+
cmd = request.cmd
|
|
1046
|
+
proc_func = self.process_f.get(cmd, None)
|
|
1047
|
+
if proc_func is None:
|
|
1048
|
+
try:
|
|
1049
|
+
self.change_setting(cmd, *request.args, **request.kwargs)
|
|
1050
|
+
except Exception as e:
|
|
1051
|
+
log.error(f"error {e} with trying to set setting {cmd} with args and kwargs {request.args} and {request.kwargs}", exc_info=1)
|
|
1052
|
+
return []
|
|
1053
|
+
elif request.args == [] and request.kwargs == {}:
|
|
1054
|
+
responses.append(proc_func())
|
|
1055
|
+
else:
|
|
1056
|
+
responses.append(proc_func(*request.args, **request.kwargs))
|
|
1057
|
+
except Exception as e:
|
|
1058
|
+
log.error(f"error in handling request {request} of {e}", exc_info=1)
|
|
1059
|
+
responses.append(SpectrometerResponse(error_msg="error processing cmd", error_lvl=ErrorLevel.medium))
|
|
1060
|
+
return responses
|