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
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import random
|
|
3
|
+
import logging
|
|
4
|
+
import datetime
|
|
5
|
+
|
|
6
|
+
from queue import Queue
|
|
7
|
+
|
|
8
|
+
from .SpectrometerResponse import SpectrometerResponse
|
|
9
|
+
from .ControlObject import ControlObject
|
|
10
|
+
from .WrapperWorker import WrapperWorker
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Wrap WasatchDevice in a non-blocking interface run in a separate
|
|
16
|
+
# thread, using multiprocess.pipes to exchange data (SpectrometerSettings,
|
|
17
|
+
# Readings and StatusMessages) for multiprocessing-safe device communications
|
|
18
|
+
# and acquisition under Windows and Qt.
|
|
19
|
+
#
|
|
20
|
+
# @todo This document is full of references to continuous_poll(). That method
|
|
21
|
+
# no longer exists, and has been replaced by WrapperWorker.run()
|
|
22
|
+
#
|
|
23
|
+
# @par Lifecycle
|
|
24
|
+
#
|
|
25
|
+
# From ENLIGHTEN's standpoint (the original Wasatch.PY caller), here's what's going on:
|
|
26
|
+
#
|
|
27
|
+
# - MainProcess (enlighten.Controller.setup_bus_listener) instantiates a
|
|
28
|
+
# wasatch.WasatchBus (bus) which will be persistent through the application
|
|
29
|
+
# lifetime. setup_bus_listener also creates a QTimer (bus_timer) which will
|
|
30
|
+
# check the USB bus for newly-connected ("hotplug") devices every second or so.
|
|
31
|
+
#
|
|
32
|
+
# - Controller.tick_bus_listener
|
|
33
|
+
# - does nothing (silently reschedules itself) if any new spectrometers are
|
|
34
|
+
# actively in the process of connecting, because things get hairy if we're
|
|
35
|
+
# trying to enumerate and configure several spectrometers at once
|
|
36
|
+
#
|
|
37
|
+
# - calls bus.update(), which will internally instantiate and use a
|
|
38
|
+
# wasatch.DeviceFinderUSB to scan the current USB bus and update its internal
|
|
39
|
+
# list of all Wasatch spectrometers (whether already connected or otherwise)
|
|
40
|
+
#
|
|
41
|
+
# - then calls Controller.connect_new() to process the updated device list
|
|
42
|
+
# (including determining whether any new devices are visible, and if so what
|
|
43
|
+
# to do about them)
|
|
44
|
+
#
|
|
45
|
+
# - Controller.connect_new()
|
|
46
|
+
# - if there is at least one new spectrometer on the device list, pull off that
|
|
47
|
+
# ONE device for connection (don't iterate over multiple new devices...we'll
|
|
48
|
+
# get them on a subsequent bus tick).
|
|
49
|
+
#
|
|
50
|
+
# - instantiates a WasatchDeviceWrapper and then calls connect() on it
|
|
51
|
+
# - WasatchDeviceWrapper.connect()
|
|
52
|
+
# - spawns a thread running the continuous_poll() method of the _same_
|
|
53
|
+
# WasatchDeviceWrapper instance
|
|
54
|
+
# - the WDW in the MainProcess then waits (blocks) while waiting for a
|
|
55
|
+
# single SpectrometerSettings object to be returned via a pipe from the
|
|
56
|
+
# child thread. This doesn't block the GUI, because this whole sequence
|
|
57
|
+
# is occuring in a background tick event on the bus_timer QTimer.
|
|
58
|
+
# - the WDW in the child threadis running continuous_poll()
|
|
59
|
+
# - instantiates a WasatchDevice to access the actual hardware spectrometer
|
|
60
|
+
# over USB (this object will only ever be referenced within this thread)
|
|
61
|
+
# - then calls WasatchDevice.connect()
|
|
62
|
+
# - WasatchDevice then instantiates an internal FeatureIdentificationDevice
|
|
63
|
+
# - WasatchDevice then populates a SpectrometerSettings object based on
|
|
64
|
+
# the connected device (loading the EEPROM, basic firmware settings etc)
|
|
65
|
+
# - continuous_poll() sends back the single SpectrometerSettings object
|
|
66
|
+
# to the blocked MainProcess by way of confirmation that a new spectrometer
|
|
67
|
+
# has successfully connected
|
|
68
|
+
# - Controller.connect_new() then completes the initialization of the spectrometer
|
|
69
|
+
# in the GUI by calling Controller.initialize_new_device(), which adds the spectrometer
|
|
70
|
+
# to Multispec, updates the EEPROMEditor, defaults the TEC controls and so on.
|
|
71
|
+
# - In the background, WasatchDeviceWrapper.continuous_poll() continues running, acting as a
|
|
72
|
+
# "free-running mode driver" of the spectrometer, passing down new commands from the
|
|
73
|
+
# enlighten.Controller, and feeding back Readings or StatusMessages when they appear.
|
|
74
|
+
#
|
|
75
|
+
# @par Shutdown
|
|
76
|
+
#
|
|
77
|
+
# The whole thing can be shutdown in various ways, using the concept of "poison pill"
|
|
78
|
+
# messages which can be flowed either upstream or downstream:
|
|
79
|
+
#
|
|
80
|
+
# - if a hardware error occurs in the spectrometer thread, it sends a poison
|
|
81
|
+
# pill upstream (a False value where a Reading is expected), then self-destructs
|
|
82
|
+
# (Controller is expected to drop the spectrometer from the GUI)
|
|
83
|
+
# - if the GUI is closing, poison-pills (None values where ControlObjects are expected)
|
|
84
|
+
# are sent downstream to each spectrometer thread, telling them to terminate themselves
|
|
85
|
+
#
|
|
86
|
+
# @par Throughput Considerations
|
|
87
|
+
#
|
|
88
|
+
# It is important to recognize that continuous_poll() updates the command/response
|
|
89
|
+
# pipes at a relatively leisurely interval (currently 20Hz, set in POLLER_WAIT_SEC).
|
|
90
|
+
# No matter how short the integration time is (1ms), you're not going to get spectra
|
|
91
|
+
# faster than 20 per second through this (as ENLIGHTEN was designed as a real-time
|
|
92
|
+
# visualization tool, not a high-speed data collection tool).
|
|
93
|
+
#
|
|
94
|
+
# Now, if you set ACQUISITION_MODE_KEEP_ALL, then you should still get every
|
|
95
|
+
# spectrum (whatever the spectrometer achieved through its scan-rate, potentially
|
|
96
|
+
# 220/sec or so) -- but you'll get them in chunks (e.g., scan rate of 220/sec
|
|
97
|
+
# polled at 20Hz = 20 chunks of 11 ea).
|
|
98
|
+
#
|
|
99
|
+
# @par Responsiveness
|
|
100
|
+
#
|
|
101
|
+
# With regard to the "immediacy" of commands like laser_enable, note that
|
|
102
|
+
# spectrometer threads are internally single-threaded: the continuous_poll()
|
|
103
|
+
# function BLOCKS on WasatchDevice.acquire_data(), meaning that even if new
|
|
104
|
+
# laser_enable commands get pushed into the downstream command pipe,
|
|
105
|
+
# continuous_poll won't check for them until the end of the current acquisition.
|
|
106
|
+
#
|
|
107
|
+
# What we'd really like is two threads running in the child, one handling
|
|
108
|
+
# acquisitions and most commands, and another handling high-priority events like
|
|
109
|
+
# laser_enable. I'm not sure the pipes are designed for multiple readers, but
|
|
110
|
+
# a SEPARATE pipe (or queue) could be setup for high-priority commands, and
|
|
111
|
+
# EXCLUSIVELY read by the secondary thread. That just leaves open the question
|
|
112
|
+
# of synchronization on WasatchDevice's USBDevice.
|
|
113
|
+
#
|
|
114
|
+
class WasatchDeviceWrapper:
|
|
115
|
+
|
|
116
|
+
ACQUISITION_MODE_KEEP_ALL = 0 # don't drop frames
|
|
117
|
+
ACQUISITION_MODE_LATEST = 1 # only grab most-recent frame (allow dropping)
|
|
118
|
+
ACQUISITION_MODE_KEEP_COMPLETE = 2 # generally grab most-recent frame (allow dropping),
|
|
119
|
+
# except don't drop FULLY AVERAGED frames (summation
|
|
120
|
+
# contributors can be skipped). If the most-recent
|
|
121
|
+
# frame IS a partial summation contributor, then send it on.
|
|
122
|
+
|
|
123
|
+
# ##########################################################################
|
|
124
|
+
# #
|
|
125
|
+
# Parent Thread #
|
|
126
|
+
# #
|
|
127
|
+
# ##########################################################################
|
|
128
|
+
|
|
129
|
+
##
|
|
130
|
+
# Instantiated by Controller.connect_new(), if-and-only-if a WasatchBus
|
|
131
|
+
# reports a DeviceID which has not already connected to the GUI. The DeviceID
|
|
132
|
+
# is "unique and relevant" to the bus which reported it, but neither the
|
|
133
|
+
# bus class nor instance is passed in to this object. If the DeviceID looks like
|
|
134
|
+
# "USB:VID:PID:bus:addr", then it is presumably USB. Future DeviceID formats
|
|
135
|
+
# could include "FILE:/path/to/dir", etc. However, device_id is just
|
|
136
|
+
# a string scalar to this class, and actually parsing / using it should be
|
|
137
|
+
# entirely encapsulated within WasatchDevice and lower using DeviceID.
|
|
138
|
+
def __init__(self, device_id, log_level, callback=None):
|
|
139
|
+
self.device_id = device_id
|
|
140
|
+
self.log_level = log_level
|
|
141
|
+
self.callback = callback
|
|
142
|
+
|
|
143
|
+
self.settings_queue = Queue() # spectrometer -> GUI (SpectrometerSettings, one-time)
|
|
144
|
+
self.response_queue = Queue() # spectrometer -> GUI (Readings)
|
|
145
|
+
self.message_queue = Queue() # spectrometer -> GUI (StatusMessages)
|
|
146
|
+
self.command_queue = Queue() # GUI -> spectrometer (ControlObjects)
|
|
147
|
+
self.alert_queue = Queue() # GUI -> spectrometer (ControlObjects)
|
|
148
|
+
|
|
149
|
+
self.connected = False
|
|
150
|
+
self.closing = False # Don't permit new acquires during close
|
|
151
|
+
self.poller = None # a handle to the child thread
|
|
152
|
+
self.is_ocean = '0x2457' in str(device_id)
|
|
153
|
+
self.is_andor = '0x136e' in str(device_id)
|
|
154
|
+
self.is_spi = '0x0403' in str(device_id)
|
|
155
|
+
self.is_ids = 'IDSPeak' in str(device_id)
|
|
156
|
+
self.mock = 'MOCK' in str(device_id).upper()
|
|
157
|
+
self.is_ble = 'BLE' in str(device_id)
|
|
158
|
+
self.is_tcp = 'TCP' in str(device_id)
|
|
159
|
+
self.wrapper_worker = None
|
|
160
|
+
self.connect_start_time = datetime.datetime(year=datetime.MAXYEAR, month=1, day=1)
|
|
161
|
+
|
|
162
|
+
# this will contain a populated SpectrometerSettings object from the
|
|
163
|
+
# WasatchDevice, for relay to the instantiating Controller
|
|
164
|
+
self.settings = None
|
|
165
|
+
|
|
166
|
+
self.previous_reading = None
|
|
167
|
+
|
|
168
|
+
self.reset_tries = 0
|
|
169
|
+
|
|
170
|
+
##
|
|
171
|
+
# Create a low level device object with the specified identifier, kick off
|
|
172
|
+
# the child thread to attempt to read from it.
|
|
173
|
+
#
|
|
174
|
+
# Called by Controller.connect_new() immediately after instantiation
|
|
175
|
+
#
|
|
176
|
+
# Spawn a child thread running the continuous_poll() method on THIS
|
|
177
|
+
# object instance.
|
|
178
|
+
#
|
|
179
|
+
# The two threads are coupled via the 3 queues:
|
|
180
|
+
#
|
|
181
|
+
# @par settings_queue
|
|
182
|
+
#
|
|
183
|
+
# Lets the child (WasatchDevice) send a single, one-time copy
|
|
184
|
+
# of a populated SpectrometerSettings object back through the
|
|
185
|
+
# Wrapper to the calling Controller. This is the primary way the
|
|
186
|
+
# Controller knows what kind of spectrometer it has connected to,
|
|
187
|
+
# what hardware features and EEPROM settings are applied etc.
|
|
188
|
+
#
|
|
189
|
+
# Thereafter both WasatchDevice and Controller will maintain
|
|
190
|
+
# their own copies of SpectrometerSettings, and they are not
|
|
191
|
+
# automatically synchronized (is there a clever way to do this?).
|
|
192
|
+
# They may well drift out-of-sync regarding state, although the
|
|
193
|
+
# command_queue helps keep them somewhat in sync.
|
|
194
|
+
#
|
|
195
|
+
# @par command_queue
|
|
196
|
+
#
|
|
197
|
+
# The Controller will send ControlObject instances (basically
|
|
198
|
+
# (name, value) pairs) through the Wrapper to the WasatchDevice
|
|
199
|
+
# to set attributes or commands on the WasatchDevice spectrometer.
|
|
200
|
+
# These may be volatile hardware settings (laser power, integration
|
|
201
|
+
# time), meta-commands to the WasatchDevice class (scan averaging),
|
|
202
|
+
# or EEPROM updates.
|
|
203
|
+
#
|
|
204
|
+
# @par response_queue
|
|
205
|
+
#
|
|
206
|
+
# The WasatchDevice will stream a continuous series of Reading
|
|
207
|
+
# instances back through the Wrapper to the Controller. These
|
|
208
|
+
# each contain a newly read spectrum, as well as metadata about
|
|
209
|
+
# the spectrometer at the time the spectrum was taken (integration
|
|
210
|
+
# time, laser power), plus additional readings from the spectrometer
|
|
211
|
+
# (detector and laser temperature, secondary ADC).
|
|
212
|
+
#
|
|
213
|
+
def connect(self):
|
|
214
|
+
|
|
215
|
+
# instantiate thread
|
|
216
|
+
self.closing = False # needed if doing reset and closing previously was True
|
|
217
|
+
self.wrapper_worker = WrapperWorker(
|
|
218
|
+
device_id = self.device_id,
|
|
219
|
+
command_queue = self.command_queue, # Main --> child (dedupable single-threaded spectrometer commands)
|
|
220
|
+
alert_queue = self.alert_queue, # Main --> child (real-time hints and interrupts relating to ongoing commands)
|
|
221
|
+
response_queue = self.response_queue, # Main <-- child \
|
|
222
|
+
settings_queue = self.settings_queue, # Main <-- child | consolidate into
|
|
223
|
+
message_queue = self.message_queue, # Main <-- child / SpectrometerMessage?
|
|
224
|
+
is_ocean = self.is_ocean,
|
|
225
|
+
is_andor = self.is_andor,
|
|
226
|
+
is_spi = self.is_spi,
|
|
227
|
+
is_ble = self.is_ble,
|
|
228
|
+
is_tcp = self.is_tcp,
|
|
229
|
+
is_ids = self.is_ids,
|
|
230
|
+
log_level = self.log_level,
|
|
231
|
+
callback = self.callback)
|
|
232
|
+
log.debug("device wrapper: Instance created for worker")
|
|
233
|
+
|
|
234
|
+
self.wrapper_worker.daemon = True
|
|
235
|
+
|
|
236
|
+
log.debug("deivce wrapper: starting WrapperWorker thread")
|
|
237
|
+
self.wrapper_worker.start()
|
|
238
|
+
|
|
239
|
+
# expect to read a single post-initialization SpectrometerSettings object off the queue
|
|
240
|
+
self.connect_start_time = datetime.datetime.now()
|
|
241
|
+
self.settings = None
|
|
242
|
+
log.debug("connect: setup connection, returning to controller for settings polling")
|
|
243
|
+
|
|
244
|
+
if self.callback:
|
|
245
|
+
self.wait_for_settings()
|
|
246
|
+
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
def wait_for_settings(self):
|
|
250
|
+
while True:
|
|
251
|
+
if self.poll_settings():
|
|
252
|
+
break
|
|
253
|
+
if (datetime.datetime.now() - self.connect_start_time).total_seconds() > 2:
|
|
254
|
+
raise Exception("Failed to read SpectrometerSettings")
|
|
255
|
+
time.sleep(0.1)
|
|
256
|
+
|
|
257
|
+
def poll_settings(self):
|
|
258
|
+
"""
|
|
259
|
+
@returns SpectrometerResponse(True) on success, (False) otherwise
|
|
260
|
+
@note this doesn't have a timeout -- that's in enlighten.Controller.check_ready_initialize
|
|
261
|
+
"""
|
|
262
|
+
log.debug("polling device settings")
|
|
263
|
+
if not self.settings_queue.empty():
|
|
264
|
+
result = self.settings_queue.get_nowait()
|
|
265
|
+
if result is None:
|
|
266
|
+
log.critical("poll_settings: failed to retrieve device settings (got None, shouldn't happen)")
|
|
267
|
+
return SpectrometerResponse(False, error_msg="Failed to retrieve device settings")
|
|
268
|
+
|
|
269
|
+
if result.data:
|
|
270
|
+
log.info(f"got spectrometer settings for device")
|
|
271
|
+
self.connected = True
|
|
272
|
+
self.settings = result.data
|
|
273
|
+
self.connect_start_time = datetime.datetime(year=datetime.MAXYEAR, month=1, day=1)
|
|
274
|
+
self.settings.state.dump("WasatchDeviceWrapper.poll_settings")
|
|
275
|
+
return SpectrometerResponse(True)
|
|
276
|
+
else:
|
|
277
|
+
log.critical("got error response instead of settings from connection request")
|
|
278
|
+
return result
|
|
279
|
+
else:
|
|
280
|
+
log.debug("settings still not obtained, returning")
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
def reset(self):
|
|
284
|
+
if "USB" in str(self.device_id):
|
|
285
|
+
self.reset_tries += 1
|
|
286
|
+
self.command_queue.put(ControlObject("reset", None))
|
|
287
|
+
|
|
288
|
+
def disconnect(self):
|
|
289
|
+
# send poison pill to the child
|
|
290
|
+
self.closing = True
|
|
291
|
+
log.debug("disconnect: sending poison pill downstream")
|
|
292
|
+
try:
|
|
293
|
+
self.command_queue.put(None)
|
|
294
|
+
except:
|
|
295
|
+
pass
|
|
296
|
+
time.sleep(0.1)
|
|
297
|
+
log.debug("disconnect: done")
|
|
298
|
+
del self.wrapper_worker
|
|
299
|
+
|
|
300
|
+
self.connected = False
|
|
301
|
+
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
##
|
|
305
|
+
# Similar to acquire_data, this method is called by the Controller in
|
|
306
|
+
# MainProcess to dequeue a StatusMessage from the spectrometer child
|
|
307
|
+
# thread, if one is available.
|
|
308
|
+
def acquire_status_message(self):
|
|
309
|
+
if self.closing or not self.connected:
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
if not self.message_queue.empty():
|
|
313
|
+
return self.message_queue.get_nowait()
|
|
314
|
+
|
|
315
|
+
def send_alert(self, setting, value):
|
|
316
|
+
self.alert_queue.put(ControlObject(setting, value))
|
|
317
|
+
|
|
318
|
+
##
|
|
319
|
+
# This method is called by the Controller. It checks the response_queue it
|
|
320
|
+
# shares with the child thread to see if any Reading objects have been queued
|
|
321
|
+
# from the spectrometer to the GUI.
|
|
322
|
+
#
|
|
323
|
+
# It is the upstream interface's job to decide how to process the potentially
|
|
324
|
+
# voluminous amount of data returned from the device. get_last by default
|
|
325
|
+
# will make sure the queue is cleared, then return the most recent reading
|
|
326
|
+
# from the device.
|
|
327
|
+
#
|
|
328
|
+
# @note it is not clear that measurement modes other than
|
|
329
|
+
# ACQUISITION_MODE_KEEP_COMPLETE have been well-tested,
|
|
330
|
+
# especially in the context of multiple spectrometers,
|
|
331
|
+
# BatchCollection etc.
|
|
332
|
+
def acquire_data(self, mode=None): # -> SpectrometerResponse
|
|
333
|
+
if self.closing or not self.connected:
|
|
334
|
+
log.critical(f"acquire_data: closing {self.closing} (sending poison-pill upstream) and connected {self.connected}")
|
|
335
|
+
return SpectrometerResponse(False, poison_pill=True)
|
|
336
|
+
|
|
337
|
+
# ENLIGHTEN "default" - if we're doing scan averaging, take either
|
|
338
|
+
#
|
|
339
|
+
# 1. the NEWEST fully-averaged spectrum (purge the queue), or
|
|
340
|
+
# 2. if no fully-averaged spectra are in the queue, then the NEWEST
|
|
341
|
+
# incomplete (pre-averaged) spectrum (purging the queue).
|
|
342
|
+
#
|
|
343
|
+
# If we're not doing scan averaging, then take the NEWEST spectrum
|
|
344
|
+
# and purge the queue.
|
|
345
|
+
#
|
|
346
|
+
if mode is None or mode == self.ACQUISITION_MODE_KEEP_COMPLETE:
|
|
347
|
+
return self.get_final_item(keep_averaged=True)
|
|
348
|
+
|
|
349
|
+
## Read from the response queue until empty (or we find an averaged item)
|
|
350
|
+
#
|
|
351
|
+
# In the currently implementation, it seems unlikely that a "True" will ever
|
|
352
|
+
# be passed up (we're basically converting them to None here).
|
|
353
|
+
def get_final_item(self, keep_averaged=False): # -> SpectrometerResponse
|
|
354
|
+
last_response = SpectrometerResponse()
|
|
355
|
+
last_averaged_response = SpectrometerResponse()
|
|
356
|
+
dequeue_count = 0
|
|
357
|
+
|
|
358
|
+
while True:
|
|
359
|
+
# without waiting (don't block), just get the first item off the
|
|
360
|
+
# queue if there is one
|
|
361
|
+
wrapper_response = SpectrometerResponse()
|
|
362
|
+
if not self.response_queue.empty():
|
|
363
|
+
wrapper_response = self.response_queue.get_nowait()
|
|
364
|
+
else:
|
|
365
|
+
# If there is nothing more to read, then we've emptied the queue
|
|
366
|
+
# log.debug("get_final has nothing more to read, sending up readings")
|
|
367
|
+
break
|
|
368
|
+
|
|
369
|
+
# If we come across a keep_alive, ignore it for the moment.
|
|
370
|
+
# for now continue cleaning-out the queue.
|
|
371
|
+
if wrapper_response.keep_alive or wrapper_response.data is None:
|
|
372
|
+
# If that keep alive is associated with an error though float it up
|
|
373
|
+
if wrapper_response.keep_alive and wrapper_response.error_msg:
|
|
374
|
+
last_response = wrapper_response
|
|
375
|
+
break
|
|
376
|
+
log.debug("get_final_item: ignoring keepalive")
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
# If we come across a poison-pill, flow that up immediately --
|
|
380
|
+
# game-over, we're done
|
|
381
|
+
if wrapper_response.poison_pill:
|
|
382
|
+
log.critical("get_final_item: poison-pill!")
|
|
383
|
+
return wrapper_response
|
|
384
|
+
|
|
385
|
+
# apparently we read a Reading
|
|
386
|
+
log.debug(f"get_final_item: read Reading {wrapper_response.data}")
|
|
387
|
+
last_response = wrapper_response
|
|
388
|
+
dequeue_count += 1
|
|
389
|
+
|
|
390
|
+
# Was this the final spectrum in an averaged sequence?
|
|
391
|
+
#
|
|
392
|
+
# If so, grab a reference, but DON'T flow it up yet...there
|
|
393
|
+
# may be a NEWER fully-averaged spectrum later on.
|
|
394
|
+
#
|
|
395
|
+
# It is the purpose of this function ("get FINAL item...")
|
|
396
|
+
# to PURGE THE QUEUE -- we are not intending to leave any
|
|
397
|
+
# values in the queue (None, bool, or Readings of any kind.
|
|
398
|
+
if keep_averaged and wrapper_response.data.averaged:
|
|
399
|
+
# log.debug("keeping last_response as last_averaged_response")
|
|
400
|
+
last_averaged_response = wrapper_response
|
|
401
|
+
|
|
402
|
+
if last_response.data is None:
|
|
403
|
+
log.debug("wrapper worker floating up keep alive last reading")
|
|
404
|
+
# apparently we didn't read anything...just pass up a keepalive
|
|
405
|
+
last_averaged_response = None
|
|
406
|
+
last_response.keep_alive = True
|
|
407
|
+
return last_response
|
|
408
|
+
|
|
409
|
+
# apparently we read at least some readings. For interest, how how many
|
|
410
|
+
# readings did we throw away (not return up to ENLIGHTEN)?
|
|
411
|
+
if dequeue_count > 1:
|
|
412
|
+
log.debug("discarded %d readings", dequeue_count - 1)
|
|
413
|
+
|
|
414
|
+
# if we're doing averaging, and we found one or more averaged readings,
|
|
415
|
+
# return the latest of those
|
|
416
|
+
if last_averaged_response.data is not None:
|
|
417
|
+
# log.debug("returning latest averaged reading")
|
|
418
|
+
last_response = None
|
|
419
|
+
return last_averaged_response
|
|
420
|
+
|
|
421
|
+
# We've had every opportunity short-cut the process: we could have
|
|
422
|
+
# returned potential poison pills or completed averages, but found
|
|
423
|
+
# none of those. Yet apparently we did read some normal readings.
|
|
424
|
+
# Return the latest of those.
|
|
425
|
+
|
|
426
|
+
# log.debug("returning last_response")
|
|
427
|
+
return last_response
|
|
428
|
+
|
|
429
|
+
##
|
|
430
|
+
# Add the specified setting and value to the local control queue.
|
|
431
|
+
#
|
|
432
|
+
# In ENLIGHTEN, this is called by MainProcess.Controller.
|
|
433
|
+
#
|
|
434
|
+
# For OEM customers controlling the spectrometer via the non-blocking
|
|
435
|
+
# WasatchDeviceWrapper interface, this is the method you would call to
|
|
436
|
+
# change the various spectrometer settings.
|
|
437
|
+
#
|
|
438
|
+
# @see \ref README_SETTINGS.md for a list of valid settings you can
|
|
439
|
+
# pass, as well as any parameters expected by each
|
|
440
|
+
def change_setting(self, setting, value=None):
|
|
441
|
+
try:
|
|
442
|
+
if not self.connected:
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
log.debug("change_setting: %s => %s", setting, value)
|
|
446
|
+
control_object = ControlObject(setting, value)
|
|
447
|
+
|
|
448
|
+
self.command_queue.put(control_object)
|
|
449
|
+
|
|
450
|
+
return
|
|
451
|
+
except Exception as e:
|
|
452
|
+
log.error(f"found an error of {e}")
|