wasatch 2.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. wasatch/AbstractUSBDevice.py +29 -0
  2. wasatch/AndorDevice.py +910 -0
  3. wasatch/AreaScanImage.py +19 -0
  4. wasatch/AutoRaman.py +457 -0
  5. wasatch/AutoRamanRequest.py +96 -0
  6. wasatch/BLEDevice.py +1032 -0
  7. wasatch/BalanceAcquisition.py +136 -0
  8. wasatch/CSVLoader.py +188 -0
  9. wasatch/CommandSettings.py +95 -0
  10. wasatch/ControlObject.py +17 -0
  11. wasatch/DetectorROI.py +36 -0
  12. wasatch/DetectorRegions.py +158 -0
  13. wasatch/DeviceFinderBLE.py +107 -0
  14. wasatch/DeviceFinderTCP.py +20 -0
  15. wasatch/DeviceFinderUSB.py +236 -0
  16. wasatch/DeviceID.py +344 -0
  17. wasatch/EEPROM.py +1450 -0
  18. wasatch/EEPROM_multical.sav +48 -0
  19. wasatch/FPGAOptions.py +125 -0
  20. wasatch/FeatureIdentificationDevice.py +3961 -0
  21. wasatch/FirmwareRequirements.py +76 -0
  22. wasatch/HardwareInfo.py +36 -0
  23. wasatch/IDSCamera.py +916 -0
  24. wasatch/IDSDevice.py +329 -0
  25. wasatch/IMX385.py +179 -0
  26. wasatch/InterfaceDevice.py +40 -0
  27. wasatch/MockUSBDevice.py +353 -0
  28. wasatch/OceanDevice.py +215 -0
  29. wasatch/PollStatus.py +15 -0
  30. wasatch/ProcessedReading.py +352 -0
  31. wasatch/ROI.py +33 -0
  32. wasatch/Reading.py +80 -0
  33. wasatch/RealUSBDevice.py +64 -0
  34. wasatch/SPIDevice.py +628 -0
  35. wasatch/SpectrometerRequest.py +15 -0
  36. wasatch/SpectrometerResponse.py +38 -0
  37. wasatch/SpectrometerSettings.py +446 -0
  38. wasatch/SpectrometerState.py +255 -0
  39. wasatch/StatusMessage.py +14 -0
  40. wasatch/TCPDevice.py +409 -0
  41. wasatch/TakeOneRequest.py +43 -0
  42. wasatch/WasatchBus.py +87 -0
  43. wasatch/WasatchDevice.py +1060 -0
  44. wasatch/WasatchDeviceWrapper.py +452 -0
  45. wasatch/WrapperWorker.py +304 -0
  46. wasatch/__init__.py +4 -0
  47. wasatch/applog.py +177 -0
  48. wasatch/simulation_protocol.py +227 -0
  49. wasatch/utils.py +731 -0
  50. wasatch-2.3.2.dist-info/LICENSE +21 -0
  51. wasatch-2.3.2.dist-info/METADATA +347 -0
  52. wasatch-2.3.2.dist-info/RECORD +53 -0
  53. wasatch-2.3.2.dist-info/WHEEL +4 -0
@@ -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}")