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,304 @@
1
+ import threading
2
+ import platform
3
+ import logging
4
+ import time
5
+ from queue import Queue
6
+ from datetime import datetime
7
+
8
+ from .SpectrometerResponse import SpectrometerResponse
9
+ from .SpectrometerRequest import SpectrometerRequest
10
+ from .WasatchDevice import WasatchDevice
11
+ from .AndorDevice import AndorDevice
12
+ from .OceanDevice import OceanDevice
13
+ from .SPIDevice import SPIDevice
14
+ from .BLEDevice import BLEDevice
15
+ from .TCPDevice import TCPDevice
16
+ from .Reading import Reading
17
+
18
+ if "windows" in platform.platform().lower():
19
+ from .IDSDevice import IDSDevice
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ ##
24
+ # Continuously process in background thread. While waiting forever for the None
25
+ # poison pill on the command queue, continuously read from the device and post
26
+ # the results on the response queue.
27
+ #
28
+ # Consider moving this class, and WasatchDeviceWrapper, our of Wasatch.PY and
29
+ # into ENLIGHTEN. Let Wasatch.PY be a simple single-spectrometer blocking
30
+ # driver, similar to Wasatch.NET and Wasatch.VCPP. No need to have Wasatch.PY
31
+ # handle the threads.
32
+ class WrapperWorker(threading.Thread):
33
+
34
+ # TODO: make this dynamic:
35
+ # - initially on number of connected spectrometers
36
+ # - ideally on configured integration times per spectrometer
37
+ # - note that this is essentially ADDED to the total measurement time
38
+ # of EACH AND EVERY INTEGRATION
39
+ # TODO: replace if check for each type of spec with single call
40
+ # TODO: Create ABC of hardware device that keeps common functions like handle_requests
41
+ POLLER_WAIT_SEC = 0.05 # .05sec = 50ms = update from hardware device at 20Hz
42
+
43
+ DEBUG_SEC = 10 # enforce debug logging for the 1st 10sec after connecting a new spectrometer
44
+
45
+ def __init__(
46
+ self,
47
+ device_id,
48
+ command_queue,
49
+ response_queue,
50
+ settings_queue,
51
+ message_queue,
52
+ is_ocean, # MZ: all these bools should have just been a string "type"
53
+ is_andor,
54
+ is_spi,
55
+ is_ble,
56
+ is_tcp,
57
+ is_ids,
58
+ log_level,
59
+ callback=None,
60
+ alert_queue=None):
61
+
62
+ threading.Thread.__init__(self)
63
+
64
+ self.device_id = device_id
65
+ self.is_ocean = is_ocean
66
+ self.is_andor = is_andor
67
+ self.is_spi = is_spi
68
+ self.is_ble = is_ble
69
+ self.is_tcp = is_tcp
70
+ self.is_ids = is_ids
71
+ self.command_queue = command_queue
72
+ self.response_queue = response_queue
73
+ self.settings_queue = settings_queue
74
+ self.message_queue = message_queue
75
+ self.alert_queue = alert_queue
76
+ self.log_level = log_level
77
+ self.callback = callback
78
+
79
+ self.connected_device = None
80
+
81
+ self.thread_start = datetime.now()
82
+ self.initial_connection_logging = True
83
+
84
+ # enforce debug logging around ENLIGHTEN connections
85
+ if not self.callback:
86
+ logging.getLogger().setLevel("DEBUG")
87
+
88
+ ##
89
+ # This is essentially the main() loop in a thread.
90
+ # All communications with the parent thread are routed through
91
+ # one of the three queues (cmd inputs, response outputs, and
92
+ # a one-shot SpectrometerSettings).
93
+ def run(self):
94
+ if "windows" in platform.platform().lower():
95
+ is_options = (self.is_ocean, self.is_andor, self.is_ble, self.is_spi, self.is_tcp, self.is_ids)
96
+ device_classes = (OceanDevice, AndorDevice, BLEDevice, SPIDevice, TCPDevice, IDSDevice, WasatchDevice)
97
+ else:
98
+ is_options = (self.is_ocean, self.is_andor, self.is_ble, self.is_spi, self.is_tcp)
99
+ device_classes = (OceanDevice, AndorDevice, BLEDevice, SPIDevice, TCPDevice, WasatchDevice)
100
+
101
+ try:
102
+ if any(is_options):
103
+ type_connection = is_options.index(True)
104
+ log.debug(f"trying to instantiate device of type {device_classes[type_connection]}")
105
+ self.connected_device = device_classes[type_connection](
106
+ device_id = self.device_id,
107
+ message_queue = self.message_queue,
108
+ alert_queue = self.alert_queue)
109
+ else:
110
+ log.debug(f"Couldn't recognize device of {self.device_id} {is_options}, trying to instantiate as WasatchDevice")
111
+ self.connected_device = device_classes[device_classes.index(WasatchDevice)](
112
+ device_id = self.device_id,
113
+ message_queue = self.message_queue,
114
+ alert_queue = self.alert_queue)
115
+ except:
116
+ log.critical("exception instantiating device", exc_info=1)
117
+ return self.settings_queue.put(None)
118
+
119
+ log.debug("calling connect")
120
+ ok = False
121
+ req = SpectrometerRequest("connect")
122
+ try:
123
+ (ok,) = self.connected_device.handle_requests([req])
124
+ except:
125
+ log.critical("exception connecting", exc_info=1)
126
+ return self.settings_queue.put_nowait(SpectrometerResponse(error_msg="exception while connecting"))
127
+
128
+ log.debug(f"on connect request got results of {ok}")
129
+
130
+ if not ok.data:
131
+ log.critical("failed to connect")
132
+ return self.settings_queue.put_nowait(ok)
133
+
134
+ log.debug("successfully connected")
135
+
136
+ # send the SpectrometerSettings back to the GUI thread
137
+ log.debug("returning SpectrometerSettings to parent via SpectrometerResponse")
138
+ self.settings_queue.put_nowait(SpectrometerResponse(self.connected_device.settings))
139
+
140
+ log.debug("entering loop")
141
+ received_poison_pill_command = False # from ENLIGHTEN
142
+
143
+ num_connected_devices = 1
144
+ while True:
145
+ dedupped = self.dedupe(self.command_queue)
146
+
147
+ # apply dedupped commands
148
+ if dedupped:
149
+ for record in dedupped:
150
+ if record is None:
151
+ # We have received a "poison pill" (shutdown command)
152
+ # from ENLIGHTEN.
153
+ #
154
+ # Reminder, poison_pills moving DOWNSTREAM from ENLIGHTEN
155
+ # are None, while poison_pills moving UPSTREAM from WasatchDevice
156
+ # are indicated with SpectrometerResponse.poison_pill.
157
+ received_poison_pill_command = True
158
+ #
159
+ # Do NOT put a 'break' here just yet -- if caller is in process of
160
+ # cleaning shutting things down, let them switch off the
161
+ # laser etc in due sequence. We can break AFTER relaying
162
+ # applying the queued settings.
163
+ else:
164
+ try:
165
+ log.debug("processing command queue: %s", record.setting)
166
+
167
+ # basically, this simply moves each de-dupped command from
168
+ # WasatchDeviceWrapper.command_queue to WasatchDevice.command_queue,
169
+ # where it gets read during the next call to
170
+ # WasatchDevice.acquire_data.
171
+ if record.setting == "reset":
172
+ log.debug(f"calling reset from command queue")
173
+ req = SpectrometerRequest(record.setting, args=[record.value])
174
+ self.connected_device.handle_requests([req])
175
+
176
+ # peek in some settings locally
177
+ if record.setting == "num_connected_devices":
178
+ num_connected_devices = record.value
179
+ except:
180
+ log.error("failed to process record {record}, treating as poison pill", exc_info=1)
181
+ received_poison_pill_command = True
182
+ else:
183
+ log.debug("command queue empty")
184
+
185
+ if received_poison_pill_command:
186
+ # ...NOW we can break
187
+ log.critical("exiting per command queue (poison pill received)")
188
+ req = SpectrometerRequest("disconnect")
189
+ # I don't see this called. I think it should.
190
+ # At minimum it's required for the BLE devices
191
+ self.connected_device.handle_requests([req])
192
+ break
193
+
194
+ # ##################################################################
195
+ # Relay one upstream reading (Spectrometer -> GUI)
196
+ # ##################################################################
197
+
198
+ try:
199
+ # Note: this is a BLOCKING CALL. If integration time is longer
200
+ # than subprocess_timeout_sec, this call itself will trigger
201
+ # shutdown.
202
+ log.debug("acquiring data")
203
+ req = SpectrometerRequest("acquire_data")
204
+ (reading_response,) = self.connected_device.handle_requests([req])
205
+ #log.debug("continuous_poll: acquire_data returned %s", str(reading))
206
+ except:
207
+ log.critical("exception calling WasatchDevice.acquire_data", exc_info=1)
208
+ continue
209
+
210
+ if not isinstance(reading_response, SpectrometerResponse):
211
+ log.error(f"Reading is not type ReadingResponse. Should not get naked responses. Happened with request {req}")
212
+ continue
213
+
214
+ log.debug(f"response {reading_response} data is {reading_response.data}")
215
+
216
+ if self.callback:
217
+ log.debug("worker returning response via callback")
218
+ self.callback(reading_response)
219
+
220
+ elif reading_response.keep_alive:
221
+ log.debug("worker is flowing up keep_alive")
222
+ self.response_queue.put(reading_response)
223
+
224
+ elif reading_response.error_msg != "":
225
+ if reading_response.data is None:
226
+ reading_response.data = Reading()
227
+ self.response_queue.put(reading_response)
228
+
229
+ elif reading_response.data is None:
230
+ log.debug("worker saw no reading (but not error, either)")
231
+
232
+ elif not reading_response.data:
233
+ log.critical(f"hardware level error...exiting because data False")
234
+ reading_response.poison_pill = True
235
+ self.response_queue.put(reading_response)
236
+
237
+ elif reading_response.data.failure is not None:
238
+ log.critical(f"hardware level error...exiting because failure {reading_response.data.failure}")
239
+ reading_response.poison_pill = True
240
+ self.response_queue.put(reading_response)
241
+
242
+ elif reading_response.poison_pill:
243
+ log.critical(f"hardware level error...exiting because poison-pill")
244
+ self.response_queue.put(reading_response)
245
+
246
+ elif reading_response.data.spectrum is not None:
247
+ log.debug("sending Reading %d back to GUI thread (%s)", reading_response.data.session_count, reading_response.data.spectrum[0:5])
248
+ try:
249
+ self.response_queue.put_nowait(reading_response)
250
+ except:
251
+ log.error("unable to push Reading %d to GUI", reading_response.data.session_count, exc_info=1)
252
+
253
+ # We just successfully sent a reading back to the main thread.
254
+ # After sending 10sec of such readings, we can disable the default
255
+ # DEBUG logging and revert to whatever the caller requested.
256
+ if (self.initial_connection_logging and
257
+ (datetime.now() - self.thread_start).total_seconds() > self.DEBUG_SEC):
258
+ log.info(f"relaxing log_level to {self.log_level} after {self.DEBUG_SEC}sec")
259
+ logging.getLogger().setLevel(self.log_level)
260
+ self.initial_connection_logging = False
261
+
262
+ else:
263
+ log.error("received non-failure Reading without spectrum...ignoring?")
264
+
265
+ # only poll hardware buses at 20Hz
266
+ sleep_sec = WrapperWorker.POLLER_WAIT_SEC * num_connected_devices
267
+ log.debug("sleeping %.2f sec", sleep_sec)
268
+ time.sleep(sleep_sec)
269
+
270
+ ########################################################################
271
+ # we have exited the loop
272
+ ########################################################################
273
+
274
+ if received_poison_pill_command:
275
+ log.critical("exiting because of downstream poison-pill command from ENLIGHTEN")
276
+ else:
277
+ log.critical("exiting for no reason?!")
278
+
279
+ log.critical("done")
280
+
281
+ def dedupe(self, q: Queue):
282
+ keep = [] # list, not a set, because we want to keep it ordered
283
+ try:
284
+ while True:
285
+ if not q.empty():
286
+ control_object = q.get_nowait()
287
+
288
+ # treat None elements (poison pills) same as everything else
289
+ setting = None if control_object is None else control_object.setting
290
+
291
+ # remove previous setting if duplicate
292
+ new_keep = []
293
+ for co in keep:
294
+ if co.setting != setting:
295
+ new_keep.append(co)
296
+ keep = new_keep
297
+
298
+ # append the setting to the de-dupped list and track index
299
+ keep.append(control_object)
300
+ else:
301
+ break
302
+ except:
303
+ log.error("failed to dedupe command queue", exc_info=1)
304
+ return keep
wasatch/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """This package provides control of Wasatch Photonics spectrometers"""
2
+
3
+ __version__ = "2.3.2" # used by flit and other pypi things
4
+ version = __version__ # legacy alias
wasatch/applog.py ADDED
@@ -0,0 +1,177 @@
1
+ ##
2
+ # Custom logging setup and helper functions.
3
+ #
4
+ # The general approach is that the control portion of the application
5
+ # instantiates a MainLogger object. This will create a separate process
6
+ # that looks for log events on a queue. Each process of the application then
7
+ # registers a queue handler, and writes its log events. The MainLogger loop
8
+ # will collect and write these log events to file and any other defined
9
+ # logging location.
10
+ #
11
+ # @see http://plumberjack.blogspot.com/2010/09/using-logging-with-multiprocessing.html
12
+ #
13
+ # The overall level of support for this file is basically "someone borrowed and
14
+ # modified it from an open-source example on the internet, and it basically works
15
+ # so I haven't putzed with it much" (except to try to understand when it breaks).
16
+ #
17
+ # @note on Windows, define PYTHONUTF8 environment variable to avoid error messages
18
+ # when log messages contain Unicode (default stdout/stderr streams are cp1252)
19
+
20
+ import os
21
+ import sys
22
+ import logging
23
+ import platform
24
+ from . import utils
25
+ from queue import Queue
26
+
27
+ # ##############################################################################
28
+ # #
29
+ # Semi-static, module-level functions #
30
+ # #
31
+ # ##############################################################################
32
+
33
+ explicit_path = None
34
+
35
+ def set_location(path):
36
+ global explicit_path
37
+ explicit_path = path
38
+
39
+ def get_location():
40
+ global explicit_path
41
+ if explicit_path is not None:
42
+ return explicit_path
43
+
44
+ module_name = __name__.replace(".", "_") # "wasatch.applog" -> "wasatch_applog"
45
+ filename = "%s.txt" % module_name # "wasatch_applog.txt"
46
+
47
+ if "Linux" in platform.platform():
48
+ return filename
49
+
50
+ if "Darwin" in platform.system():
51
+ return filename
52
+
53
+ if "macOS" in platform.platform():
54
+ return filename
55
+
56
+ pathname = os.path.join("C:\\ProgramData", filename)
57
+ return pathname
58
+
59
+ ##
60
+ # Called at the beginning of every subprocess, to tie into the existing
61
+ # root logger (owned and created by the MainProcess).
62
+ #
63
+ # Adds a queue handler object to the root logger to be processed in the
64
+ # main listener.
65
+ #
66
+ # Only on Windows though. Apparently Linux will pass the root logger
67
+ # amongst processes as expected, so if you add another queue handler you
68
+ # will get double log prints.
69
+ #
70
+ # Mimic the capturelog style of just slurping the entire log file contents.
71
+ #
72
+ # MZ: if we're just interested in the 'tail' of the log, this will be horribly
73
+ # inefficient for memory as the log file grows!
74
+ def get_text_from_log():
75
+ log_text = ""
76
+ with open(get_location()) as log_file:
77
+ # MZ: why not just 'return log_file.read()'?
78
+ for line_read in log_file:
79
+ log_text += line_read
80
+ return log_text
81
+
82
+ def log_file_created():
83
+ return os.path.exists(get_location())
84
+
85
+ ## Remove the specified log file and return True if succesful.
86
+ def delete_log_file_if_exists():
87
+ pathname = get_location()
88
+ if os.path.exists(pathname):
89
+ os.remove(pathname)
90
+ return not os.path.exists(pathname)
91
+
92
+ def explicit_log_close():
93
+ root_log = logging.getLogger()
94
+ root_log.debug("applog.explicit_log_close: closing and removing all handlers")
95
+ handlers = root_log.handlers[:]
96
+ for handler in handlers:
97
+ handler.close()
98
+ root_log.removeHandler(handler)
99
+
100
+ # ##############################################################################
101
+ # #
102
+ # MainLogger #
103
+ # #
104
+ # ##############################################################################
105
+
106
+ class MainLogger:
107
+ FORMAT = u'%(asctime)s [0x%(thread)08x] %(name)s %(levelname)-8s %(message)s'
108
+
109
+ def __init__(self,
110
+ log_level=logging.DEBUG,
111
+ enable_stdout=True,
112
+ logfile=None,
113
+ timeout_sec=5,
114
+ append_arg="True"):
115
+ self.log_queue = Queue()
116
+ self.log_level = log_level
117
+ self.enable_stdout = enable_stdout
118
+ self.logfile = logfile
119
+ self.timeout_sec = timeout_sec
120
+
121
+ if self.logfile is not None:
122
+ set_location(self.logfile)
123
+
124
+ # append file size limits are enforced upon program restart
125
+ if append_arg.lower() == "true":
126
+ # limit to 300mb if --log-append is explicitly set to "True"
127
+ append = True
128
+ if append_arg.lower() == "false":
129
+ # when append is a falsy value, the log file is always reset on reboot
130
+ append = False
131
+ if append_arg.lower() == "limit":
132
+ # the default --log-append keeps up to 2mb between sessions
133
+ append = 2*1024*1024
134
+
135
+ root_log = logging.getLogger()
136
+ self.log_configurer(self.logfile, append)
137
+ root_log.setLevel(self.log_level)
138
+ root_log.warning("Top level log configuration (%d handlers, get_location %s)", len(root_log.handlers), get_location())
139
+
140
+ ## Setup file handler and command window stream handlers. Every log
141
+ # message received on the queue handler will use these log configurers.
142
+ def log_configurer(self, logfile=None, append=False):
143
+ if logfile is not None:
144
+ pathname = logfile
145
+ elif self.logfile is not None:
146
+ pathname = self.logfile
147
+ else:
148
+ pathname = get_location()
149
+
150
+ try:
151
+ if isinstance(append, int):
152
+ utils.resize_file(path=pathname, nbytes=append)
153
+ except (IOError, FileNotFoundError):
154
+ print("Unable to truncate log file.")
155
+
156
+ root_logger = logging.getLogger()
157
+ fh = logging.FileHandler(pathname, mode='a' if append else 'w', encoding='utf-8')
158
+ formatter = logging.Formatter(self.FORMAT)
159
+ fh.setFormatter(formatter)
160
+ root_logger.addHandler(fh)
161
+
162
+ if self.enable_stdout and sys.stdout is not None:
163
+ sys.stdout.reconfigure(encoding='utf-8')
164
+ stream_handler = logging.StreamHandler(sys.stdout)
165
+ stream_handler.setFormatter(formatter)
166
+ root_logger.addHandler(stream_handler)
167
+
168
+ self.root = root_logger
169
+
170
+ ## Wrapper to add a None poison pill to the listener process queue to
171
+ # ensure it exits.
172
+ def close(self):
173
+ self.log_queue.put_nowait(None)
174
+ try:
175
+ self.listener.join()
176
+ except:
177
+ pass