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/StatusMessage.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class StatusMessage:
|
|
2
|
+
"""
|
|
3
|
+
An outbound analog to ControlObject. Not re-using the same class so that
|
|
4
|
+
it can be extended in different ways, perhaps subclassed etc.
|
|
5
|
+
|
|
6
|
+
There is no enumeration of supported status messages, but ENLIGHTEN's
|
|
7
|
+
Controller.process_status_message() would be a good source.
|
|
8
|
+
"""
|
|
9
|
+
def __init__(self, setting, value):
|
|
10
|
+
self.setting = setting # probably a string
|
|
11
|
+
self.value = value # don't presume type
|
|
12
|
+
|
|
13
|
+
def __repr__(self):
|
|
14
|
+
return f"StatusMessage<setting {self.setting}, value {self.value}>"
|
wasatch/TCPDevice.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import struct
|
|
3
|
+
import socket
|
|
4
|
+
|
|
5
|
+
from .SpectrometerResponse import SpectrometerResponse, ErrorLevel
|
|
6
|
+
from .SpectrometerSettings import SpectrometerSettings
|
|
7
|
+
from .InterfaceDevice import InterfaceDevice
|
|
8
|
+
from .DeviceID import DeviceID
|
|
9
|
+
from .Reading import Reading
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
class TCPDevice(InterfaceDevice):
|
|
14
|
+
|
|
15
|
+
SUCCESS = 0x00 # byte response from setter and command (ACQUIRE, DISCONNECT) opcodes
|
|
16
|
+
|
|
17
|
+
############################################################################
|
|
18
|
+
# lifecycle
|
|
19
|
+
############################################################################
|
|
20
|
+
|
|
21
|
+
def __init__(self, device_id, message_queue=None, alert_queue=None):
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
self.device_id = device_id
|
|
25
|
+
if device_id.type != "TCP":
|
|
26
|
+
raise RuntimeError("TCPDevice can only be constructed with TCP DeviceID")
|
|
27
|
+
|
|
28
|
+
self.addr = device_id.address
|
|
29
|
+
self.port = device_id.port
|
|
30
|
+
|
|
31
|
+
self.process_f = self.init_process_funcs()
|
|
32
|
+
|
|
33
|
+
self.reset()
|
|
34
|
+
|
|
35
|
+
def reset(self):
|
|
36
|
+
self.sock = None
|
|
37
|
+
self.mode = "ascii"
|
|
38
|
+
|
|
39
|
+
def connect(self):
|
|
40
|
+
"""
|
|
41
|
+
This method is called by WrapperWorker.run. At the end of the method,
|
|
42
|
+
an InterfaceDevice should have a non-null .settings with a populated
|
|
43
|
+
SpectrometerSettings.
|
|
44
|
+
"""
|
|
45
|
+
log.debug(f"connect: trying to connect to {self.device_id}")
|
|
46
|
+
try:
|
|
47
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
48
|
+
self.sock.settimeout(3)
|
|
49
|
+
self.sock.connect((self.addr, self.port))
|
|
50
|
+
except:
|
|
51
|
+
log.error(f"connect: failed to connect to {self.addr} port {self.port}", exc_info=1)
|
|
52
|
+
return SpectrometerResponse(error_lvl=ErrorLevel.high, error_msg=f"Failed to connect to {self.device_id}")
|
|
53
|
+
|
|
54
|
+
log.debug(f"connect: connected to {self.device_id}")
|
|
55
|
+
|
|
56
|
+
self.set_binary_mode()
|
|
57
|
+
|
|
58
|
+
self.protocol_version = self.get_protocol_version()
|
|
59
|
+
log.debug(f"Protocol version: {self.protocol_version}")
|
|
60
|
+
|
|
61
|
+
# use this to hold settings that already have a standard place
|
|
62
|
+
self.settings = SpectrometerSettings()
|
|
63
|
+
|
|
64
|
+
self.settings.state.microcontroller_firmware_version = self.get_firmware_version()
|
|
65
|
+
|
|
66
|
+
# this is not currently a SpectrometerSettings attribute
|
|
67
|
+
self.product_id = self.get_product_id()
|
|
68
|
+
log.debug(f"Product ID: {self.product_id}")
|
|
69
|
+
|
|
70
|
+
self.settings.eeprom.model = self.get_model_name()
|
|
71
|
+
log.debug(f"Model Name: {self.settings.eeprom.model}")
|
|
72
|
+
|
|
73
|
+
self.settings.eeprom.serial_number = self.get_serial_number()
|
|
74
|
+
log.debug(f"Serial Number: {self.settings.eeprom.serial_number}")
|
|
75
|
+
|
|
76
|
+
self.settings.eeprom.active_pixels_horizontal = self.get_line_length()
|
|
77
|
+
self.settings.eeprom.active_pixels_vertical = self.get_line_count()
|
|
78
|
+
log.debug(f"Detector Size: {self.settings.pixels()} (H) x {self.settings.eeprom.active_pixels_vertical} (V)")
|
|
79
|
+
|
|
80
|
+
self.settings.eeprom.excitation_nm_float = self.get_excitation()
|
|
81
|
+
log.debug(f"Excitation: {self.settings.excitation()}")
|
|
82
|
+
|
|
83
|
+
# read wavecal
|
|
84
|
+
self.settings.eeprom.wavecal_coeffs = []
|
|
85
|
+
for i in range(5):
|
|
86
|
+
self.settings.eeprom.wavecal_coeffs.append(self.get_wavecal_coeff(i))
|
|
87
|
+
self.settings.update_wavecal()
|
|
88
|
+
|
|
89
|
+
return SpectrometerResponse(True)
|
|
90
|
+
|
|
91
|
+
def disconnect(self):
|
|
92
|
+
if self.sock:
|
|
93
|
+
self.send_cmd(0xff, 0xaa14, label="DISCONNECT")
|
|
94
|
+
self.reset()
|
|
95
|
+
|
|
96
|
+
############################################################################
|
|
97
|
+
# ascii protocol
|
|
98
|
+
############################################################################
|
|
99
|
+
|
|
100
|
+
def set_binary_mode(self):
|
|
101
|
+
log.debug("awaiting ready")
|
|
102
|
+
data = self.read_data(3)
|
|
103
|
+
log.debug("<< " + "".join([chr(c) for c in data]))
|
|
104
|
+
for a, b in zip(data, bytes("OK\n", encoding='ascii')):
|
|
105
|
+
if a != b:
|
|
106
|
+
raise(RuntimeError("failed handshaking"))
|
|
107
|
+
|
|
108
|
+
log.debug("setting binary mode")
|
|
109
|
+
self.send_string("BIN\n")
|
|
110
|
+
if 0 == self.read_data(1)[0]:
|
|
111
|
+
self.mode = "binary"
|
|
112
|
+
else:
|
|
113
|
+
raise(RuntimeError("failed to set BIN mode"))
|
|
114
|
+
|
|
115
|
+
############################################################################
|
|
116
|
+
# binary protocol
|
|
117
|
+
############################################################################
|
|
118
|
+
|
|
119
|
+
def get_protocol_version(self):
|
|
120
|
+
data = self.get_cmd(0xff, 0xaa13, length=4, label="GET_PROTOCOL_VERSION")
|
|
121
|
+
return ".".join([str(v) for v in data])
|
|
122
|
+
|
|
123
|
+
def get_firmware_version(self):
|
|
124
|
+
data = self.get_cmd(0xc0, length=4, label="GET_FIRMWARE_VERSION")
|
|
125
|
+
return ".".join([str(v) for v in data])
|
|
126
|
+
|
|
127
|
+
def get_product_id(self):
|
|
128
|
+
data = self.get_cmd(0xff, 0xaa01, length=4, label="GET_PRODUCT_ID")
|
|
129
|
+
vid = (data[0] << 8) | data[1]
|
|
130
|
+
pid = (data[2] << 8) | data[3]
|
|
131
|
+
return f"0x{vid:04x}:0x{pid:04x}"
|
|
132
|
+
|
|
133
|
+
def get_model_name(self):
|
|
134
|
+
return self.get_cmd(0xff, 0xaa0b, str_len=32, label="GET_MODEL_NAME")
|
|
135
|
+
|
|
136
|
+
def get_serial_number(self):
|
|
137
|
+
return self.get_cmd(0xff, 0xaa09, str_len=16, label="GET_SERIAL_NUMBER")
|
|
138
|
+
|
|
139
|
+
def get_line_length(self):
|
|
140
|
+
return self.get_cmd(0x03, lsb_len=2, label="GET_LINE_LENGTH")
|
|
141
|
+
|
|
142
|
+
def get_line_count(self):
|
|
143
|
+
return self.get_cmd(0xff, 0xaa10, lsb_len=2, label="GET_LINE_COUNT")
|
|
144
|
+
|
|
145
|
+
def get_excitation(self):
|
|
146
|
+
data = self.get_cmd(0xff, 0xaa12, length=4, label="GET_EXCITATION")
|
|
147
|
+
return self.to_float32(data, label="GET_EXCITATION")
|
|
148
|
+
|
|
149
|
+
def get_wavecal_coeff(self, exponent):
|
|
150
|
+
label = f"GET_WAVECAL_COEFF({exponent})"
|
|
151
|
+
data = self.get_cmd(0xff, 0xaa0d, exponent, length=4, label=label)
|
|
152
|
+
return self.to_float32(data, label=label)
|
|
153
|
+
|
|
154
|
+
def set_integration_time_ms(self, ms):
|
|
155
|
+
lsw = ms & 0xffff
|
|
156
|
+
msb = (ms >> 16) & 0xff
|
|
157
|
+
self.send_cmd(0xb2, lsw, msb, label="SET_INTEGRATION_TIME_MS")
|
|
158
|
+
|
|
159
|
+
def get_integration_time_ms(self):
|
|
160
|
+
return self.get_cmd(0xbf, lsb_len=3, label="GET_INTEGRATION_TIME_MS")
|
|
161
|
+
|
|
162
|
+
def set_detector_gain(self, db):
|
|
163
|
+
word = self.float_to_uint16(db, label="SET_DETECTOR_GAIN")
|
|
164
|
+
self.send_cmd(0xb7, word, label="SET_DETECTOR_GAIN")
|
|
165
|
+
|
|
166
|
+
def get_detector_gain(self):
|
|
167
|
+
data = self.get_cmd(0xc5, length=2, label="GET_DETECTOR_GAIN")
|
|
168
|
+
msb = data[0]
|
|
169
|
+
lsb = data[1]
|
|
170
|
+
return msb + lsb / 256.0
|
|
171
|
+
|
|
172
|
+
def set_start_line(self, line):
|
|
173
|
+
self.send_cmd(0xff, 0x21, line, label="SET_START_LINE")
|
|
174
|
+
|
|
175
|
+
def get_start_line(self):
|
|
176
|
+
return self.get_cmd(0xff, 0x22, lsb_len=2, label="GET_START_LINE")
|
|
177
|
+
|
|
178
|
+
def set_stop_line(self, line):
|
|
179
|
+
self.send_cmd(0xff, 0x23, line, label="SET_STOP_LINE")
|
|
180
|
+
|
|
181
|
+
def get_stop_line(self):
|
|
182
|
+
return self.get_cmd(0xff, 0x24, lsb_len=2, label="GET_STOP_LINE")
|
|
183
|
+
|
|
184
|
+
def get_spectrum(self):
|
|
185
|
+
self.send_cmd(0xad, label="ACQUIRE")
|
|
186
|
+
data = self.read_data(self.settings.pixels() * 2, label="GET_SPECTRUM", quiet=True)
|
|
187
|
+
|
|
188
|
+
spectrum = []
|
|
189
|
+
for i in range(self.settings.pixels()):
|
|
190
|
+
intensity = data[i*2] | (data[i*2 + 1] << 8) # little-endian
|
|
191
|
+
spectrum.append(intensity)
|
|
192
|
+
|
|
193
|
+
return spectrum
|
|
194
|
+
|
|
195
|
+
# not implemented:
|
|
196
|
+
#
|
|
197
|
+
# - SET_AREA_SCAN_MODE
|
|
198
|
+
# - SET_IMX_SENSOR_MODE
|
|
199
|
+
# - GET_IMX_SENSOR_MODE
|
|
200
|
+
# - SET_START_PIXEL
|
|
201
|
+
# - GET_START_PIXEL
|
|
202
|
+
# - SET_STOP_PIXEL
|
|
203
|
+
# - GET_STOP_PIXEL
|
|
204
|
+
# - SET_SERIAL_NUMBER
|
|
205
|
+
# - SET_MODEL_NAME
|
|
206
|
+
# - SET_WAVELENGTH_COEFF
|
|
207
|
+
# - SET_BAD_PIXEL
|
|
208
|
+
# - GET_BAD_PIXEL
|
|
209
|
+
|
|
210
|
+
############################################################################
|
|
211
|
+
# operations
|
|
212
|
+
############################################################################
|
|
213
|
+
|
|
214
|
+
def acquire_data(self):
|
|
215
|
+
reading = Reading(self.device_id)
|
|
216
|
+
|
|
217
|
+
spectrum = self.get_spectrum()
|
|
218
|
+
|
|
219
|
+
reading.spectrum = spectrum
|
|
220
|
+
|
|
221
|
+
return SpectrometerResponse(data=reading)
|
|
222
|
+
|
|
223
|
+
############################################################################
|
|
224
|
+
# utility
|
|
225
|
+
############################################################################
|
|
226
|
+
|
|
227
|
+
def to_hex(self, data):
|
|
228
|
+
return "[ " + " ".join([f"0x{v:02x}" for v in data]) + " ]"
|
|
229
|
+
|
|
230
|
+
def get_cmd(self, bRequest, wValue=0, wIndex=0, lsb_len=None, msb_len=None, str_len=None, length=None, label=None):
|
|
231
|
+
"""
|
|
232
|
+
bRequest is required, and one length parameter must be provided. The
|
|
233
|
+
length parameter used determines the return data format.
|
|
234
|
+
"""
|
|
235
|
+
# send outgoing request
|
|
236
|
+
packet = MessagePacket(bRequest, wValue, wIndex)
|
|
237
|
+
serialized = packet.serialize()
|
|
238
|
+
log.debug(f">> {packet} ({label})")
|
|
239
|
+
log.debug(f">> {self.to_hex(serialized)} ({label})")
|
|
240
|
+
self.sock.sendall(serialized)
|
|
241
|
+
|
|
242
|
+
# determine response length
|
|
243
|
+
if lsb_len: expected_len = lsb_len
|
|
244
|
+
elif msb_len: expected_len = msb_len
|
|
245
|
+
elif str_len: expected_len = str_len
|
|
246
|
+
elif length: expected_len = length
|
|
247
|
+
else:
|
|
248
|
+
raise(RuntimeError(f"get_cmd called without length parameter [{label}]"))
|
|
249
|
+
|
|
250
|
+
data = self.read_data(expected_len, label=label)
|
|
251
|
+
|
|
252
|
+
# demarshall response
|
|
253
|
+
value = 0
|
|
254
|
+
if lsb_len:
|
|
255
|
+
for i, byte in enumerate(data):
|
|
256
|
+
value |= (byte << (i * 8)) # little-endian
|
|
257
|
+
elif msb_len:
|
|
258
|
+
for byte in data:
|
|
259
|
+
value = (value << 8) | byte # big-endian
|
|
260
|
+
elif str_len:
|
|
261
|
+
value = ""
|
|
262
|
+
for c in data:
|
|
263
|
+
if c == 0: # null-terminated
|
|
264
|
+
break
|
|
265
|
+
value += chr(c)
|
|
266
|
+
else:
|
|
267
|
+
value = data # raw data
|
|
268
|
+
|
|
269
|
+
# log.debug(f"get_cmd: received {value} ({label})")
|
|
270
|
+
return value
|
|
271
|
+
|
|
272
|
+
def send_cmd(self, bRequest, wValue=0, wIndex=0, payload=None, readback_len=None, label=None):
|
|
273
|
+
packet = MessagePacket(bRequest, wValue, wIndex, payload)
|
|
274
|
+
data = packet.serialize()
|
|
275
|
+
log.debug(f">> {packet} ({label})")
|
|
276
|
+
log.debug(f">> {self.to_hex(data)} ({label})")
|
|
277
|
+
self.sock.sendall(data)
|
|
278
|
+
|
|
279
|
+
if readback_len is None:
|
|
280
|
+
# by default, read back one byte
|
|
281
|
+
response = self.read_data(1)[0]
|
|
282
|
+
if response != self.SUCCESS:
|
|
283
|
+
raise(RuntimeError(f"send_cmd received {response} from {packet} [{label}]"))
|
|
284
|
+
log.debug(f"<< SUCCESS ({label})")
|
|
285
|
+
else:
|
|
286
|
+
response = self.read_data(readback_len, label=label)
|
|
287
|
+
log.debug(f"<< {self.to_hex(response)} ({label})")
|
|
288
|
+
return response
|
|
289
|
+
|
|
290
|
+
def read_data(self, length, label=None, quiet=False):
|
|
291
|
+
if length == 0:
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
response = []
|
|
295
|
+
while True:
|
|
296
|
+
data = self.sock.recv(1)
|
|
297
|
+
if len(data):
|
|
298
|
+
response.append(data[0])
|
|
299
|
+
if length == len(response):
|
|
300
|
+
retval = bytes(response)
|
|
301
|
+
if quiet:
|
|
302
|
+
log.debug(f"<< ({length} bytes) ({label})")
|
|
303
|
+
else:
|
|
304
|
+
log.debug(f"<< {self.to_hex(response)} ({length} bytes) ({label})")
|
|
305
|
+
return retval
|
|
306
|
+
|
|
307
|
+
def send_string(self, msg, length=None, label=None):
|
|
308
|
+
if length is None:
|
|
309
|
+
length = len(msg)
|
|
310
|
+
data = []
|
|
311
|
+
for i in range(length):
|
|
312
|
+
if i < len(msg):
|
|
313
|
+
data.append(ord(msg[i]))
|
|
314
|
+
else:
|
|
315
|
+
data.append(0)
|
|
316
|
+
log.debug(f">> {data} ({msg.strip()}) ({label})")
|
|
317
|
+
self.sock.sendall(bytes(data))
|
|
318
|
+
|
|
319
|
+
def to_float32(self, data, label=None):
|
|
320
|
+
value = struct.unpack('f', data)[0]
|
|
321
|
+
return value
|
|
322
|
+
|
|
323
|
+
def float_to_uint16(self, f, label=None):
|
|
324
|
+
msb = int(round(f, 5)) & 0xff
|
|
325
|
+
lsb = int((f - msb) * 256) & 0xff
|
|
326
|
+
value = (msb << 8) | lsb
|
|
327
|
+
return value
|
|
328
|
+
|
|
329
|
+
def init_process_funcs(self):
|
|
330
|
+
process_f = {}
|
|
331
|
+
|
|
332
|
+
process_f["connect"] = self.connect
|
|
333
|
+
process_f["disconnect"] = self.disconnect
|
|
334
|
+
process_f["close"] = self.disconnect
|
|
335
|
+
|
|
336
|
+
process_f["acquire_data"] = self.acquire_data
|
|
337
|
+
|
|
338
|
+
process_f["integration_time_ms"] = lambda x: self.set_integration_time_ms(x)
|
|
339
|
+
process_f["detector_gain"] = lambda x: self.set_gain(x)
|
|
340
|
+
process_f["laser_enable"] = lambda x: self.set_laser_enable(x)
|
|
341
|
+
process_f["vertical_binning"] = lambda x: self.set_vertical_roi(x)
|
|
342
|
+
|
|
343
|
+
return process_f
|
|
344
|
+
|
|
345
|
+
class MessagePacket:
|
|
346
|
+
"""
|
|
347
|
+
Essentially a 6-byte simplified USB Setup Packet, with bmRequestType removed
|
|
348
|
+
and wLength reduced to 1 byte.
|
|
349
|
+
|
|
350
|
+
This format was deliberately chosen to minimize porting complexity from
|
|
351
|
+
the existing ENG-0001 USB FID API.
|
|
352
|
+
|
|
353
|
+
Offset Length Datatype Field Description
|
|
354
|
+
------ ------ -------- -------- ---------------------
|
|
355
|
+
0 1 uint8 bRequest The command/opcode
|
|
356
|
+
1 2 uint16 wValue Parameter #1 (or 2nd-tier opcode if bRequest was 0xff)
|
|
357
|
+
3 2 uint16 wIndex Parameter #2
|
|
358
|
+
5 1 uint8 bLength Payload length
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
def __init__(self, bRequest=None, wValue=0, wIndex=0, payload=None, serialized=None):
|
|
362
|
+
if serialized:
|
|
363
|
+
self.deserialize(serialized)
|
|
364
|
+
else:
|
|
365
|
+
self.bRequest = bRequest
|
|
366
|
+
self.wValue = wValue
|
|
367
|
+
self.wIndex = wIndex
|
|
368
|
+
self.payload = None
|
|
369
|
+
|
|
370
|
+
if payload is None:
|
|
371
|
+
self.bLength = 0
|
|
372
|
+
elif isinstance(payload, bytes):
|
|
373
|
+
self.bLength = len(payload)
|
|
374
|
+
self.payload = payload
|
|
375
|
+
else:
|
|
376
|
+
raise(RuntimeError(f"payload must be 'bytes' (currently {type(payload)})"))
|
|
377
|
+
|
|
378
|
+
def __repr__(self):
|
|
379
|
+
return f"MessagePacket(bRequest 0x{self.bRequest:02x}, wValue 0x{self.wValue:04x}, wIndex 0x{self.wIndex:04x}, bLength {self.bLength})"
|
|
380
|
+
|
|
381
|
+
def serialize(self):
|
|
382
|
+
data = []
|
|
383
|
+
data.append(self.bRequest)
|
|
384
|
+
data.append((self.wValue >> 8) & 0xff)
|
|
385
|
+
data.append((self.wValue ) & 0xff)
|
|
386
|
+
data.append((self.wIndex >> 8) & 0xff)
|
|
387
|
+
data.append((self.wIndex ) & 0xff)
|
|
388
|
+
data.append(self.bLength)
|
|
389
|
+
|
|
390
|
+
if self.payload is not None:
|
|
391
|
+
for v in self.payload:
|
|
392
|
+
data.append(v)
|
|
393
|
+
|
|
394
|
+
return bytes(data)
|
|
395
|
+
|
|
396
|
+
def deserialize(self, serialized):
|
|
397
|
+
if len(serialized) < 6:
|
|
398
|
+
raise(RuntimeError(f"len({serialized}) < 6"))
|
|
399
|
+
|
|
400
|
+
self.bRequest = serialized[0]
|
|
401
|
+
self.wValue = (serialized[1] << 8) | serialized[2]
|
|
402
|
+
self.wIndex = (serialized[3] << 8) | serialized[4]
|
|
403
|
+
self.bLength = serialized[5]
|
|
404
|
+
|
|
405
|
+
payload_len = len(serialized) - 6
|
|
406
|
+
if payload_len != self.bLength:
|
|
407
|
+
raise(RuntimeError(f"payload_len {payload_len} != bLength {self.bLength}"))
|
|
408
|
+
|
|
409
|
+
self.payload = serialized[6:]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import random
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
class TakeOneRequest:
|
|
7
|
+
|
|
8
|
+
def __init__(self, take_dark = False,
|
|
9
|
+
enable_laser_before = False,
|
|
10
|
+
disable_laser_after = False,
|
|
11
|
+
laser_warmup_ms = 0,
|
|
12
|
+
scans_to_average = 1,
|
|
13
|
+
auto_raman_request = None,
|
|
14
|
+
template = None,
|
|
15
|
+
readings_target = None,
|
|
16
|
+
readings_current = None):
|
|
17
|
+
|
|
18
|
+
if template:
|
|
19
|
+
self.take_dark = template.take_dark
|
|
20
|
+
self.enable_laser_before = template.enable_laser_before
|
|
21
|
+
self.disable_laser_after = template.disable_laser_after
|
|
22
|
+
self.laser_warmup_ms = template.laser_warmup_ms
|
|
23
|
+
self.scans_to_average = template.scans_to_average
|
|
24
|
+
self.auto_raman_request = template.auto_raman_request
|
|
25
|
+
self.readings_target = template.readings_target
|
|
26
|
+
self.readings_current = template.readings_current
|
|
27
|
+
else:
|
|
28
|
+
self.take_dark = take_dark
|
|
29
|
+
self.enable_laser_before = enable_laser_before
|
|
30
|
+
self.disable_laser_after = disable_laser_after
|
|
31
|
+
self.laser_warmup_ms = laser_warmup_ms
|
|
32
|
+
self.scans_to_average = scans_to_average
|
|
33
|
+
self.auto_raman_request = auto_raman_request
|
|
34
|
+
self.readings_target = readings_target
|
|
35
|
+
self.readings_current = readings_current
|
|
36
|
+
|
|
37
|
+
self.request_id = f"{os.getpid()}-{threading.get_native_id()}-{time.time_ns()}-{random.randrange(65536)}"
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
return f"TakeOneRequest <id {self.request_id}, avg {self.scans_to_average}, take_dark {self.take_dark}, enable_laser_before {self.enable_laser_before}, disable_laser_after {self.disable_laser_after}, laser_warmup_ms {self.laser_warmup_ms}, readings {self.readings_current}/{self.readings_target}, auto_raman_request {self.auto_raman_request}>"
|
|
41
|
+
|
|
42
|
+
def __eq__(self, rhs):
|
|
43
|
+
return self.request_id == rhs.request_id
|
wasatch/WasatchBus.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .DeviceFinderUSB import DeviceFinderUSB
|
|
4
|
+
from .DeviceFinderTCP import DeviceFinderTCP
|
|
5
|
+
|
|
6
|
+
from usb import USBError
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# The different bus classes don't use inheritance and don't follow a common ABC
|
|
12
|
+
# or interface, but each should have an update() method, and each should have a
|
|
13
|
+
# 'device_ids' array.
|
|
14
|
+
#
|
|
15
|
+
# @param use_sim not used, left to avoid breaking old code
|
|
16
|
+
# @param monitor_dir not used, left to avoid breaking old code
|
|
17
|
+
class WasatchBus:
|
|
18
|
+
def __init__(self, use_sim=False, monitor_dir=None):
|
|
19
|
+
self.device_ids = []
|
|
20
|
+
|
|
21
|
+
self.usb_bus = USBBus()
|
|
22
|
+
self.tcp_bus = TCPBus()
|
|
23
|
+
|
|
24
|
+
self.update()
|
|
25
|
+
|
|
26
|
+
## called by enlighten.Controller.tick_bus_listener()
|
|
27
|
+
def update(self, poll=False):
|
|
28
|
+
device_ids = []
|
|
29
|
+
if self.usb_bus:
|
|
30
|
+
# MZ: if we call .extend here...when are devices ever purged from the stateful list?
|
|
31
|
+
# self.device_ids.extend(self.usb_bus.update(poll))
|
|
32
|
+
usb_device_ids = self.usb_bus.update(poll)
|
|
33
|
+
device_ids.extend(usb_device_ids)
|
|
34
|
+
|
|
35
|
+
if self.tcp_bus:
|
|
36
|
+
tcp_device_ids = self.tcp_bus.update(poll)
|
|
37
|
+
device_ids.extend(tcp_device_ids)
|
|
38
|
+
|
|
39
|
+
# purge any duplicates (can happen while USB device is enumerating)
|
|
40
|
+
self.device_ids = list(set(device_ids))
|
|
41
|
+
|
|
42
|
+
def is_empty(self):
|
|
43
|
+
return 0 == len(self.device_ids)
|
|
44
|
+
|
|
45
|
+
def dump(self):
|
|
46
|
+
""" called by Controller.update_connections """
|
|
47
|
+
log.debug("WasatchBus:")
|
|
48
|
+
for device_id in self.device_ids:
|
|
49
|
+
log.debug(f" {device_id}")
|
|
50
|
+
|
|
51
|
+
class USBBus:
|
|
52
|
+
finder = DeviceFinderUSB() # static attribute
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
self.backend_error_raised = False
|
|
56
|
+
# self.update()
|
|
57
|
+
|
|
58
|
+
def update(self, poll=False):
|
|
59
|
+
""" Return a list of DeviceIDs on the USB bus """
|
|
60
|
+
device_ids = []
|
|
61
|
+
try:
|
|
62
|
+
device_ids = self.finder.find_usb_devices(poll=True)
|
|
63
|
+
except USBError:
|
|
64
|
+
# MZ: this seems to happen when I run from Git Bash shell
|
|
65
|
+
# (resolved on MacOS with 'brew install libusb')
|
|
66
|
+
if not self.backend_error_raised:
|
|
67
|
+
log.warn("USBBus: No libusb backend", exc_info=1)
|
|
68
|
+
self.backend_error_raised = True
|
|
69
|
+
except Exception:
|
|
70
|
+
log.critical("USBBus: LIBUSB error", exc_info=1)
|
|
71
|
+
|
|
72
|
+
return device_ids
|
|
73
|
+
|
|
74
|
+
class TCPBus:
|
|
75
|
+
|
|
76
|
+
# these are static so callers can casually instantiate and release WasatchBus
|
|
77
|
+
# objects whenever they want, but these singletons will remain quietly
|
|
78
|
+
# persistent
|
|
79
|
+
finder = DeviceFinderTCP() # static
|
|
80
|
+
addresses = []
|
|
81
|
+
|
|
82
|
+
def __init__(self):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def update(self, poll=False):
|
|
86
|
+
device_ids = self.finder.find_tcp_devices()
|
|
87
|
+
return device_ids
|