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,3961 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import datetime
|
|
3
|
+
import logging
|
|
4
|
+
import random
|
|
5
|
+
import numpy as np
|
|
6
|
+
import copy
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from random import randint
|
|
12
|
+
from time import sleep
|
|
13
|
+
|
|
14
|
+
from . import utils
|
|
15
|
+
|
|
16
|
+
from .SpectrometerSettings import SpectrometerSettings
|
|
17
|
+
from .SpectrometerResponse import SpectrometerResponse, ErrorLevel
|
|
18
|
+
from .SpectrometerRequest import SpectrometerRequest
|
|
19
|
+
from .SpectrometerState import SpectrometerState
|
|
20
|
+
from .InterfaceDevice import InterfaceDevice
|
|
21
|
+
from .DetectorRegions import DetectorRegions
|
|
22
|
+
from .ControlObject import ControlObject
|
|
23
|
+
from .StatusMessage import StatusMessage
|
|
24
|
+
from .RealUSBDevice import RealUSBDevice
|
|
25
|
+
from .MockUSBDevice import MockUSBDevice
|
|
26
|
+
from .AreaScanImage import AreaScanImage
|
|
27
|
+
from .DetectorROI import DetectorROI
|
|
28
|
+
from .PollStatus import PollStatus
|
|
29
|
+
from .Reading import Reading
|
|
30
|
+
from .EEPROM import EEPROM
|
|
31
|
+
from .IMX385 import IMX385
|
|
32
|
+
from .ROI import ROI
|
|
33
|
+
|
|
34
|
+
log = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
MICROSEC_TO_SEC = 0.000001
|
|
37
|
+
UNINITIALIZED_TEMPERATURE_DEG_C = -999
|
|
38
|
+
|
|
39
|
+
class SpectrumAndRow:
|
|
40
|
+
def __init__(self, spectrum=None, row=-1):
|
|
41
|
+
self.spectrum = None
|
|
42
|
+
self.row = row
|
|
43
|
+
|
|
44
|
+
if spectrum is not None:
|
|
45
|
+
self.spectrum = spectrum.copy()
|
|
46
|
+
|
|
47
|
+
class FeatureIdentificationDevice(InterfaceDevice):
|
|
48
|
+
"""
|
|
49
|
+
This is the basic implementation of our FeatureIdentificationDevice (FID)
|
|
50
|
+
spectrometer USB API as defined in ENG-0001.
|
|
51
|
+
This class is roughly comparable to Wasatch.NET's Spectrometer.cs.
|
|
52
|
+
|
|
53
|
+
This class is normally not accessed directly, but through the higher-level
|
|
54
|
+
abstraction WasatchDevice.
|
|
55
|
+
|
|
56
|
+
@see ENG-0001
|
|
57
|
+
##########################################################################
|
|
58
|
+
This class adopts the external device interface structure
|
|
59
|
+
This invlovles receiving a request through the handle_request function
|
|
60
|
+
A request is processed based on the key in the request
|
|
61
|
+
The processing function passes the commands to the requested device
|
|
62
|
+
Once it recevies a response from the connected device it then passes that
|
|
63
|
+
back up the chain
|
|
64
|
+
@verbatim
|
|
65
|
+
Enlighten Request
|
|
66
|
+
|
|
|
67
|
+
handle_requests
|
|
68
|
+
|
|
|
69
|
+
------------
|
|
70
|
+
/ / | \ \
|
|
71
|
+
{ get_laser status, acquire, set_laser_watchdog, etc....}
|
|
72
|
+
\ \ | / /
|
|
73
|
+
------------
|
|
74
|
+
|
|
|
75
|
+
_send_code
|
|
76
|
+
@endverbatim
|
|
77
|
+
############################################################################
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
# ##########################################################################
|
|
81
|
+
# Lifecycle
|
|
82
|
+
# ##########################################################################
|
|
83
|
+
|
|
84
|
+
def __init__(self, device_id, message_queue=None, alert_queue=None):
|
|
85
|
+
"""
|
|
86
|
+
Instantiate a FeatureIdentificationDevice with from the given device_id.
|
|
87
|
+
@param device_id [in] device ID ("USB:0x24aa:0x1000:1:24")
|
|
88
|
+
@param message_queue [in] if provided, provides an outbound (from FID
|
|
89
|
+
to WrapperWorker) queue for writing StatusMessage objects upstream
|
|
90
|
+
@param alert_queue [in] if provided, accepts an inbound (from
|
|
91
|
+
WrapperWorker to FID) queue for receiving AlertMessage objects
|
|
92
|
+
"""
|
|
93
|
+
super().__init__()
|
|
94
|
+
self.device_id = device_id
|
|
95
|
+
self.message_queue = message_queue
|
|
96
|
+
self.alert_queue = alert_queue
|
|
97
|
+
|
|
98
|
+
self.device = None
|
|
99
|
+
if "MOCK" in str(device_id).upper():
|
|
100
|
+
self.device_type = MockUSBDevice(device_id.name,
|
|
101
|
+
device_id.directory,
|
|
102
|
+
device_id.overrides,
|
|
103
|
+
device_id.spectra_options)
|
|
104
|
+
else:
|
|
105
|
+
self.device_type = RealUSBDevice(device_id)
|
|
106
|
+
|
|
107
|
+
self.last_usb_timestamp = None
|
|
108
|
+
|
|
109
|
+
self.laser_temperature_invalid = False
|
|
110
|
+
self.ccd_temperature_invalid = False
|
|
111
|
+
|
|
112
|
+
self.settings = SpectrometerSettings(device_id)
|
|
113
|
+
self.eeprom_backup = None
|
|
114
|
+
|
|
115
|
+
self.alerts = set()
|
|
116
|
+
|
|
117
|
+
# ######################################################################
|
|
118
|
+
# these are "driver state" within FeatureIdentificationDevice, and don't
|
|
119
|
+
# really relate to the spectrometer hardware
|
|
120
|
+
# ######################################################################
|
|
121
|
+
|
|
122
|
+
self.detector_tec_setpoint_has_been_set = False
|
|
123
|
+
self.last_applied_laser_power = 0.0 # last power level APPLIED to laser, either by turning off (0) or on
|
|
124
|
+
self.next_applied_laser_power = None # power level to be applied NEXT time the laser is enabled
|
|
125
|
+
self.has_received_spectrum = False
|
|
126
|
+
self.extra_area_scan_data = []
|
|
127
|
+
|
|
128
|
+
self.raise_exceptions = False
|
|
129
|
+
self.inject_random_errors = False
|
|
130
|
+
self.random_error_perc = 0.001 # 0.1%
|
|
131
|
+
self.allow_default_gain_reset = True
|
|
132
|
+
|
|
133
|
+
self.connected = False
|
|
134
|
+
self.connecting = False
|
|
135
|
+
self.shutdown_requested = False
|
|
136
|
+
|
|
137
|
+
self.last_spectrum = None
|
|
138
|
+
self.prev_pixels = None
|
|
139
|
+
|
|
140
|
+
# in case of I2C collisions within the spectrometer, e.g. due to battery-LED status
|
|
141
|
+
self.retry_enabled = True
|
|
142
|
+
self.retry_ms = 5
|
|
143
|
+
self.retry_max = 3
|
|
144
|
+
|
|
145
|
+
self.process_f = self._init_process_funcs()
|
|
146
|
+
self.imx385 = IMX385()
|
|
147
|
+
|
|
148
|
+
def handle_requests(self, requests: list[SpectrometerRequest]):
|
|
149
|
+
"""
|
|
150
|
+
@todo consider making 'requests' an object, and dynamically checking to
|
|
151
|
+
see if it is a single SpectrometerRequest or a list[SpectrometerRequest];
|
|
152
|
+
if the former, only return a single SpectrometerResponse.
|
|
153
|
+
"""
|
|
154
|
+
responses = []
|
|
155
|
+
for request in requests:
|
|
156
|
+
try:
|
|
157
|
+
cmd = request.cmd
|
|
158
|
+
proc_func = self.process_f.get(cmd, None)
|
|
159
|
+
if proc_func is None:
|
|
160
|
+
responses.append(SpectrometerResponse(error_msg=f"unsupported cmd {request.cmd}", error_lvl=ErrorLevel.low))
|
|
161
|
+
elif request.args == [] and request.kwargs == {}:
|
|
162
|
+
responses.append(proc_func())
|
|
163
|
+
else:
|
|
164
|
+
responses.append(proc_func(*request.args, **request.kwargs))
|
|
165
|
+
except Exception as e:
|
|
166
|
+
log.error(f"error in handling request {request} of {e}", exc_info=1)
|
|
167
|
+
responses.append(SpectrometerResponse(error_msg="error processing cmd", error_lvl=ErrorLevel.medium))
|
|
168
|
+
return responses
|
|
169
|
+
|
|
170
|
+
def connect(self, retries=0):
|
|
171
|
+
"""
|
|
172
|
+
Connect to the device and initialize basic settings.
|
|
173
|
+
@warning this causes a problem in non-blocking mode (WasatchDeviceWrapper)
|
|
174
|
+
on MacOS
|
|
175
|
+
"""
|
|
176
|
+
self.connecting = True
|
|
177
|
+
|
|
178
|
+
# ######################################################################
|
|
179
|
+
# USB Connection
|
|
180
|
+
# ######################################################################
|
|
181
|
+
|
|
182
|
+
# Generate a fresh listing of USB devices with the requested VID and PID.
|
|
183
|
+
# Note that this is NOT how WasatchBus traverses the list. It actually
|
|
184
|
+
# calls usb.busses(), then iterates over bus.devices, but that's because
|
|
185
|
+
# it doesn't know what PIDs it might be looking for. We know, so just
|
|
186
|
+
# narrow down the search to those devices.
|
|
187
|
+
|
|
188
|
+
log.info(f"FID.connect: asked to connect to device_type {self.device_type}")
|
|
189
|
+
log.info(f"FID.connect: calling device_type.find, looking for VID 0x{self.device_id.vid:04x} and PID 0x{self.device_id.pid:04x}")
|
|
190
|
+
devices = self.device_type.find(find_all=True, idVendor=self.device_id.vid, idProduct=self.device_id.pid)
|
|
191
|
+
log.info(f"FID.connect: found devices {devices}")
|
|
192
|
+
dev_list = list(devices) # convert from array
|
|
193
|
+
|
|
194
|
+
device = None
|
|
195
|
+
log.info(f"searching for specified device in dev_list {dev_list}")
|
|
196
|
+
for dev in dev_list:
|
|
197
|
+
if dev.bus != self.device_id.bus:
|
|
198
|
+
log.debug("FID.connect: rejecting device (bus %d != requested %d)", dev.bus, self.device_id.bus)
|
|
199
|
+
elif dev.address != self.device_id.address:
|
|
200
|
+
log.debug("FID.connect: rejecting device (address %d != requested %d)", dev.address, self.device_id.address)
|
|
201
|
+
else:
|
|
202
|
+
device = dev
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
if device is None:
|
|
206
|
+
log.debug(f"FID.connect: unable to find DeviceID {self.device_id}")
|
|
207
|
+
self.connecting = False
|
|
208
|
+
return SpectrometerResponse(data=False, error_msg=f"unable to find DeviceID {self.device_id}")
|
|
209
|
+
else:
|
|
210
|
+
log.debug("FID.connect: matched DeviceID %s", str(self.device_id))
|
|
211
|
+
|
|
212
|
+
if os.name != "posix":
|
|
213
|
+
log.debug("on Windows, so NOT setting configuration and claiming interface")
|
|
214
|
+
elif "macOS" in platform.platform():
|
|
215
|
+
log.debug("on MacOS, so NOT setting configuration and claiming interface")
|
|
216
|
+
# elif "raspberrypi" in str(os.uname()):
|
|
217
|
+
# log.debug("on Raspberry Pi, so NOT setting configuration and claiming interface")
|
|
218
|
+
else:
|
|
219
|
+
log.debug("on posix, so setting configuration and claiming interface")
|
|
220
|
+
|
|
221
|
+
# in the following, return SpectrometerResponse objects rather than
|
|
222
|
+
# raising exceptions so the user will have a more-useful error
|
|
223
|
+
# message to report or use in troubleshooting (exception still gets
|
|
224
|
+
# logged)
|
|
225
|
+
try:
|
|
226
|
+
log.debug("setting configuration")
|
|
227
|
+
self.device_type.set_configuration(device)
|
|
228
|
+
except Exception as exc:
|
|
229
|
+
#####################################################################################################################
|
|
230
|
+
# This additional if statement is present for the Raspberry Pi. There is an issue with resource busy errors.
|
|
231
|
+
# Adding dev.reset() solves this. See https://stackoverflow.com/questions/29345325/raspberry-pyusb-gets-resource-busy
|
|
232
|
+
#####################################################################################################################
|
|
233
|
+
if "Resource busy" in str(exc) and retries <= 3:
|
|
234
|
+
log.warn("Hardware Failure in setConfiguration. Resource busy error. Attempting to reattach driver by reset.")
|
|
235
|
+
self.device_type.reset(dev)
|
|
236
|
+
sleep_ms = 10 ** retries # 10^3 ms = 1sec max delay
|
|
237
|
+
sleep(sleep_ms / 1000.0)
|
|
238
|
+
return self.connect(retries=retries+1)
|
|
239
|
+
|
|
240
|
+
self.connecting = False
|
|
241
|
+
msg = f"Hardware Failure in setConfiguration, giving up after {retries} retries"
|
|
242
|
+
log.critical(msg, exc_info=1)
|
|
243
|
+
return SpectrometerResponse(False, error_msg=msg)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
log.debug("claiming interface")
|
|
247
|
+
self.device_type.claim_interface(device, 0)
|
|
248
|
+
except:
|
|
249
|
+
self.connecting = False
|
|
250
|
+
msg = "Hardware Failure in claimInterface"
|
|
251
|
+
log.critical(msg, exc_info=1)
|
|
252
|
+
return SpectrometerResponse(False, error_msg=msg)
|
|
253
|
+
|
|
254
|
+
self.device = device
|
|
255
|
+
|
|
256
|
+
return self._post_connect()
|
|
257
|
+
|
|
258
|
+
def _post_connect(self):
|
|
259
|
+
"""
|
|
260
|
+
Perform additional setup after instantiating FID device.
|
|
261
|
+
Split-out from physical / bus connect() to simplify MockSpectrometer.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
# grab firmware versions early (and capture in debug log)
|
|
265
|
+
self.get_microcontroller_firmware_version()
|
|
266
|
+
log.debug(f"Microcontroller firmware version {self.settings.microcontroller_firmware_version}")
|
|
267
|
+
|
|
268
|
+
self.get_fpga_firmware_version()
|
|
269
|
+
log.debug(f"FPGA firmware version {self.settings.fpga_firmware_version}")
|
|
270
|
+
|
|
271
|
+
# self.get_microcontroller_serial_number()
|
|
272
|
+
# log.debug(f"Microcontroller serial number {self.settings.microcontroller_serial_number}")
|
|
273
|
+
|
|
274
|
+
# issue: BL652 may not be fully booted if this was a hotplug. We could
|
|
275
|
+
# of course re-poll if None, but the initial SpectrometerSettings
|
|
276
|
+
# will already have been used to populate the EEPROMEditor. What
|
|
277
|
+
# we really need to do is send a "change" in this setting upstream
|
|
278
|
+
# via the MessageQueue, and have a listener in ENLIGHTEN/caller
|
|
279
|
+
# for such updates. The way we mainly do this now is by adding
|
|
280
|
+
# fields to Reading (like temperature, battery, laser interlock
|
|
281
|
+
# etc), since most dynamic (uncommanded) changes in spectrometer
|
|
282
|
+
# state are usually measurement-related. It would be a little
|
|
283
|
+
# weird to add a "dynamic firmware version" to Reading, implying
|
|
284
|
+
# that firmware versions might suddenly change mid-runtime...
|
|
285
|
+
# self.get_ble_firmware_version()
|
|
286
|
+
# log.debug(f"BLE firmware version {self.settings.ble_firmware_version}")
|
|
287
|
+
|
|
288
|
+
# ######################################################################
|
|
289
|
+
# model-specific settings
|
|
290
|
+
# ######################################################################
|
|
291
|
+
|
|
292
|
+
log.debug("model-specific settings")
|
|
293
|
+
|
|
294
|
+
# default high-gain mode for InGaAs
|
|
295
|
+
if self.settings.is_ingaas():
|
|
296
|
+
self.set_high_gain_mode_enable(True)
|
|
297
|
+
|
|
298
|
+
# ######################################################################
|
|
299
|
+
# EEPROM
|
|
300
|
+
# ######################################################################
|
|
301
|
+
|
|
302
|
+
log.debug("reading EEPROM")
|
|
303
|
+
|
|
304
|
+
result = self._read_eeprom()
|
|
305
|
+
if not result.data:
|
|
306
|
+
log.error(f"failed to read EEPROM, got error message of {result.error_msg}")
|
|
307
|
+
self.connecting = False
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
# ######################################################################
|
|
311
|
+
# Automatically disable laser at connection
|
|
312
|
+
# ######################################################################
|
|
313
|
+
|
|
314
|
+
# if self.settings.eeprom.has_laser:
|
|
315
|
+
# self.set_laser_enable(False)
|
|
316
|
+
|
|
317
|
+
# ######################################################################
|
|
318
|
+
# TEC Setpoint
|
|
319
|
+
# ######################################################################
|
|
320
|
+
|
|
321
|
+
if self.settings.is_xs():
|
|
322
|
+
if self.settings.eeprom.has_laser:
|
|
323
|
+
if self.settings.eeprom.sig_laser_tec:
|
|
324
|
+
|
|
325
|
+
# check the new location
|
|
326
|
+
setpoint = self.settings.eeprom.startup_laser_tec_setpoint # prefer new
|
|
327
|
+
if not setpoint:
|
|
328
|
+
setpoint = self.settings.eeprom.startup_temp_degC # default to old
|
|
329
|
+
|
|
330
|
+
# sanity-check for reasonable setpoint range (raw 12-bit)
|
|
331
|
+
if 700 <= setpoint <= 900:
|
|
332
|
+
log.debug("initializing XS laser TEC setpoint")
|
|
333
|
+
|
|
334
|
+
# kludge: for now, use the detector TEC startup setpoint for laser
|
|
335
|
+
self.settings.state.laser_tec_setpoint = setpoint
|
|
336
|
+
self.set_laser_temperature_setpoint_raw(self.settings.state.laser_tec_setpoint)
|
|
337
|
+
|
|
338
|
+
# this should be the default in firmware, but set anyway
|
|
339
|
+
log.debug("initializing XS laser TEC mode -> AUTO")
|
|
340
|
+
self.set_laser_tec_mode("AUTO")
|
|
341
|
+
else:
|
|
342
|
+
# don't set anything if default setpoint looks way off
|
|
343
|
+
log.error(f"laser TEC setpoint looks invalid: {self.settings.eeprom.startup_temp_degC}")
|
|
344
|
+
|
|
345
|
+
# load this for software-based Auto-Raman timing
|
|
346
|
+
log.debug("initializing laser warning delay")
|
|
347
|
+
self.get_laser_warning_delay_sec()
|
|
348
|
+
|
|
349
|
+
# ######################################################################
|
|
350
|
+
# Detector TEC
|
|
351
|
+
# ######################################################################
|
|
352
|
+
|
|
353
|
+
if not self.settings.is_xs():
|
|
354
|
+
|
|
355
|
+
# X/XM models don't require runtime configuration of the laser TEC
|
|
356
|
+
# (it's set via pots on 110280 (SML) or 110613 (MML)), but do need
|
|
357
|
+
# to initialize the detector TEC for R and C units.
|
|
358
|
+
if self.settings.eeprom.has_cooling:
|
|
359
|
+
|
|
360
|
+
degC = None
|
|
361
|
+
eeprom = self.settings.eeprom
|
|
362
|
+
|
|
363
|
+
if eeprom.min_temp_degC <= eeprom.startup_temp_degC <= eeprom.max_temp_degC:
|
|
364
|
+
degC = eeprom.startup_temp_degC
|
|
365
|
+
elif re.match(r"7031|10141|9214", eeprom.detector):
|
|
366
|
+
degC = -15
|
|
367
|
+
elif re.match(r"16011|11511|11850|13971", eeprom.detector):
|
|
368
|
+
degC = 10
|
|
369
|
+
|
|
370
|
+
if degC is not None:
|
|
371
|
+
# TEC doesn't do anything unless you give it a temperature first
|
|
372
|
+
log.debug(f"setting detector TEC setpoint to {degC} deg C")
|
|
373
|
+
self.detector_tec_setpoint_degC = degC
|
|
374
|
+
self.set_detector_tec_setpoint_degC(self.detector_tec_setpoint_degC)
|
|
375
|
+
|
|
376
|
+
log.debug("enabling detector TEC")
|
|
377
|
+
self.detector_tec_setpoint_has_been_set = True
|
|
378
|
+
self.set_tec_enable(True)
|
|
379
|
+
|
|
380
|
+
# ######################################################################
|
|
381
|
+
# FPGA
|
|
382
|
+
# ######################################################################
|
|
383
|
+
|
|
384
|
+
log.debug("reading FPGA compilation options")
|
|
385
|
+
self._read_fpga_compilation_options()
|
|
386
|
+
|
|
387
|
+
log.debug("configuring FPGA")
|
|
388
|
+
|
|
389
|
+
# automatically push EEPROM values to the FPGA (on modern EEPROMs)
|
|
390
|
+
# (this will work on Series-XS as well, even if we subsequently track its gain
|
|
391
|
+
# somewhat differently as state.gain_db)
|
|
392
|
+
if self.settings.eeprom.format >= 4:
|
|
393
|
+
log.debug("sending gain/offset to FPGA")
|
|
394
|
+
self.set_detector_gain (self.settings.eeprom.detector_gain)
|
|
395
|
+
self.set_detector_offset (self.settings.eeprom.detector_offset)
|
|
396
|
+
self.set_detector_gain_odd (self.settings.eeprom.detector_gain_odd)
|
|
397
|
+
self.set_detector_offset_odd(self.settings.eeprom.detector_offset_odd)
|
|
398
|
+
|
|
399
|
+
# initialize state.gain_db from EEPROM startup value
|
|
400
|
+
self.settings.state.gain_db = self.settings.eeprom.detector_gain
|
|
401
|
+
|
|
402
|
+
self.update_vertical_roi()
|
|
403
|
+
|
|
404
|
+
# ######################################################################
|
|
405
|
+
# post-connection defaults
|
|
406
|
+
# ######################################################################
|
|
407
|
+
|
|
408
|
+
# default to internal triggering
|
|
409
|
+
self.set_trigger_source(SpectrometerState.TRIGGER_SOURCE_INTERNAL)
|
|
410
|
+
|
|
411
|
+
if self.settings.is_xs():
|
|
412
|
+
|
|
413
|
+
if self.settings.eeprom.has_laser:
|
|
414
|
+
has_sml = self.settings.eeprom.has_sml()
|
|
415
|
+
sig_laser_tec = self.settings.eeprom.sig_laser_tec
|
|
416
|
+
log.debug(f"is_xs {self.settings.is_xs()}, has_laser {self.settings.eeprom.has_laser}, has_sml {has_sml}, sig_laser_tec {sig_laser_tec}")
|
|
417
|
+
|
|
418
|
+
# laser watchdog
|
|
419
|
+
sec = self.settings.eeprom.laser_watchdog_sec
|
|
420
|
+
if sec <= 0 and has_sml and not sig_laser_tec:
|
|
421
|
+
sec = EEPROM.DEFAULT_LASER_WATCHDOG_SEC
|
|
422
|
+
log.debug(f"declining to disable laser watchdog for SML w/o TEC, defaulting to {sec}sec")
|
|
423
|
+
log.debug(f"post-connect: initializing laser watchdog to {sec}sec")
|
|
424
|
+
self.set_laser_watchdog_sec(sec)
|
|
425
|
+
|
|
426
|
+
self.set_integration_time_ms(self.settings.eeprom.startup_integration_time_ms)
|
|
427
|
+
|
|
428
|
+
# # for now, enable Gen 1.5 accessory connector by default
|
|
429
|
+
# if self.settings.is_gen15():
|
|
430
|
+
# log.debug("enabling Gen 1.5 accessory connector")
|
|
431
|
+
# self.set_accessory_enable(True)
|
|
432
|
+
|
|
433
|
+
# ######################################################################
|
|
434
|
+
# Area Scan
|
|
435
|
+
# ######################################################################
|
|
436
|
+
|
|
437
|
+
if self.settings.is_xs():
|
|
438
|
+
self.settings.eeprom.actual_pixels_vertical = 1080 # MZ: probably higher
|
|
439
|
+
else:
|
|
440
|
+
self.settings.eeprom.actual_pixels_vertical = 70
|
|
441
|
+
|
|
442
|
+
self.reset_area_scan_frame()
|
|
443
|
+
|
|
444
|
+
# ######################################################################
|
|
445
|
+
# Done
|
|
446
|
+
# ######################################################################
|
|
447
|
+
|
|
448
|
+
log.debug("connection successful")
|
|
449
|
+
self.connected = True
|
|
450
|
+
self.connecting = False
|
|
451
|
+
|
|
452
|
+
self.settings.state.dump("FID.post_connect")
|
|
453
|
+
|
|
454
|
+
return SpectrometerResponse(self.connected)
|
|
455
|
+
|
|
456
|
+
def reset_area_scan_frame(self):
|
|
457
|
+
self.area_scan_frame = [ [0] * self.settings.pixels() for line in range(self.settings.eeprom.actual_pixels_vertical) ]
|
|
458
|
+
|
|
459
|
+
def disconnect(self):
|
|
460
|
+
if self.last_applied_laser_power:
|
|
461
|
+
log.debug("fid.disconnect: disabling laser")
|
|
462
|
+
self._set_laser_enable_immediate(False)
|
|
463
|
+
|
|
464
|
+
self.connected = False
|
|
465
|
+
|
|
466
|
+
log.critical("fid.disconnect: releasing interface")
|
|
467
|
+
try:
|
|
468
|
+
#result = self.device_type.release_interface(self.device, 0)
|
|
469
|
+
try:
|
|
470
|
+
self.device_type.reset(self.device)
|
|
471
|
+
except:
|
|
472
|
+
log.error("Couldn't reset device")
|
|
473
|
+
except:
|
|
474
|
+
log.warn("Failure in release interface", exc_info=1)
|
|
475
|
+
raise
|
|
476
|
+
return SpectrometerResponse(True)
|
|
477
|
+
|
|
478
|
+
def _schedule_disconnect(self, exc):
|
|
479
|
+
"""
|
|
480
|
+
Something in the driver has caused it to request the controlling
|
|
481
|
+
application to close the peripheral. The next time
|
|
482
|
+
WasatchDevice.acquire_data is called, it will pass a "poison pill" back
|
|
483
|
+
up the response queue.
|
|
484
|
+
Alternately, non-ENLIGHTEN callers can set "raise_exceptions" -> True for
|
|
485
|
+
in-process exception-handling.
|
|
486
|
+
"""
|
|
487
|
+
if self.raise_exceptions:
|
|
488
|
+
log.critical("_schedule_disconnect: raising exception %s", exc)
|
|
489
|
+
raise exc
|
|
490
|
+
else:
|
|
491
|
+
log.critical("requesting shutdown due to exception %s", exc)
|
|
492
|
+
self.shutdown_requested = True
|
|
493
|
+
|
|
494
|
+
def reset(self, *args):
|
|
495
|
+
log.debug("FID performing device reset")
|
|
496
|
+
#self.device_type.release_interface(self.device, 0)
|
|
497
|
+
self.device_type.reset(self.device)
|
|
498
|
+
log.debug(f"freed interface")
|
|
499
|
+
'''
|
|
500
|
+
pyusb_devices = list(self.device_type.find(find_all=True,
|
|
501
|
+
idVendor=self.device_id.vid,
|
|
502
|
+
idProduct=self.device_id.pid,
|
|
503
|
+
custom_match = lambda d: d.address == self.device_id.address and d.bus == self.device_id.bus))
|
|
504
|
+
# a couple notes,
|
|
505
|
+
# When you search how to do a reset by enable/disable on windows what comes up is devcon
|
|
506
|
+
# devcon links to pnputil as the recommended tool to use and comes preinstalled on windows
|
|
507
|
+
# pnputil needs the instance ID, which is pretty well hidden in pyusb
|
|
508
|
+
# I had to look at the full configuration string and saw that the way to get the instance id was the following
|
|
509
|
+
# using _try_get_string https://github.com/pyusb/pyusb/blob/master/usb/core.py#L1221
|
|
510
|
+
# so techincally we shouldn't do this by the _ meaning it should be private, but it's the only way I see
|
|
511
|
+
device_instance_id = f'USB\VID_{self.device_id.vid:04X}&PID_{self.device_id.pid:04X}\{usb.core._try_get_string(pyusb_devices[0], pyusb_devices[0].iSerialNumber)}'
|
|
512
|
+
log.debug(f"In reset and restart trying to reset instance id {device_instance_id}")
|
|
513
|
+
subprocess.run(["pnputil", r"/reboot", r"/disable-device", device_instance_id])
|
|
514
|
+
subprocess.run(["pnputil", r"/reboot", r"/enable-device", device_instance_id])
|
|
515
|
+
'''
|
|
516
|
+
# ##########################################################################
|
|
517
|
+
# Utility Methods
|
|
518
|
+
# ##########################################################################
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _to40bit(self, val):
|
|
522
|
+
"""
|
|
523
|
+
Laser modulation and continuous-strobe commands take arguments in micro-
|
|
524
|
+
seconds as 40-bit values, where the least-significant 16 bits are passed
|
|
525
|
+
as wValue, the next-significant 16 as wIndex, and the most-significant
|
|
526
|
+
as a single byte of payload. This function takes an unsigned integral
|
|
527
|
+
value (presumably microseconds) and returns a tuple of wValue, wIndex
|
|
528
|
+
and a buffer to pass as payload.
|
|
529
|
+
"""
|
|
530
|
+
lsw = val & 0xffff
|
|
531
|
+
msw = (val >> 16) & 0xffff
|
|
532
|
+
buf = [ (val >> 32) & 0xff ] + [0] * 7
|
|
533
|
+
return (lsw, msw, buf)
|
|
534
|
+
|
|
535
|
+
def _wait_for_usb_available(self):
|
|
536
|
+
"""
|
|
537
|
+
Wait until any enforced USB packet intervals have elapsed. This does
|
|
538
|
+
nothing in most cases - the function is normally a no-op.
|
|
539
|
+
However, if the application has defined min/max_usb_interval_ms (say
|
|
540
|
+
(20, 50ms), then pick a random delay in the defined window (e.g. 37ms)
|
|
541
|
+
and sleep until it has been at least that long since the last USB
|
|
542
|
+
exchange.
|
|
543
|
+
The purpose of this function was to wring-out some early ARM micro-
|
|
544
|
+
controllers with apparent timing issues under high-speed USB 2.0, to see
|
|
545
|
+
if communications issues disappeared if we enforced a communication
|
|
546
|
+
latency from the software side.
|
|
547
|
+
"""
|
|
548
|
+
if self.settings.state.max_usb_interval_ms <= 0:
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
if self.last_usb_timestamp is not None:
|
|
552
|
+
delay_ms = randint(self.settings.state.min_usb_interval_ms, self.settings.state.max_usb_interval_ms)
|
|
553
|
+
next_usb_timestamp = self.last_usb_timestamp + datetime.timedelta(milliseconds=delay_ms)
|
|
554
|
+
now = datetime.datetime.now()
|
|
555
|
+
if now < next_usb_timestamp:
|
|
556
|
+
sleep_sec = (next_usb_timestamp - now).total_seconds()
|
|
557
|
+
log.debug("fid: sleeping %.3f sec to enforce %d ms USB interval", sleep_sec, delay_ms)
|
|
558
|
+
sleep(sleep_sec)
|
|
559
|
+
self.last_usb_timestamp = datetime.datetime.now()
|
|
560
|
+
|
|
561
|
+
def _check_for_random_error(self):
|
|
562
|
+
"""
|
|
563
|
+
This function is provided to simulate random USB communication errors
|
|
564
|
+
during regression testing, and is normally a no-op.
|
|
565
|
+
"""
|
|
566
|
+
if not self.inject_random_errors:
|
|
567
|
+
return False
|
|
568
|
+
|
|
569
|
+
if random.random() <= self.random_error_perc:
|
|
570
|
+
log.critical("Randomly-injected error")
|
|
571
|
+
self._schedule_disconnect(Exception("Randomly-injected error"))
|
|
572
|
+
return True
|
|
573
|
+
return False
|
|
574
|
+
|
|
575
|
+
##
|
|
576
|
+
# Until support for even/odd InGaAs gain and offset have been added to the
|
|
577
|
+
# firmware, apply the correction in software.
|
|
578
|
+
def _correct_ingaas_gain_and_offset(self, spectrum: list[float]):
|
|
579
|
+
if not self.settings.is_ingaas() or self.settings.eeprom.hardware_even_odd:
|
|
580
|
+
return False
|
|
581
|
+
|
|
582
|
+
# if even and odd pixels have the same settings, there's no point in doing anything
|
|
583
|
+
if self.settings.eeprom.detector_gain_odd == self.settings.eeprom.detector_gain and \
|
|
584
|
+
self.settings.eeprom.detector_offset_odd == self.settings.eeprom.detector_offset:
|
|
585
|
+
return False
|
|
586
|
+
|
|
587
|
+
if 0 == self.settings.eeprom.detector_gain:
|
|
588
|
+
log.debug("declining to attempt division by zero")
|
|
589
|
+
return
|
|
590
|
+
|
|
591
|
+
log.debug("rescaling InGaAs odd pixels from even gain %.4f, offset %d to odd gain %.4f, offset %d",
|
|
592
|
+
self.settings.eeprom.detector_gain,
|
|
593
|
+
self.settings.eeprom.detector_offset,
|
|
594
|
+
self.settings.eeprom.detector_gain_odd,
|
|
595
|
+
self.settings.eeprom.detector_offset_odd)
|
|
596
|
+
|
|
597
|
+
log.debug("before: %d, %d, %d, %d, %d", spectrum[0], spectrum[1], spectrum[2], spectrum[3], spectrum[4])
|
|
598
|
+
|
|
599
|
+
# iterate over the ODD pixels of the spectrum
|
|
600
|
+
for i in range(1, len(spectrum), 2):
|
|
601
|
+
|
|
602
|
+
# back-out the incorrectly applied "even" gain and offset
|
|
603
|
+
old = float(spectrum[i])
|
|
604
|
+
raw = (old - self.settings.eeprom.detector_offset) / self.settings.eeprom.detector_gain
|
|
605
|
+
|
|
606
|
+
# apply the correct "odd" gain and offset
|
|
607
|
+
spectrum[i] = (raw * self.settings.eeprom.detector_gain_odd) + self.settings.eeprom.detector_offset_odd
|
|
608
|
+
|
|
609
|
+
if i < 5 or i > len(spectrum) - 5:
|
|
610
|
+
log.debug(" pixel %4d: old %.2f raw %.2f new %.2f", i, old, raw, spectrum[i])
|
|
611
|
+
|
|
612
|
+
log.debug("after: %d, %d, %d, %d, %d", spectrum[0], spectrum[1], spectrum[2], spectrum[3], spectrum[4])
|
|
613
|
+
|
|
614
|
+
return True
|
|
615
|
+
|
|
616
|
+
def _apply_linear_pixel_calibration(self, spectrum):
|
|
617
|
+
if not self.settings.linear_pixel_calibration:
|
|
618
|
+
return
|
|
619
|
+
|
|
620
|
+
(slopes, offsets) = self.settings.linear_pixel_calibration
|
|
621
|
+
smoothed = [] # could be faster in Numpy
|
|
622
|
+
for i, intensity in enumerate(spectrum):
|
|
623
|
+
smoothed.append(intensity * slopes[i] + offsets[i])
|
|
624
|
+
return smoothed
|
|
625
|
+
|
|
626
|
+
def _apply_horizontal_binning(self, spectrum):
|
|
627
|
+
if not self.settings.eeprom.horiz_binning_enabled:
|
|
628
|
+
return spectrum
|
|
629
|
+
|
|
630
|
+
mode = self.settings.eeprom.multi_wavelength_calibration.get("horiz_binning_mode")
|
|
631
|
+
if mode == IMX385.BIN_2X2:
|
|
632
|
+
return self.imx385.bin_2x2(spectrum)
|
|
633
|
+
elif mode == IMX385.CORRECT_SSC:
|
|
634
|
+
return self.imx385.correct_ssc(spectrum, self.settings.wavelengths)
|
|
635
|
+
elif mode == IMX385.CORRECT_SSC_BIN_2X2:
|
|
636
|
+
spectrum = self.imx385.correct_ssc(spectrum, self.settings.wavelengths)
|
|
637
|
+
return self.imx385.bin_2x2(spectrum)
|
|
638
|
+
elif mode == IMX385.BIN_4X2:
|
|
639
|
+
return self.imx385.bin_4x2(spectrum)
|
|
640
|
+
elif mode == IMX385.BIN_4X2_INTERP:
|
|
641
|
+
return self.imx385.bin_4x2_interp(spectrum, self.settings.wavelengths)
|
|
642
|
+
elif mode == IMX385.BIN_4X2_AVG:
|
|
643
|
+
return self.imx385.bin_4x2_avg(spectrum)
|
|
644
|
+
else:
|
|
645
|
+
# there may be legacy units in the field where this byte is
|
|
646
|
+
# uninitialized to 0xff...treat as 0x00 for now
|
|
647
|
+
log.error("invalid horizontal binning mode {mode}...defaulting to bin_2x2")
|
|
648
|
+
return self.imx385.bin_2x2(spectrum)
|
|
649
|
+
|
|
650
|
+
def _correct_bad_pixels(self, spectrum):
|
|
651
|
+
"""
|
|
652
|
+
If a spectrometer has bad_pixels configured in the EEPROM, then average
|
|
653
|
+
over them in the driver.
|
|
654
|
+
Note this function modifies the passed array in-place, rather than
|
|
655
|
+
returning a modified copy.
|
|
656
|
+
@note assumes bad_pixels is previously sorted
|
|
657
|
+
"""
|
|
658
|
+
|
|
659
|
+
if self.settings is None or \
|
|
660
|
+
self.settings.eeprom is None or \
|
|
661
|
+
self.settings.eeprom.bad_pixels is None or \
|
|
662
|
+
len(self.settings.eeprom.bad_pixels) == 0 or \
|
|
663
|
+
self.settings.state.detector_regions is not None:
|
|
664
|
+
return False
|
|
665
|
+
|
|
666
|
+
if spectrum is None or len(spectrum) == 0:
|
|
667
|
+
return False
|
|
668
|
+
|
|
669
|
+
pixels = len(spectrum)
|
|
670
|
+
bad_pixels = self.settings.eeprom.bad_pixels
|
|
671
|
+
|
|
672
|
+
# iterate over each bad pixel
|
|
673
|
+
i = 0
|
|
674
|
+
while i < len(bad_pixels):
|
|
675
|
+
|
|
676
|
+
bad_pix = bad_pixels[i]
|
|
677
|
+
|
|
678
|
+
if bad_pix == 0:
|
|
679
|
+
# handle the left edge
|
|
680
|
+
next_good = bad_pix + 1
|
|
681
|
+
while next_good in bad_pixels and next_good < pixels:
|
|
682
|
+
next_good += 1
|
|
683
|
+
i += 1
|
|
684
|
+
if next_good < pixels:
|
|
685
|
+
for j in range(next_good):
|
|
686
|
+
spectrum[j] = spectrum[next_good]
|
|
687
|
+
else:
|
|
688
|
+
|
|
689
|
+
# find previous good pixel
|
|
690
|
+
prev_good = bad_pix - 1
|
|
691
|
+
while prev_good in bad_pixels and prev_good >= 0:
|
|
692
|
+
prev_good -= 1
|
|
693
|
+
|
|
694
|
+
if prev_good >= 0:
|
|
695
|
+
# find next good pixel
|
|
696
|
+
next_good = bad_pix + 1
|
|
697
|
+
while next_good in bad_pixels and next_good < pixels:
|
|
698
|
+
next_good += 1
|
|
699
|
+
i += 1
|
|
700
|
+
|
|
701
|
+
if next_good < pixels:
|
|
702
|
+
# for now, draw a line between previous and next_good pixels
|
|
703
|
+
# TODO: consider some kind of curve-fit
|
|
704
|
+
delta = float(spectrum[next_good] - spectrum[prev_good])
|
|
705
|
+
rng = next_good - prev_good
|
|
706
|
+
step = delta / rng
|
|
707
|
+
# log.debug(f"correct_bad_pixels: bad_pix {bad_pix}, prev_good {prev_good} ({spectrum[prev_good]}), next_good {next_good} ({spectrum[next_good]}), delta {delta:.2f}, rng {rng}, step {step:.2f}")
|
|
708
|
+
for j in range(rng - 1):
|
|
709
|
+
new = spectrum[prev_good] + step * (j + 1)
|
|
710
|
+
spectrum[prev_good + j + 1] = new
|
|
711
|
+
# log.debug(f"correct_bad_pixels: stomping pix {prev_good + j + 1} with {new}")
|
|
712
|
+
else:
|
|
713
|
+
# we ran off the high end, so copy-right
|
|
714
|
+
for j in range(bad_pix, pixels):
|
|
715
|
+
spectrum[j] = spectrum[prev_good]
|
|
716
|
+
|
|
717
|
+
# advance to next bad pixel
|
|
718
|
+
i += 1
|
|
719
|
+
return True
|
|
720
|
+
|
|
721
|
+
def _send_code(self,
|
|
722
|
+
bRequest: int,
|
|
723
|
+
wValue: int = 0,
|
|
724
|
+
wIndex: int = 0,
|
|
725
|
+
data_or_wLength = None,
|
|
726
|
+
label: str = "",
|
|
727
|
+
dry_run: bool = False,
|
|
728
|
+
retry_on_error: bool = False,
|
|
729
|
+
success_result: int = 0x00) -> SpectrometerResponse:
|
|
730
|
+
if self.shutdown_requested or (not self.connected and not self.connecting):
|
|
731
|
+
log.debug("_send_code: not attempting because not connected")
|
|
732
|
+
return SpectrometerResponse(False)
|
|
733
|
+
|
|
734
|
+
prefix = "" if not label else ("%s: " % label)
|
|
735
|
+
result = None
|
|
736
|
+
|
|
737
|
+
if data_or_wLength is None:
|
|
738
|
+
if self.settings.is_arm():
|
|
739
|
+
data_or_wLength = [0] * 8
|
|
740
|
+
else:
|
|
741
|
+
data_or_wLength = 0
|
|
742
|
+
|
|
743
|
+
log.debug("%s_send_code: request 0x%02x value 0x%04x index 0x%04x data/len %s",
|
|
744
|
+
prefix, bRequest, wValue, wIndex, data_or_wLength)
|
|
745
|
+
|
|
746
|
+
if dry_run:
|
|
747
|
+
return SpectrometerResponse(keep_alive=True)
|
|
748
|
+
|
|
749
|
+
if self._check_for_random_error():
|
|
750
|
+
return SpectrometerResponse(poison_pill=False)
|
|
751
|
+
|
|
752
|
+
retry_count = 0
|
|
753
|
+
while True:
|
|
754
|
+
try:
|
|
755
|
+
self._wait_for_usb_available()
|
|
756
|
+
result = self.device_type.ctrl_transfer(self.device,
|
|
757
|
+
0x40, # HOST_TO_DEVICE
|
|
758
|
+
bRequest,
|
|
759
|
+
wValue,
|
|
760
|
+
wIndex,
|
|
761
|
+
data_or_wLength) # add TIMEOUT_MS parameter?
|
|
762
|
+
except Exception as exc:
|
|
763
|
+
log.critical("Hardware Failure FID Send Code Problem with ctrl transfer", exc_info=1)
|
|
764
|
+
self._schedule_disconnect(exc)
|
|
765
|
+
return SpectrometerResponse(poison_pill=True)
|
|
766
|
+
|
|
767
|
+
log.debug("%s_send_code: request 0x%02x value 0x%04x index 0x%04x data/len %s: result %s",
|
|
768
|
+
prefix, bRequest, wValue, wIndex, data_or_wLength, result)
|
|
769
|
+
|
|
770
|
+
if not retry_on_error:
|
|
771
|
+
return SpectrometerResponse(keep_alive=True)
|
|
772
|
+
|
|
773
|
+
# retry logic enabled, so compare result to expected
|
|
774
|
+
matched_expected = True
|
|
775
|
+
if len(success_result) < len(result):
|
|
776
|
+
matched_expected = False
|
|
777
|
+
else:
|
|
778
|
+
for i in range(len(success_result)):
|
|
779
|
+
if result[i] != success_result[i]:
|
|
780
|
+
matched_expected = False
|
|
781
|
+
break
|
|
782
|
+
|
|
783
|
+
if matched_expected:
|
|
784
|
+
return SpectrometerResponse(keep_alive=True)
|
|
785
|
+
|
|
786
|
+
# apparently it didn't match expected
|
|
787
|
+
retry_count += 1
|
|
788
|
+
if retry_count > self.retry_max:
|
|
789
|
+
log.error("giving up after %d retries", retry_count)
|
|
790
|
+
return SpectrometerResponse(poison_pill=True)
|
|
791
|
+
|
|
792
|
+
# try again
|
|
793
|
+
log.error("retrying (attempt %d)", retry_count + 1)
|
|
794
|
+
return SpectrometerResponse(True)
|
|
795
|
+
|
|
796
|
+
## @note weird that so few calls to this function override the default wLength
|
|
797
|
+
# @todo consider adding retry logic as well
|
|
798
|
+
def _get_code(self,
|
|
799
|
+
bRequest: int,
|
|
800
|
+
wValue: int = 0,
|
|
801
|
+
wIndex: int = 0,
|
|
802
|
+
wLength: int = 64,
|
|
803
|
+
label: str = "",
|
|
804
|
+
msb_len: int = None,
|
|
805
|
+
lsb_len: int = None) -> SpectrometerResponse:
|
|
806
|
+
prefix = "" if not label else ("%s: " % label)
|
|
807
|
+
result = None
|
|
808
|
+
|
|
809
|
+
if self.shutdown_requested or (not self.connected and not self.connecting):
|
|
810
|
+
log.debug("_get_code: not attempting because not connected")
|
|
811
|
+
return SpectrometerResponse()
|
|
812
|
+
|
|
813
|
+
if self._check_for_random_error():
|
|
814
|
+
log.debug("random error")
|
|
815
|
+
return SpectrometerResponse(poison_pill=True)
|
|
816
|
+
|
|
817
|
+
try:
|
|
818
|
+
self._wait_for_usb_available()
|
|
819
|
+
result = self.device_type.ctrl_transfer(self.device,
|
|
820
|
+
0xc0, # DEVICE_TO_HOST
|
|
821
|
+
bRequest,
|
|
822
|
+
wValue,
|
|
823
|
+
wIndex,
|
|
824
|
+
wLength)
|
|
825
|
+
except Exception as exc:
|
|
826
|
+
log.critical(f"Hardware Failure FID Get Code Problem with ctrl transfer (bRequest 0x{bRequest:02x}, wValue 0x{wValue:04x}, wIndex 0x{wIndex:04x}, label {label})", exc_info=1)
|
|
827
|
+
self._schedule_disconnect(exc)
|
|
828
|
+
return SpectrometerResponse(poison_pill=True)
|
|
829
|
+
|
|
830
|
+
result_hex = " ".join([f"{v:02x}" for v in result])
|
|
831
|
+
log.debug("%s_get_code: request 0x%02x value 0x%04x index 0x%04x = [%s]",
|
|
832
|
+
prefix, bRequest, wValue, wIndex, result_hex)
|
|
833
|
+
|
|
834
|
+
if result is None:
|
|
835
|
+
log.critical("_get_code[%s, %s]: received null", label, self.device_id)
|
|
836
|
+
self._schedule_disconnect(f"_get_code[{label}] received NULL")
|
|
837
|
+
return SpectrometerResponse(keep_alive=True)
|
|
838
|
+
|
|
839
|
+
# demarshall or return raw array
|
|
840
|
+
value = 0
|
|
841
|
+
if msb_len is not None:
|
|
842
|
+
for i in range(msb_len):
|
|
843
|
+
value = value << 8 | result[i]
|
|
844
|
+
return SpectrometerResponse(data=value)
|
|
845
|
+
elif lsb_len is not None:
|
|
846
|
+
for i in range(lsb_len):
|
|
847
|
+
if i < len(result):
|
|
848
|
+
value = (result[i] << (8 * i)) | value
|
|
849
|
+
return SpectrometerResponse(data=value)
|
|
850
|
+
else:
|
|
851
|
+
return SpectrometerResponse(data=result)
|
|
852
|
+
|
|
853
|
+
def get_upper_code(self,
|
|
854
|
+
wValue: int,
|
|
855
|
+
wIndex: int = 0,
|
|
856
|
+
wLength: int = 64,
|
|
857
|
+
label: str = "",
|
|
858
|
+
msb_len: int = None,
|
|
859
|
+
lsb_len: int = None) -> SpectrometerResponse:
|
|
860
|
+
return self._get_code(0xff, wValue, wIndex, wLength, label=label, msb_len=msb_len, lsb_len=lsb_len)
|
|
861
|
+
|
|
862
|
+
# ##########################################################################
|
|
863
|
+
# initialization
|
|
864
|
+
# ##########################################################################
|
|
865
|
+
|
|
866
|
+
def _read_eeprom(self):
|
|
867
|
+
buffers = []
|
|
868
|
+
for page in range(EEPROM.MAX_PAGES):
|
|
869
|
+
buf = None
|
|
870
|
+
try:
|
|
871
|
+
response = self.get_upper_code(0x01, page, label="GET_MODEL_CONFIG(%d)" % page)
|
|
872
|
+
buf = response.data
|
|
873
|
+
if response.error_lvl != ErrorLevel.ok:
|
|
874
|
+
return response
|
|
875
|
+
except:
|
|
876
|
+
log.error("exception reading upper_code 0x01 with page %d", page, exc_info=1)
|
|
877
|
+
if buf is None:
|
|
878
|
+
msg = "unable to read EEPROM (null buf)"
|
|
879
|
+
log.error(msg)
|
|
880
|
+
return SpectrometerResponse(False, error_lvl=ErrorLevel.medium, error_msg=msg)
|
|
881
|
+
elif len(buf) < 64:
|
|
882
|
+
msg = f"unable to read EEPROM received buf of {buf} and len {len(buf)}"
|
|
883
|
+
log.error(msg)
|
|
884
|
+
return SpectrometerResponse(False, error_lvl=ErrorLevel.medium, error_msg=msg)
|
|
885
|
+
buffers.append(buf)
|
|
886
|
+
|
|
887
|
+
flat_buffers_all_ones = True
|
|
888
|
+
for page in buffers:
|
|
889
|
+
for byte in page:
|
|
890
|
+
flat_buffers_all_ones = flat_buffers_all_ones and (byte == 0xFF)
|
|
891
|
+
|
|
892
|
+
if flat_buffers_all_ones:
|
|
893
|
+
return SpectrometerResponse(data=False, error_msg="Saw all Fs for EEPROM. Check EEPROM Programmed.", error_lvl=ErrorLevel.low)
|
|
894
|
+
return SpectrometerResponse(data=self.settings.eeprom.parse(buffers))
|
|
895
|
+
|
|
896
|
+
def has_linearity_coeffs(self):
|
|
897
|
+
"""
|
|
898
|
+
At least one linearity coeff is other than 0 or -1 (and no NaN).
|
|
899
|
+
|
|
900
|
+
Public because used by wasatch-shell.
|
|
901
|
+
"""
|
|
902
|
+
if self.settings.eeprom.linearity_coeffs:
|
|
903
|
+
for c in self.settings.eeprom.linearity_coeffs:
|
|
904
|
+
if math.isnan(c):
|
|
905
|
+
return False
|
|
906
|
+
for c in self.settings.eeprom.linearity_coeffs:
|
|
907
|
+
if c != 0 and c != -1:
|
|
908
|
+
return True
|
|
909
|
+
return False
|
|
910
|
+
|
|
911
|
+
def _read_fpga_compilation_options(self):
|
|
912
|
+
if self.settings.is_arm():
|
|
913
|
+
log.debug("ARM spectrometers no longer supporting FPGA compilation options")
|
|
914
|
+
return
|
|
915
|
+
|
|
916
|
+
response = self.get_upper_code(0x04, label="READ_COMPILATION_OPTIONS", lsb_len=3)
|
|
917
|
+
word = response.data
|
|
918
|
+
self.settings.fpga_options.parse(word)
|
|
919
|
+
|
|
920
|
+
# ##########################################################################
|
|
921
|
+
# Accessors
|
|
922
|
+
# ##########################################################################
|
|
923
|
+
|
|
924
|
+
# @todo test endian order (in and out)
|
|
925
|
+
def get_battery_register(self, reg: int):
|
|
926
|
+
reg = reg & 0xffff
|
|
927
|
+
return self.get_upper_code(0x14, wIndex=reg, label="GET_BATTERY_REG", msb_len=2)
|
|
928
|
+
|
|
929
|
+
def get_battery_state_raw(self):
|
|
930
|
+
"""Retrieves the raw battery reading and then caches it for 1 sec"""
|
|
931
|
+
now = datetime.datetime.now()
|
|
932
|
+
if self.settings.state.battery_timestamp is not None and (now - self.settings.state.battery_timestamp).total_seconds() < 1:
|
|
933
|
+
return SpectrometerResponse(data=self.settings.state.battery_raw)
|
|
934
|
+
|
|
935
|
+
self.settings.state.battery_timestamp = now
|
|
936
|
+
response = self.get_upper_code(0x13, label="GET_BATTERY_STATE", msb_len=3)
|
|
937
|
+
self.settings.state.battery_raw = response.data
|
|
938
|
+
|
|
939
|
+
log.debug(f"battery_state_raw: 0x{self.settings.state.battery_raw:04x}")
|
|
940
|
+
return SpectrometerResponse(data=self.settings.state.battery_raw)
|
|
941
|
+
|
|
942
|
+
def get_battery_percentage(self):
|
|
943
|
+
response = self.get_battery_state_raw()
|
|
944
|
+
word = response.data
|
|
945
|
+
lsb = (word >> 16) & 0xff
|
|
946
|
+
msb = (word >> 8) & 0xff
|
|
947
|
+
perc = msb + (1.0 * lsb / 256.0)
|
|
948
|
+
log.debug("battery_perc: %.2f%%", perc)
|
|
949
|
+
return SpectrometerResponse(data=perc)
|
|
950
|
+
|
|
951
|
+
def get_battery_charging(self):
|
|
952
|
+
res = self.get_battery_state_raw()
|
|
953
|
+
word = res.data
|
|
954
|
+
charging = (0 != (word & 0xff))
|
|
955
|
+
return SpectrometerResponse(data=charging)
|
|
956
|
+
|
|
957
|
+
def get_integration_time_ms(self):
|
|
958
|
+
response = self._get_code(0xbf, label="GET_INTEGRATION_TIME_MS", lsb_len=3)
|
|
959
|
+
ms = response.data
|
|
960
|
+
|
|
961
|
+
if self.settings.state.integration_time_ms > 0:
|
|
962
|
+
log.debug(f"GET_INTEGRATION_TIME_MS: now {ms}")
|
|
963
|
+
self.settings.state.integration_time_ms = ms
|
|
964
|
+
else:
|
|
965
|
+
log.debug("declining to initialize session integration_time_ms from spectrometer")
|
|
966
|
+
|
|
967
|
+
return SpectrometerResponse(data=ms)
|
|
968
|
+
|
|
969
|
+
def set_dfu_enable(self):
|
|
970
|
+
"""
|
|
971
|
+
Puts ARM-based spectrometers into Device Firmware Update (DFU) mode.
|
|
972
|
+
@warning reflashing spectrometer firmware without specific instruction and
|
|
973
|
+
support from Wasatch Photonics will void your warranty
|
|
974
|
+
"""
|
|
975
|
+
if not self.settings.is_arm():
|
|
976
|
+
msg = "DFU mode only supported for ARM-based spectrometers"
|
|
977
|
+
log.error(msg)
|
|
978
|
+
return SpectrometerResponse(error_lvl=ErrorLevel.low, error_msg=msg, keep_alive=True)
|
|
979
|
+
|
|
980
|
+
result = self._send_code(0xfe, label="SET_DFU_ENABLE")
|
|
981
|
+
|
|
982
|
+
self.queue_message("marquee_info", "%s in DFU mode" % self.settings.eeprom.serial_number)
|
|
983
|
+
|
|
984
|
+
self._schedule_disconnect(Exception("DFU Mode"))
|
|
985
|
+
return SpectrometerResponse(data=result)
|
|
986
|
+
|
|
987
|
+
def set_detector_offset(self, value: int):
|
|
988
|
+
word = utils.clamp_to_int16(value)
|
|
989
|
+
self.settings.eeprom.detector_offset = word
|
|
990
|
+
# log.debug("value %d (%s) = 0x%04x (%s)", value, format(value, 'b'), word, format(word, 'b'))
|
|
991
|
+
return self._send_code(0xb6, word, label="SET_DETECTOR_OFFSET")
|
|
992
|
+
|
|
993
|
+
def set_detector_offset_odd(self, value: int):
|
|
994
|
+
if not self.settings.is_ingaas():
|
|
995
|
+
log.debug("SET_DETECTOR_OFFSET_ODD only supported on InGaAs")
|
|
996
|
+
return SpectrometerResponse(keep_alive=True, error_lvl=ErrorLevel.low)
|
|
997
|
+
|
|
998
|
+
word = utils.clamp_to_int16(value)
|
|
999
|
+
self.settings.eeprom.detector_offset_odd = word
|
|
1000
|
+
|
|
1001
|
+
return self._send_code(0x9c, word, label="SET_DETECTOR_OFFSET_ODD")
|
|
1002
|
+
|
|
1003
|
+
def get_detector_gain(self, update_session_eeprom: bool = False):
|
|
1004
|
+
"""
|
|
1005
|
+
Read the device stored gain. Convert from binary "half-precision" float.
|
|
1006
|
+
- 1st byte (LSB) is binary encoded: bit 0 = 1/2, bit 1 = 1/4, bit 2 = 1/8 etc.
|
|
1007
|
+
- 2nd byte (MSB) is the integral part to the left of the decimal
|
|
1008
|
+
E.g., 231 dec == 0x01e7 == 1.90234375
|
|
1009
|
+
"""
|
|
1010
|
+
res = self._get_code(0xc5, label="GET_DETECTOR_GAIN")
|
|
1011
|
+
|
|
1012
|
+
result = res.data
|
|
1013
|
+
|
|
1014
|
+
if result is None:
|
|
1015
|
+
msg = "GET_DETECTOR_GAIN returned NULL!"
|
|
1016
|
+
log.error(msg)
|
|
1017
|
+
return SpectrometerResponse(error_lvl=ErrorLevel.medium, error_msg=msg, keep_alive=True)
|
|
1018
|
+
|
|
1019
|
+
lsb = result[0] # little-endian, OPPOSITE of set_detector_gain
|
|
1020
|
+
msb = result[1]
|
|
1021
|
+
|
|
1022
|
+
raw = (msb << 8) | lsb
|
|
1023
|
+
|
|
1024
|
+
gain = msb + lsb / 256.0
|
|
1025
|
+
log.debug("get_detector_gain: %f (raw 0x%04x) (session eeprom %f)" % (
|
|
1026
|
+
gain, raw, self.settings.eeprom.detector_gain))
|
|
1027
|
+
|
|
1028
|
+
if update_session_eeprom:
|
|
1029
|
+
self.settings.eeprom.detector_gain = gain
|
|
1030
|
+
|
|
1031
|
+
if self.settings.is_micro():
|
|
1032
|
+
self.settings.state.gain_db = gain
|
|
1033
|
+
|
|
1034
|
+
return SpectrometerResponse(data=gain)
|
|
1035
|
+
|
|
1036
|
+
def get_detector_gain_odd(self, update_session_eeprom: bool = False):
|
|
1037
|
+
if not self.settings.is_ingaas():
|
|
1038
|
+
log.debug("GET_DETECTOR_GAIN_ODD only supported on InGaAs")
|
|
1039
|
+
return SpectrometerResponse(data=self.settings.eeprom.detector_gain_odd)
|
|
1040
|
+
|
|
1041
|
+
result = self._get_code(0x9f, label="GET_DETECTOR_GAIN_ODD")
|
|
1042
|
+
|
|
1043
|
+
# not reversing this for XS, because we don't currently have an XS-NIR
|
|
1044
|
+
lsb = result[0] # LSB-MSB
|
|
1045
|
+
msb = result[1]
|
|
1046
|
+
raw = (msb << 8) | lsb
|
|
1047
|
+
|
|
1048
|
+
gain = msb + lsb / 256.0
|
|
1049
|
+
log.debug("get_detector_gain_odd: %f (0x%04x) (session eeprom %f)" % (
|
|
1050
|
+
gain, raw, self.settings.eeprom.detector_gain_odd))
|
|
1051
|
+
if update_session_eeprom:
|
|
1052
|
+
self.settings.eeprom.detector_gain_odd = gain
|
|
1053
|
+
return SpectrometerResponse(data=gain)
|
|
1054
|
+
|
|
1055
|
+
##
|
|
1056
|
+
# Note that this is used for detector types, including:
|
|
1057
|
+
#
|
|
1058
|
+
# - Hamamatsu silicon (S16010-*, S16011-*, etc)
|
|
1059
|
+
# - Hamamatsu InGaAs (G9214 etc)
|
|
1060
|
+
# - Sony IMX (IMX385 etc)
|
|
1061
|
+
#
|
|
1062
|
+
# It is important to understand that the UNIT of this value changes between
|
|
1063
|
+
# Hamamatsu and IMX detectors, but the DATATYPE does not.
|
|
1064
|
+
#
|
|
1065
|
+
# Reasonable gain levels for Hamamatsu are a floating-point scalar, literally
|
|
1066
|
+
# used to scale (gain) the signal, and are usually in the range (0.8 .. 1.2)
|
|
1067
|
+
# or thereabouts.
|
|
1068
|
+
#
|
|
1069
|
+
# Reasonable levels for IMX sensors are in dB and vary by detector, but are
|
|
1070
|
+
# usually in the range (0.0 .. 31.0), with exactly 0.1dB precision. The
|
|
1071
|
+
# spectrometer's FW will round to the nearest setting (1.23 will be rounded
|
|
1072
|
+
# to 1.2). IMX sensors switch from "analog gain" to "digital gain" above
|
|
1073
|
+
# a given threshold...on the IMX123, analog gain is 0.0 - 31.0, and digital
|
|
1074
|
+
# is 31.1 - 72.0 (I think).
|
|
1075
|
+
#
|
|
1076
|
+
# @see https://wasatchphotonics.com/api/Wasatch.NET/class_wasatch_n_e_t_1_1_funky_float.html
|
|
1077
|
+
# @todo we should probably track runtime hardware gain in SpectrometerState,
|
|
1078
|
+
# not EEPROM
|
|
1079
|
+
def set_detector_gain(self, gain: float):
|
|
1080
|
+
if not self.is_sensor_stable():
|
|
1081
|
+
log.error("declining to change sensor settings while stabilizing")
|
|
1082
|
+
return
|
|
1083
|
+
|
|
1084
|
+
raw = self.settings.eeprom.float_to_uint16(gain)
|
|
1085
|
+
|
|
1086
|
+
# MZ: note that we SEND gain MSB-LSB, but we READ gain LSB-MSB?!
|
|
1087
|
+
log.debug("Send Detector Gain: 0x%04x (%s)", raw, gain)
|
|
1088
|
+
result = self._send_code(0xb7, raw, label="SET_DETECTOR_GAIN")
|
|
1089
|
+
|
|
1090
|
+
self.require_throwaway(gain != self.settings.eeprom.detector_gain)
|
|
1091
|
+
self.settings.eeprom.detector_gain = gain
|
|
1092
|
+
|
|
1093
|
+
if self.settings.is_xs():
|
|
1094
|
+
# self.queue_message("marquee_info", "sensor is stabilizing (gain)")
|
|
1095
|
+
self.settings.state.gain_db = gain
|
|
1096
|
+
|
|
1097
|
+
return result
|
|
1098
|
+
|
|
1099
|
+
def set_detector_gain_odd(self, gain: float):
|
|
1100
|
+
if not self.settings.is_ingaas():
|
|
1101
|
+
log.debug("SET_DETECTOR_GAIN_ODD only supported on InGaAs")
|
|
1102
|
+
return SpectrometerResponse(error_lvl=ErrorLevel.low, error_msg="SET_DETECTOR_GAIN_ODD only supported on InGaAs")
|
|
1103
|
+
|
|
1104
|
+
raw = self.settings.eeprom.float_to_uint16(gain)
|
|
1105
|
+
|
|
1106
|
+
# MZ: note that we SEND gain MSB-LSB, but we READ gain LSB-MSB?!
|
|
1107
|
+
log.debug("Send Detector Gain Odd: 0x%04x (%s)", raw, gain)
|
|
1108
|
+
self.settings.eeprom.detector_gain_odd = gain
|
|
1109
|
+
return self._send_code(0x9d, raw, label="SET_DETECTOR_GAIN_ODD")
|
|
1110
|
+
|
|
1111
|
+
def set_area_scan_enable(self, flag):
|
|
1112
|
+
"""
|
|
1113
|
+
Historically, this opcode moved around a bit. It is currently 0xeb, which
|
|
1114
|
+
conflicts with CF_SELECT). At one point it was 0xe9, which conflicted with
|
|
1115
|
+
LASER_RAMP_ENABLE.
|
|
1116
|
+
"""
|
|
1117
|
+
if self.settings.is_ingaas():
|
|
1118
|
+
log.error("area scan is not supported on InGaAs detectors (single line array)")
|
|
1119
|
+
return SpectrometerResponse(error_lvl=ErrorLevel.low, error_msg="area scan is not supported on InGaAs detectors (single line array)")
|
|
1120
|
+
|
|
1121
|
+
value = 1 if flag else 0
|
|
1122
|
+
self.settings.state.area_scan_enabled = flag
|
|
1123
|
+
self.settings.state.area_scan_first_trigger_sent = False
|
|
1124
|
+
return self._send_code(0xeb, value, label="SET_AREA_SCAN_ENABLE")
|
|
1125
|
+
|
|
1126
|
+
def set_area_scan_line_step(self, n):
|
|
1127
|
+
if not self.settings.is_xs():
|
|
1128
|
+
msg = "area scan line step is only supported on XS"
|
|
1129
|
+
log.error(msg)
|
|
1130
|
+
return SpectrometerResponse(error_lvl=ErrorLevel.low, error_msg=msg)
|
|
1131
|
+
|
|
1132
|
+
n = int(max(1, min(n, 255)))
|
|
1133
|
+
|
|
1134
|
+
log.debug(f"setting area scan line step to {n}")
|
|
1135
|
+
self.settings.state.area_scan_line_step = n
|
|
1136
|
+
return self.i2c_write(0x17, [n], label="SET_AREA_SCAN_LINE_STEP")
|
|
1137
|
+
|
|
1138
|
+
# MZ: I don't remember what boards / firmware supports this?
|
|
1139
|
+
def set_area_scan_line_count(self, n):
|
|
1140
|
+
self.debug(f"setting line count to {n}")
|
|
1141
|
+
self.dev.ctrl_transfer(H2D, 0xa6, n, 0, Z, TIMEOUT)
|
|
1142
|
+
return self._send_code(0xa6, n, label="SET_AREA_SCAN_LINE_COUNT")
|
|
1143
|
+
|
|
1144
|
+
# MZ: I don't remember what boards / firmware supports this?
|
|
1145
|
+
def set_area_scan_line_interval(self, n):
|
|
1146
|
+
self.debug(f"setting line interval to {n}")
|
|
1147
|
+
return self._send_code(0xa8, n, label="SET_AREA_SCAN_INTERVAL")
|
|
1148
|
+
|
|
1149
|
+
def get_sensor_line_length(self):
|
|
1150
|
+
value = self.get_upper_code(0x03, label="GET_LINE_LENGTH", lsb_len=2)
|
|
1151
|
+
if value != self.settings.eeprom.actual_pixels_horizontal:
|
|
1152
|
+
log.error("GET_LINE_LENGTH opcode result %d != EEPROM.actual_pixels_horizontal %d (using opcode result)",
|
|
1153
|
+
value, self.settings.eeprom.actual_pixels_horizontal)
|
|
1154
|
+
return SpectrometerResponse(data=value)
|
|
1155
|
+
|
|
1156
|
+
def get_microcontroller_firmware_version(self):
|
|
1157
|
+
if self.settings.microcontroller_firmware_version is not None:
|
|
1158
|
+
return SpectrometerResponse(data=self.settings.microcontroller_firmware_version)
|
|
1159
|
+
|
|
1160
|
+
res = self._get_code(0xc0, label="GET_CODE_REVISION")
|
|
1161
|
+
result = res.data
|
|
1162
|
+
version = "?.?.?.?"
|
|
1163
|
+
if result is not None and len(result) >= 4:
|
|
1164
|
+
version = "%d.%d.%d.%d" % (result[3], result[2], result[1], result[0]) # MSB-LSB
|
|
1165
|
+
|
|
1166
|
+
self.settings.microcontroller_firmware_version = version
|
|
1167
|
+
return SpectrometerResponse(data=version)
|
|
1168
|
+
|
|
1169
|
+
def get_fpga_firmware_version(self):
|
|
1170
|
+
if self.settings.fpga_firmware_version is not None:
|
|
1171
|
+
return SpectrometerResponse(data=self.settings.fpga_firmware_version)
|
|
1172
|
+
|
|
1173
|
+
s = ""
|
|
1174
|
+
res = self._get_code(0xb4, wLength=7, label="GET_FPGA_REV")
|
|
1175
|
+
result = res.data
|
|
1176
|
+
if result is not None:
|
|
1177
|
+
for i in range(len(result)):
|
|
1178
|
+
c = result[i]
|
|
1179
|
+
if 0x20 <= c < 0x7f: # visible ASCII
|
|
1180
|
+
s += chr(c)
|
|
1181
|
+
|
|
1182
|
+
self.settings.fpga_firmware_version = s
|
|
1183
|
+
return SpectrometerResponse(data=s)
|
|
1184
|
+
|
|
1185
|
+
def get_microcontroller_serial_number(self):
|
|
1186
|
+
if not self.settings.is_arm():
|
|
1187
|
+
log.debug("GET_MICROCONTROLLER_SERIAL_NUMBER requires ARM")
|
|
1188
|
+
return None
|
|
1189
|
+
|
|
1190
|
+
if not self.settings.supports_feature("microcontroller_serial_number"):
|
|
1191
|
+
log.debug("GET_MICROCONTROLLER_SERIAL_NUMBER not supported on this firmware")
|
|
1192
|
+
return None
|
|
1193
|
+
|
|
1194
|
+
result = self._get_code(0xff, wValue=0x2c, wLength=12, label="GET_MICROCONTROLLER_SERIAL_NUMBER")
|
|
1195
|
+
if result is None:
|
|
1196
|
+
log.error("GET_MICROCONTROLLER_SERIAL_NUMBER returned None")
|
|
1197
|
+
return None
|
|
1198
|
+
|
|
1199
|
+
data = result.data
|
|
1200
|
+
if data is None or len(data) != 12:
|
|
1201
|
+
log.error("GET_MICROCONTROLLER_SERIAL_NUMBER expected 12 bytes (received {len(data)})")
|
|
1202
|
+
return None
|
|
1203
|
+
|
|
1204
|
+
des = "".join(f"{v:02x}" for v in data) # Device Electronic Signature
|
|
1205
|
+
self.settings.microcontroller_serial_number = des
|
|
1206
|
+
return SpectrometerResponse(data=des)
|
|
1207
|
+
|
|
1208
|
+
def get_ble_firmware_version(self):
|
|
1209
|
+
if not self.settings.is_arm():
|
|
1210
|
+
log.debug("GET_BLE_FIRMWARE_VERSION requires ARM")
|
|
1211
|
+
return None
|
|
1212
|
+
|
|
1213
|
+
if not self.settings.supports_feature("get_ble_firmware_version"):
|
|
1214
|
+
log.debug("GET_BLE_FIRMWARE_VERSION not supported on this firmware")
|
|
1215
|
+
return None
|
|
1216
|
+
|
|
1217
|
+
result = self._get_code(0xff, wValue=0x2d, wLength=32, label="GET_BLE_FIRMWARE_VERSION")
|
|
1218
|
+
if result is None:
|
|
1219
|
+
return None
|
|
1220
|
+
|
|
1221
|
+
data = result.data
|
|
1222
|
+
if data is None:
|
|
1223
|
+
return None
|
|
1224
|
+
|
|
1225
|
+
s = ""
|
|
1226
|
+
for c in data:
|
|
1227
|
+
if c == 0:
|
|
1228
|
+
break
|
|
1229
|
+
s += chr(c)
|
|
1230
|
+
self.settings.ble_firmware_version = s
|
|
1231
|
+
return SpectrometerResponse(data=s)
|
|
1232
|
+
|
|
1233
|
+
def apply_edc(self, spectrum):
|
|
1234
|
+
"""
|
|
1235
|
+
Use optically black pixels for electrical dark correction (EDC).
|
|
1236
|
+
|
|
1237
|
+
@par IMX385LQR-C datasheet (p8)
|
|
1238
|
+
|
|
1239
|
+
@verbatim
|
|
1240
|
+
Pixels Count Description
|
|
1241
|
+
0-3 4 OB side ignored area <-- using this
|
|
1242
|
+
4-7 4 Effective pixel side ignored area
|
|
1243
|
+
8-15 8 Effective margin for color processing
|
|
1244
|
+
16-1935 1920 Recording pixel area
|
|
1245
|
+
1936-1944 9 Effective margin for color processing
|
|
1246
|
+
1945-1948 4 Effective pixel side ignored area
|
|
1247
|
+
1949-1951 3 Dummy
|
|
1248
|
+
@endverbatim
|
|
1249
|
+
|
|
1250
|
+
@note that this is called BEFORE horizontal binning, so should still work
|
|
1251
|
+
even with BIN_4X2
|
|
1252
|
+
|
|
1253
|
+
@todo we might want to make buffer length configurable, either in spectra
|
|
1254
|
+
or by time (consider 10ms vs 1sec integration time)
|
|
1255
|
+
"""
|
|
1256
|
+
if not self.settings.state.edc_enabled:
|
|
1257
|
+
return spectrum
|
|
1258
|
+
|
|
1259
|
+
if not self.settings.is_imx():
|
|
1260
|
+
return spectrum
|
|
1261
|
+
|
|
1262
|
+
if len(spectrum) != 1952:
|
|
1263
|
+
log.error("IMX EDC hard-coded to expect 1952px")
|
|
1264
|
+
return spectrum
|
|
1265
|
+
|
|
1266
|
+
# this averages electrical dark over SPACE
|
|
1267
|
+
dark_pixels = spectrum[0:4]
|
|
1268
|
+
avg_dark = round(sum(dark_pixels) / len(dark_pixels), 2)
|
|
1269
|
+
|
|
1270
|
+
return [ v - avg_dark for v in spectrum ]
|
|
1271
|
+
|
|
1272
|
+
def get_line(self, trigger=True, auto_raman_params=None):
|
|
1273
|
+
""" legacy alias """
|
|
1274
|
+
return self.get_spectrum(trigger, auto_raman_params)
|
|
1275
|
+
|
|
1276
|
+
def generate_timeout_ms(self):
|
|
1277
|
+
max_integ_ms = max(self.settings.state.integration_time_ms, self.settings.state.prev_integration_time_ms)
|
|
1278
|
+
if self.settings.is_micro():
|
|
1279
|
+
# we have no idea if Series-XS has to "wake up" the sensor, so wait
|
|
1280
|
+
# long enough for 20ms + 8 throwaway frames if need be (IMX385 datasheet p69)
|
|
1281
|
+
if self.settings.state.onboard_averaging:
|
|
1282
|
+
timeout_ms = max_integ_ms * (self.settings.state.scans_to_average + 7) + 500 * self.settings.num_connected_devices + 20
|
|
1283
|
+
else:
|
|
1284
|
+
timeout_ms = max_integ_ms * 8 + 500 * self.settings.num_connected_devices + 20
|
|
1285
|
+
if not self.has_received_spectrum:
|
|
1286
|
+
timeout_ms += 10_000
|
|
1287
|
+
else:
|
|
1288
|
+
timeout_ms = max_integ_ms * 2 + 1_000 * self.settings.num_connected_devices
|
|
1289
|
+
return int(timeout_ms)
|
|
1290
|
+
|
|
1291
|
+
def get_spectrum(self, trigger=True, auto_raman_params=None):
|
|
1292
|
+
"""
|
|
1293
|
+
Send "acquire", then immediately read the bulk endpoint(s).
|
|
1294
|
+
Probably the most important method in this class.
|
|
1295
|
+
|
|
1296
|
+
@param trigger (Input) send an initial ACQUIRE
|
|
1297
|
+
@param auto_raman_params (Input) if present, use VR_ACQUIRE_AUTO_RAMAN
|
|
1298
|
+
rather than the usual VR_ACQUIRE_CCD
|
|
1299
|
+
@returns tuple of (spectrum[], area_scan_row_count) for success
|
|
1300
|
+
@returns None when it times-out while waiting for an external trigger
|
|
1301
|
+
(interpret as, "didn't find any fish this time, try again in a bit")
|
|
1302
|
+
@returns False (bool) when it times-out or encounters an exception
|
|
1303
|
+
when NOT in external-triggered mode
|
|
1304
|
+
@throws exception on timeout (unless external triggering enabled)
|
|
1305
|
+
"""
|
|
1306
|
+
response = SpectrometerResponse()
|
|
1307
|
+
|
|
1308
|
+
if not self.is_sensor_stable():
|
|
1309
|
+
response.error_msg = "get_spectrum: leaving sensor alone while stabilizing"
|
|
1310
|
+
response.error_lvl = ErrorLevel.low
|
|
1311
|
+
response.keep_alive = True
|
|
1312
|
+
log.debug(response.error_msg)
|
|
1313
|
+
self.queue_message("marquee_info", "sensor is stabilizing")
|
|
1314
|
+
return response
|
|
1315
|
+
|
|
1316
|
+
########################################################################
|
|
1317
|
+
# send the ACQUIRE
|
|
1318
|
+
########################################################################
|
|
1319
|
+
|
|
1320
|
+
# main use-case for NOT sending a trigger would be when reading
|
|
1321
|
+
# subsequent lines of data from area scan "fast" mode
|
|
1322
|
+
|
|
1323
|
+
acquisition_timestamp = datetime.datetime.now()
|
|
1324
|
+
|
|
1325
|
+
# should we send a trigger?
|
|
1326
|
+
if self.settings.state.trigger_source != SpectrometerState.TRIGGER_SOURCE_INTERNAL:
|
|
1327
|
+
trigger = False
|
|
1328
|
+
if self.settings.state.area_scan_enabled:
|
|
1329
|
+
if self.settings.state.area_scan_first_trigger_sent:
|
|
1330
|
+
log.debug("get_spectrum: sending FIRST trigger after enabling area scan")
|
|
1331
|
+
self.settings.state.area_scan_first_trigger_sent = True
|
|
1332
|
+
else:
|
|
1333
|
+
log.debug("get_spectrum: skipping trigger during area scan")
|
|
1334
|
+
trigger = False
|
|
1335
|
+
|
|
1336
|
+
if trigger:
|
|
1337
|
+
# send an internal SW trigger
|
|
1338
|
+
log.debug("get_spectrum: requesting spectrum")
|
|
1339
|
+
|
|
1340
|
+
# should that trigger be an ACQUIRE or ACQUIRE_AUTO_RAMAN?
|
|
1341
|
+
if auto_raman_params:
|
|
1342
|
+
self._send_code(0xfd, data_or_wLength=auto_raman_params, label="ACQUIRE_AUTO_RAMAN")
|
|
1343
|
+
else:
|
|
1344
|
+
self._send_code(0xad, label="ACQUIRE_SPECTRUM")
|
|
1345
|
+
|
|
1346
|
+
########################################################################
|
|
1347
|
+
# prepare to read spectrum
|
|
1348
|
+
########################################################################
|
|
1349
|
+
|
|
1350
|
+
# this is the number of pixels we expect to read-out over USB
|
|
1351
|
+
if self.settings.is_imx385():
|
|
1352
|
+
# Currently, only the IMX385 uses horizontal binning, so for now
|
|
1353
|
+
# limit code changes to that detector (even though they haven't been
|
|
1354
|
+
# shown to break other hardware)
|
|
1355
|
+
pixels = self.settings.eeprom.actual_pixels_horizontal
|
|
1356
|
+
else:
|
|
1357
|
+
pixels = self.settings.pixels()
|
|
1358
|
+
|
|
1359
|
+
# when changing detector ROI, exactly ONE READ should be at the previous length
|
|
1360
|
+
# if self.prev_pixels is not None:
|
|
1361
|
+
# log.debug(f"get_spectrum: using one-time prev_pixels value of {self.prev_pixels} rather than {pixels}")
|
|
1362
|
+
# pixels = self.prev_pixels
|
|
1363
|
+
# self.prev_pixels = None
|
|
1364
|
+
|
|
1365
|
+
# all models return spectra as [uint16]
|
|
1366
|
+
endpoints = [0x82]
|
|
1367
|
+
block_len_bytes = pixels * 2
|
|
1368
|
+
if pixels == 2048 and not self.settings.is_arm():
|
|
1369
|
+
endpoints = [0x82, 0x86]
|
|
1370
|
+
block_len_bytes = 2048 # 1024 pixels apiece from two endpoints
|
|
1371
|
+
|
|
1372
|
+
timeout_ms = self.generate_timeout_ms()
|
|
1373
|
+
|
|
1374
|
+
self._wait_for_usb_available()
|
|
1375
|
+
|
|
1376
|
+
########################################################################
|
|
1377
|
+
# read the data from bulk endpoint(s)
|
|
1378
|
+
########################################################################
|
|
1379
|
+
|
|
1380
|
+
spectrum = []
|
|
1381
|
+
errors = 0
|
|
1382
|
+
for endpoint in endpoints:
|
|
1383
|
+
data = None
|
|
1384
|
+
while data is None:
|
|
1385
|
+
try:
|
|
1386
|
+
log.debug("waiting for %d bytes (timeout %s ms)", block_len_bytes, timeout_ms)
|
|
1387
|
+
data = self.device_type.read(self.device, endpoint, block_len_bytes, timeout=timeout_ms)
|
|
1388
|
+
log.debug("read %d bytes", len(data))
|
|
1389
|
+
except Exception as exc:
|
|
1390
|
+
if self.device_type is None:
|
|
1391
|
+
log.error(f"No device_type")
|
|
1392
|
+
response.error_msg = f"Encountered error on read"
|
|
1393
|
+
response.error_lvl = ErrorLevel.high
|
|
1394
|
+
response.poison_pill = True # unrecoverable
|
|
1395
|
+
return response
|
|
1396
|
+
elif self.settings.state.trigger_source == SpectrometerState.TRIGGER_SOURCE_EXTERNAL or auto_raman_params:
|
|
1397
|
+
# we don't know how long we'll have to wait for the response, so
|
|
1398
|
+
# just loop and hope
|
|
1399
|
+
# log.debug("still waiting for spectrum")
|
|
1400
|
+
pass
|
|
1401
|
+
else:
|
|
1402
|
+
errors += 1
|
|
1403
|
+
log.error(f"Encountered error on read of {exc}", exc_info=1)
|
|
1404
|
+
if errors < 3:
|
|
1405
|
+
log.error(f"ignoring error number {errors}")
|
|
1406
|
+
else:
|
|
1407
|
+
response.error_msg = "Encountered error on read"
|
|
1408
|
+
response.error_lvl = ErrorLevel.high
|
|
1409
|
+
return response
|
|
1410
|
+
|
|
1411
|
+
# This is a convoluted way to iterate across the received bytes in 'data' as
|
|
1412
|
+
# two interleaved arrays, both only processing alternating bytes, but one (i)
|
|
1413
|
+
# starting at zero (even bytes) and the other (j) starting at 1 (odd bytes).
|
|
1414
|
+
subspectrum = [int(i | (j << 8)) for i, j in zip(data[::2], data[1::2])] # LSB-MSB
|
|
1415
|
+
|
|
1416
|
+
spectrum.extend(subspectrum)
|
|
1417
|
+
|
|
1418
|
+
# empirically determined need for 5ms delay when switching endpoints
|
|
1419
|
+
# on 2048px detectors during area scan
|
|
1420
|
+
if self.settings.state.area_scan_enabled and pixels == 2048: # and endpoint == 0x82:
|
|
1421
|
+
log.debug("sleeping 5ms between endpoints")
|
|
1422
|
+
sleep(0.005)
|
|
1423
|
+
|
|
1424
|
+
# received a response, so decrement throwaways
|
|
1425
|
+
self.remaining_throwaways = max(0, self.remaining_throwaways - 1)
|
|
1426
|
+
|
|
1427
|
+
########################################################################
|
|
1428
|
+
# error-check the received spectrum
|
|
1429
|
+
########################################################################
|
|
1430
|
+
|
|
1431
|
+
log.debug("get_spectrum: completed in %d ms (vs integration time %d ms)",
|
|
1432
|
+
round((datetime.datetime.now() - acquisition_timestamp).total_seconds() * 1000, 0),
|
|
1433
|
+
self.settings.state.integration_time_ms)
|
|
1434
|
+
|
|
1435
|
+
log.debug("get_spectrum: pixels %d, endpoints %s, block %d, spectrum %s ...",
|
|
1436
|
+
len(spectrum), endpoints, block_len_bytes, spectrum[0:9])
|
|
1437
|
+
|
|
1438
|
+
if len(spectrum) != pixels:
|
|
1439
|
+
log.error("get_spectrum read wrong number of pixels (expected %d, read %d)", pixels, len(spectrum))
|
|
1440
|
+
response.error_msg = f"get_spectrum read wrong number of pixels (expected {pixels}, read {len(spectrum)})"
|
|
1441
|
+
response.error_lvl = ErrorLevel.low
|
|
1442
|
+
response.keep_alive = True
|
|
1443
|
+
return response
|
|
1444
|
+
|
|
1445
|
+
########################################################################
|
|
1446
|
+
#
|
|
1447
|
+
# post-process the spectrum
|
|
1448
|
+
#
|
|
1449
|
+
########################################################################
|
|
1450
|
+
|
|
1451
|
+
########################################################################
|
|
1452
|
+
# Success, so update "working" integration time
|
|
1453
|
+
########################################################################
|
|
1454
|
+
|
|
1455
|
+
self.settings.state.prev_integration_time_ms = self.settings.state.integration_time_ms
|
|
1456
|
+
self.has_received_spectrum = True
|
|
1457
|
+
|
|
1458
|
+
########################################################################
|
|
1459
|
+
# Apply InGaAs even/odd gain/offset in software
|
|
1460
|
+
########################################################################
|
|
1461
|
+
|
|
1462
|
+
# (before x-axis inversion because this is where FPGA will do it)
|
|
1463
|
+
|
|
1464
|
+
# this should be done in the FPGA, but older FW didn't have that
|
|
1465
|
+
# implemented, so fix in SW unless EEPROM indicates "already handled"
|
|
1466
|
+
if self.settings.is_ingaas():
|
|
1467
|
+
if not self.settings.eeprom.hardware_even_odd:
|
|
1468
|
+
self._correct_ingaas_gain_and_offset(spectrum)
|
|
1469
|
+
|
|
1470
|
+
########################################################################
|
|
1471
|
+
# Area Scan (rare)
|
|
1472
|
+
########################################################################
|
|
1473
|
+
|
|
1474
|
+
# (before x-axis inversion because line index at FPGA pixel 0)
|
|
1475
|
+
|
|
1476
|
+
# If we're in area scan mode, use SECOND pixel as row index (leave pixel
|
|
1477
|
+
# in spectrum). Do this before any horizontal averaging which might
|
|
1478
|
+
# corrupt first pixel).
|
|
1479
|
+
area_scan_row_count = -1
|
|
1480
|
+
if self.settings.state.area_scan_enabled:
|
|
1481
|
+
if spectrum[0] == 0xffff:
|
|
1482
|
+
area_scan_row_count = spectrum[1]
|
|
1483
|
+
else:
|
|
1484
|
+
log.error("first pixel of area scan expected to be 0xffff, read 0x{spectrum[0]:04x}")
|
|
1485
|
+
|
|
1486
|
+
########################################################################
|
|
1487
|
+
# Stomp array ends
|
|
1488
|
+
########################################################################
|
|
1489
|
+
|
|
1490
|
+
# (before x-axis inversion because optically masked pixels are physical)
|
|
1491
|
+
|
|
1492
|
+
# some detectors have "garbage" pixels at the front or end of every
|
|
1493
|
+
# spectrum (sync bytes and what-not)
|
|
1494
|
+
if not self.settings.state.area_scan_enabled:
|
|
1495
|
+
if self.settings.is_imx385() and self.settings.fpga_firmware_version == "01.1.01":
|
|
1496
|
+
utils.stomp_first(spectrum, 3)
|
|
1497
|
+
utils.stomp_last (spectrum, 2)
|
|
1498
|
+
if self.settings.is_imx392() and self.settings.state.detector_regions is None:
|
|
1499
|
+
utils.stomp_first(spectrum, 3)
|
|
1500
|
+
utils.stomp_last (spectrum, 17)
|
|
1501
|
+
|
|
1502
|
+
########################################################################
|
|
1503
|
+
# Electrical Dark Correction (EDC, experimental)
|
|
1504
|
+
########################################################################
|
|
1505
|
+
|
|
1506
|
+
if not self.settings.state.area_scan_enabled:
|
|
1507
|
+
spectrum = self.apply_edc(spectrum)
|
|
1508
|
+
|
|
1509
|
+
########################################################################
|
|
1510
|
+
# Invert X-Axis
|
|
1511
|
+
########################################################################
|
|
1512
|
+
|
|
1513
|
+
# For benches where the detector is essentially rotated 180-deg from our
|
|
1514
|
+
# typical orientation with regard to the grating (e.g. InGaAs OEM
|
|
1515
|
+
# benches, where red wavelengths are diffracted toward pixel 0, and blue
|
|
1516
|
+
# wavelengths toward pixel 511).
|
|
1517
|
+
#
|
|
1518
|
+
# Note this simply performs a horizontal FLIP (mirror) of the vertically-
|
|
1519
|
+
# binned 1-D spectra, and is NOT sufficient to perform a genuine 180-
|
|
1520
|
+
# degree rotation of 2-D imaging mode; if "authentic" area scan is
|
|
1521
|
+
# desired, the caller would likewise need to reverse the display order of
|
|
1522
|
+
# the rows.
|
|
1523
|
+
#
|
|
1524
|
+
# It is VITALLY IMPORTANT that all drivers agree
|
|
1525
|
+
# on when to perform X-Axis inversion in the
|
|
1526
|
+
# Order of Operations.
|
|
1527
|
+
#
|
|
1528
|
+
if not self.settings.state.area_scan_enabled:
|
|
1529
|
+
if self.settings.eeprom.invert_x_axis:
|
|
1530
|
+
spectrum.reverse()
|
|
1531
|
+
|
|
1532
|
+
########################################################################
|
|
1533
|
+
# ignore isolated "flat" spectra on SiG
|
|
1534
|
+
########################################################################
|
|
1535
|
+
|
|
1536
|
+
if not self.settings.state.area_scan_enabled:
|
|
1537
|
+
if self.settings.is_micro() and utils.all_same(spectrum):
|
|
1538
|
+
response.error_msg = "skipping flat spectrum"
|
|
1539
|
+
response.error_lvl = ErrorLevel.low
|
|
1540
|
+
response.keep_alive = True
|
|
1541
|
+
log.debug(response.error_msg)
|
|
1542
|
+
self.queue_message("marquee_info", "sensor is stabilizing")
|
|
1543
|
+
return response
|
|
1544
|
+
|
|
1545
|
+
########################################################################
|
|
1546
|
+
# Bad Pixel Correction
|
|
1547
|
+
########################################################################
|
|
1548
|
+
|
|
1549
|
+
# Note these are pre-horizontal binning...
|
|
1550
|
+
if not self.settings.state.area_scan_enabled:
|
|
1551
|
+
if self.settings.state.bad_pixel_mode == SpectrometerState.BAD_PIXEL_MODE_AVERAGE:
|
|
1552
|
+
self._correct_bad_pixels(spectrum)
|
|
1553
|
+
|
|
1554
|
+
########################################################################
|
|
1555
|
+
# Linear Pixel Calibration (experimental)
|
|
1556
|
+
########################################################################
|
|
1557
|
+
|
|
1558
|
+
# should be done AFTER detector inversion (because that's how calibration
|
|
1559
|
+
# is generated) and AFTER bad pixel correction
|
|
1560
|
+
|
|
1561
|
+
if not self.settings.state.area_scan_enabled:
|
|
1562
|
+
if self.settings.linear_pixel_calibration:
|
|
1563
|
+
spectrum = self._apply_linear_pixel_calibration(spectrum)
|
|
1564
|
+
|
|
1565
|
+
########################################################################
|
|
1566
|
+
# horizontal binning
|
|
1567
|
+
########################################################################
|
|
1568
|
+
|
|
1569
|
+
spectrum = self._apply_horizontal_binning(spectrum)
|
|
1570
|
+
|
|
1571
|
+
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
1572
|
+
# Note: len(spectrum) may no longer == eeprom.actual_pixels_horizontal!
|
|
1573
|
+
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
1574
|
+
|
|
1575
|
+
########################################################################
|
|
1576
|
+
# Graph Alternating Pixels
|
|
1577
|
+
########################################################################
|
|
1578
|
+
|
|
1579
|
+
# When integrating new sensors, or testing "interleaved" detectors like
|
|
1580
|
+
# the InGaAs, sometimes we want to only look at "every other" pixel to
|
|
1581
|
+
# flatten-out irregularities in Bayer filters or photodiode arrays.
|
|
1582
|
+
# However, we don't want to disrupt the expected pixel-count, so just
|
|
1583
|
+
# average-over the skipped pixels.
|
|
1584
|
+
#
|
|
1585
|
+
# Not important enough to update for DetectorRegions
|
|
1586
|
+
if self.settings.state.graph_alternating_pixels:
|
|
1587
|
+
log.debug("applying graph_alternating_pixels")
|
|
1588
|
+
smoothed = []
|
|
1589
|
+
for i in range(len(spectrum)):
|
|
1590
|
+
if i % 2 == 0:
|
|
1591
|
+
smoothed.append(spectrum[i])
|
|
1592
|
+
else:
|
|
1593
|
+
if i + 1 < len(spectrum):
|
|
1594
|
+
averaged = (spectrum[i-1] + spectrum[i+1]) / 2.0
|
|
1595
|
+
else:
|
|
1596
|
+
averaged = spectrum[i - 1]
|
|
1597
|
+
smoothed.append(averaged)
|
|
1598
|
+
spectrum = smoothed
|
|
1599
|
+
|
|
1600
|
+
# Somewhat oddly, we're currently returning a TUPLE of the spectrum and
|
|
1601
|
+
# the area scan row count.
|
|
1602
|
+
response.data = SpectrumAndRow(spectrum, area_scan_row_count)
|
|
1603
|
+
return response
|
|
1604
|
+
|
|
1605
|
+
def require_throwaway(self, flag):
|
|
1606
|
+
if flag and self.settings.is_xs() and self.remaining_throwaways < 1:
|
|
1607
|
+
log.debug("queuing throwaways")
|
|
1608
|
+
self.remaining_throwaways += 1
|
|
1609
|
+
|
|
1610
|
+
def set_onboard_scans_to_average(self, n):
|
|
1611
|
+
if not self.settings.is_xs():
|
|
1612
|
+
return SpectrometerResponse(False)
|
|
1613
|
+
|
|
1614
|
+
retval = self._send_code(0xff, 0x62, n, label="SET_ONBOARD_SCANS_TO_AVERAGE")
|
|
1615
|
+
self.settings.state.scans_to_average = n
|
|
1616
|
+
self.settings.state.onboard_averaging = True
|
|
1617
|
+
return retval
|
|
1618
|
+
|
|
1619
|
+
def get_scans_to_average(self):
|
|
1620
|
+
return self._get_upper_code(0x63, lsb_len=2)
|
|
1621
|
+
|
|
1622
|
+
def set_integration_time_ms(self, ms: float):
|
|
1623
|
+
"""
|
|
1624
|
+
Send the updated integration time in a control message to the device
|
|
1625
|
+
@warning disabled EEPROM range-checking by customer
|
|
1626
|
+
request; range limits in EEPROM are defined as 16-bit
|
|
1627
|
+
values, while integration time is actually a 24-bit value,
|
|
1628
|
+
such that the EEPROM is artificially limiting our range.
|
|
1629
|
+
|
|
1630
|
+
@todo SiG needs to wait 20ms + 8 frames for stabilization.
|
|
1631
|
+
"""
|
|
1632
|
+
if not self.is_sensor_stable():
|
|
1633
|
+
log.error("declining to change sensor settings while stabilizing")
|
|
1634
|
+
return
|
|
1635
|
+
|
|
1636
|
+
ms = max(1, int(round(ms)))
|
|
1637
|
+
|
|
1638
|
+
lsw = ms & 0xffff
|
|
1639
|
+
msw = (ms >> 16) & 0x00ff
|
|
1640
|
+
|
|
1641
|
+
result = self._send_code(0xB2, lsw, msw, label="SET_INTEGRATION_TIME_MS")
|
|
1642
|
+
log.debug("SET_INTEGRATION_TIME_MS: now %d", ms)
|
|
1643
|
+
|
|
1644
|
+
self.require_throwaway(ms != self.settings.state.integration_time_ms)
|
|
1645
|
+
self.settings.state.prev_integration_time_ms = self.settings.state.integration_time_ms
|
|
1646
|
+
self.settings.state.integration_time_ms = ms
|
|
1647
|
+
|
|
1648
|
+
if self.settings.is_xs():
|
|
1649
|
+
# self.queue_message("marquee_info", "sensor is stabilizing (int time)")
|
|
1650
|
+
pass
|
|
1651
|
+
|
|
1652
|
+
return result
|
|
1653
|
+
|
|
1654
|
+
def is_sensor_stable(self):
|
|
1655
|
+
if not (self.settings.is_imx() and self.settings.supports_feature("imx_stabilization")):
|
|
1656
|
+
return True
|
|
1657
|
+
|
|
1658
|
+
self.get_poll_status()
|
|
1659
|
+
stable = PollStatus.IDLE == self.settings.state.poll_status
|
|
1660
|
+
# log.debug(f"is_sensor_stable: sensor {'IS' if stable else 'IS NOT'} stable (poll_status 0x{self.settings.state.poll_status:02x})")
|
|
1661
|
+
return stable
|
|
1662
|
+
|
|
1663
|
+
def get_poll_status(self):
|
|
1664
|
+
status = PollStatus.IDLE # .UNDEFINED (kludge)
|
|
1665
|
+
result = SpectrometerResponse(data=status)
|
|
1666
|
+
|
|
1667
|
+
if False and self.settings.is_imx() and self.settings.supports_feature("imx_stabilization"):
|
|
1668
|
+
result = self._get_code(0xd4, lsb_len=1, label="GET_POLL_STATUS")
|
|
1669
|
+
if result is not None:
|
|
1670
|
+
status = result.data
|
|
1671
|
+
self.settings.state.poll_status = status
|
|
1672
|
+
# log.debug(f"get_poll_status: status 0x{status:02x}")
|
|
1673
|
+
return result
|
|
1674
|
+
|
|
1675
|
+
def i2c_write(self, address, buf, label=""):
|
|
1676
|
+
return self._send_code(0x90, wValue=address, wIndex=len(buf), data_or_wLength=buf, label=label)
|
|
1677
|
+
|
|
1678
|
+
# ##########################################################################
|
|
1679
|
+
# Area Scan
|
|
1680
|
+
# ##########################################################################
|
|
1681
|
+
|
|
1682
|
+
def get_area_scan(self):
|
|
1683
|
+
"""
|
|
1684
|
+
@returns Reading(spectrum, AreaScanImage(data=nd_array))
|
|
1685
|
+
|
|
1686
|
+
It's important to understand that Hamamatsu and XS area scan features
|
|
1687
|
+
are architected differently in several respects.
|
|
1688
|
+
|
|
1689
|
+
@par Hamamatsu Area Scan
|
|
1690
|
+
|
|
1691
|
+
Full-frame Hamamatsu area scans are collected and returned in a single
|
|
1692
|
+
blocking call. This is possible because:
|
|
1693
|
+
|
|
1694
|
+
- the frame is only 69 lines (6% of a 1080p image)
|
|
1695
|
+
- FX2-based 110008 boards use 480 mbps High-Speed USB (40x faster than
|
|
1696
|
+
12 mbps Full-Speed)
|
|
1697
|
+
- CCDs are more sensitive as CMOS, allowing shorter integration times
|
|
1698
|
+
- Hamamatsu sensors don't have the 32ms min per-frame LVDS transfer time
|
|
1699
|
+
- Hamamatsu sensors allow vertical shifting and horizontal-readout to be
|
|
1700
|
+
delayed by the FPGA until the previous line has been pushed to the FX2,
|
|
1701
|
+
allowing every line in a frame to be sequentially read and pushed (vs
|
|
1702
|
+
IMX385 free-running frame times forcing one line/frame)
|
|
1703
|
+
- the FX2 internally generates subsequent ACQUIRE signals to the FPGA
|
|
1704
|
+
after each line has been read-out from the USB buffers to host, reducing
|
|
1705
|
+
the number of EP0 control messages
|
|
1706
|
+
|
|
1707
|
+
Due to the above, the Hamamatsu area scan can be executed much faster
|
|
1708
|
+
than an IMX385 area scan, and is considered fast enough to "block" on
|
|
1709
|
+
the full frame.
|
|
1710
|
+
|
|
1711
|
+
Therefore, a single blocking call to get_area_scan_hamamatsu() will
|
|
1712
|
+
return a complete area scan frame in roughly the following time:
|
|
1713
|
+
|
|
1714
|
+
INTEG_MS + TICK_US x LINES x (PIXELS + 1)
|
|
1715
|
+
|
|
1716
|
+
where:
|
|
1717
|
+
INTEG_MS is current integration time
|
|
1718
|
+
LINES is 69 for most Hamamatsu CCDs (128 for C0)
|
|
1719
|
+
PIXELS is 1044 or 2068 as appropriate (physical, not active)
|
|
1720
|
+
TICK_US is 2µs or 4µs as appropriate (depends on clock speed at
|
|
1721
|
+
which FPGA 'ticks' the CCD)
|
|
1722
|
+
|
|
1723
|
+
"PIXELS + 1" represents the 1 tick required for each line's vertical
|
|
1724
|
+
shift, plus PIXELS ticks for the horizontal read-out.
|
|
1725
|
+
|
|
1726
|
+
@par XS Area Scan
|
|
1727
|
+
|
|
1728
|
+
In contrast, as long as our IMX385 sensor is controlled in "main mode,"
|
|
1729
|
+
via Spartan6 with no additional DDR memory, over LVDS, the XS image scan
|
|
1730
|
+
must be read line-by-line, where each line requires a separate full-frame
|
|
1731
|
+
acquisition (32ms minimum latency).
|
|
1732
|
+
|
|
1733
|
+
Therefore, a full area scan of a 1080p detector would take AT LEAST 32ms
|
|
1734
|
+
x 1080 lines = 35sec just for LVDS readout to the FPGA. The 1080 ACQUIRE
|
|
1735
|
+
commands would stretch that out further. Over 12mbps Full-Speed USB, the
|
|
1736
|
+
entire process comes to something like 2.5 MINUTES operationally.
|
|
1737
|
+
|
|
1738
|
+
Therefore, a blocking call to get_area_scan_xs() will return ASINGLE LINE
|
|
1739
|
+
of an ONGOING area scan. This allows ENLIGHTEN to appear "responsive"
|
|
1740
|
+
during the area scan process, updating its graphical area scan image in
|
|
1741
|
+
real-time as each line is received.
|
|
1742
|
+
|
|
1743
|
+
To further speed the process, the XS FPGA will only return lines within
|
|
1744
|
+
the configured vertical ROI, so if a vertical ROI of (300, 500) is config-
|
|
1745
|
+
ured, the area scan image will only comprise 200 lines rather than the
|
|
1746
|
+
full 1080.
|
|
1747
|
+
|
|
1748
|
+
Also, since each call to this function will only return a single line,
|
|
1749
|
+
and the FPGA will automatically "roll-over" the current line index after
|
|
1750
|
+
coming to the configured stop_line (returning to the configured start_line),
|
|
1751
|
+
this function does not have a concept of "stopping" or "finishing" a frame;
|
|
1752
|
+
subsequent calls will just keep sending out additional lines, until the
|
|
1753
|
+
spectrometer is taken out of area scan mode.
|
|
1754
|
+
|
|
1755
|
+
"""
|
|
1756
|
+
if self.settings.is_ingaas():
|
|
1757
|
+
return None
|
|
1758
|
+
elif self.settings.is_xs():
|
|
1759
|
+
return self.get_area_scan_xs()
|
|
1760
|
+
else:
|
|
1761
|
+
return self.get_area_scan_hamamatsu()
|
|
1762
|
+
|
|
1763
|
+
def get_area_scan_xs(self):
|
|
1764
|
+
"""
|
|
1765
|
+
@returns Reading(spectrum, AreaScanImage(data=nd_array))
|
|
1766
|
+
"""
|
|
1767
|
+
start = self.settings.eeprom.roi_vertical_region_1_start
|
|
1768
|
+
stop = self.settings.eeprom.roi_vertical_region_1_end
|
|
1769
|
+
line_len = self.settings.pixels() * 2
|
|
1770
|
+
timeout_ms = self.generate_timeout_ms()
|
|
1771
|
+
|
|
1772
|
+
# read a line (might be the "next" line, might be the "first" or "last"
|
|
1773
|
+
# line, might have skipped a few, who knows)
|
|
1774
|
+
self._send_code(0xad, label="ACQUIRE_SPECTRUM")
|
|
1775
|
+
|
|
1776
|
+
# start with any extra data we might have picked up on the last read
|
|
1777
|
+
data = self.extra_area_scan_data
|
|
1778
|
+
self.extra_area_scan_data = []
|
|
1779
|
+
|
|
1780
|
+
try:
|
|
1781
|
+
while len(data) < line_len:
|
|
1782
|
+
bytes_remaining = line_len - len(data)
|
|
1783
|
+
latest_data = self.device_type.read(self.device, 0x82, bytes_remaining, timeout=timeout_ms)
|
|
1784
|
+
data.extend(latest_data)
|
|
1785
|
+
except:
|
|
1786
|
+
log.error(f"get_area_scan_xs: error reading line {line}", exc_info=1)
|
|
1787
|
+
return None
|
|
1788
|
+
|
|
1789
|
+
extra_len = len(data) - line_len
|
|
1790
|
+
if extra_len > 0:
|
|
1791
|
+
log.warn(f"get_area_scan_xs: storing {extra_len} extra bytes toward the next line")
|
|
1792
|
+
self.extra_area_scan_data = line_data[line_len:]
|
|
1793
|
+
|
|
1794
|
+
# demarshal line into spectrum
|
|
1795
|
+
spectrum = []
|
|
1796
|
+
for i in range(self.settings.pixels()):
|
|
1797
|
+
lsb = data[i*2 + 0]
|
|
1798
|
+
msb = data[i*2 + 1]
|
|
1799
|
+
intensity = (msb << 8) + lsb
|
|
1800
|
+
spectrum.append(intensity)
|
|
1801
|
+
spectrum = np.array(spectrum, dtype=np.uint16)
|
|
1802
|
+
|
|
1803
|
+
# first pixel is line index
|
|
1804
|
+
line_index = spectrum[0]
|
|
1805
|
+
if self.settings.supports_feature("xs_area_scan_offset_kludge"):
|
|
1806
|
+
line_index -= 40
|
|
1807
|
+
|
|
1808
|
+
# for some reason, line index is copied across first four pixels?
|
|
1809
|
+
for i in range(4):
|
|
1810
|
+
spectrum[i] = spectrum[4]
|
|
1811
|
+
|
|
1812
|
+
# update new line(s) in image
|
|
1813
|
+
max_lines = len(self.area_scan_frame)
|
|
1814
|
+
for offset in range(self.settings.state.area_scan_line_step + 1):
|
|
1815
|
+
index = line_index + offset
|
|
1816
|
+
if 0 <= index < max_lines and index <= stop:
|
|
1817
|
+
self.area_scan_frame[index] = spectrum
|
|
1818
|
+
|
|
1819
|
+
########################################################################
|
|
1820
|
+
# process completed frame
|
|
1821
|
+
########################################################################
|
|
1822
|
+
|
|
1823
|
+
# store full-frame image data for Area Scan
|
|
1824
|
+
data = np.array(self.area_scan_frame, dtype=np.float32, copy=True)
|
|
1825
|
+
data -= data.min() # remove baseline
|
|
1826
|
+
|
|
1827
|
+
# vertically bin within vertical ROI for live graph
|
|
1828
|
+
log.debug(f"vertically binning ({start}, {stop})")
|
|
1829
|
+
cropped = data[start:(stop + 1), :]
|
|
1830
|
+
spectrum = np.sum(cropped, axis=0)
|
|
1831
|
+
|
|
1832
|
+
# flag the first and last 5pixels of the current line only
|
|
1833
|
+
if 0 <= line_index < max_lines:
|
|
1834
|
+
data[line_index][:5] = data.max()
|
|
1835
|
+
data[line_index][-5:] = data.max()
|
|
1836
|
+
asi = AreaScanImage(data=data, width=len(data[0]), height=len(data), line_index=line_index)
|
|
1837
|
+
|
|
1838
|
+
# return image and spectrum in Reading
|
|
1839
|
+
reading = Reading(self.device_id)
|
|
1840
|
+
reading.spectrum = spectrum
|
|
1841
|
+
reading.area_scan_image = asi
|
|
1842
|
+
|
|
1843
|
+
return reading
|
|
1844
|
+
|
|
1845
|
+
def get_area_scan_hamamatsu(self):
|
|
1846
|
+
"""
|
|
1847
|
+
@returns Reading(spectrum, AreaScanImage(data=nd_array))
|
|
1848
|
+
"""
|
|
1849
|
+
marker = 0xffff
|
|
1850
|
+
clamp = 0xfffe
|
|
1851
|
+
lines = 69
|
|
1852
|
+
line_len = self.settings.pixels() * 2
|
|
1853
|
+
timeout_ms = self.generate_timeout_ms()
|
|
1854
|
+
|
|
1855
|
+
# request a frame
|
|
1856
|
+
self._send_code(0xad, label="ACQUIRE_SPECTRUM")
|
|
1857
|
+
|
|
1858
|
+
# read frame line-by-line
|
|
1859
|
+
line_count = 0
|
|
1860
|
+
for line in range(lines):
|
|
1861
|
+
prefix = f"line {line_count}"
|
|
1862
|
+
|
|
1863
|
+
data = self.extra_area_scan_data # start with any extra data we might have picked up on the last read
|
|
1864
|
+
self.extra_area_scan_data = []
|
|
1865
|
+
|
|
1866
|
+
try:
|
|
1867
|
+
while len(data) < line_len:
|
|
1868
|
+
bytes_remaining = line_len - len(data)
|
|
1869
|
+
# @todo expand this to 0x86 for S16011-1101
|
|
1870
|
+
latest_data = self.device_type.read(self.device, 0x82, bytes_remaining, timeout=timeout_ms)
|
|
1871
|
+
data.extend(latest_data)
|
|
1872
|
+
except:
|
|
1873
|
+
log.error(f"get_area_scan_hamamatsu {prefix}: error reading line {line}", exc_info=1)
|
|
1874
|
+
break
|
|
1875
|
+
|
|
1876
|
+
extra_len = len(data) - line_len
|
|
1877
|
+
if extra_len > 0:
|
|
1878
|
+
log.warn(f"get_area_scan_hamamatsu {prefix}: storing {extra_len} extra bytes toward the next line")
|
|
1879
|
+
self.extra_area_scan_data = line_data[line_len:]
|
|
1880
|
+
|
|
1881
|
+
# demarshal line into spectrum
|
|
1882
|
+
spectrum = []
|
|
1883
|
+
for i in range(self.settings.pixels()):
|
|
1884
|
+
lsb = data[i*2 + 0]
|
|
1885
|
+
msb = data[i*2 + 1]
|
|
1886
|
+
intensity = (msb << 8) + lsb
|
|
1887
|
+
spectrum.append(intensity)
|
|
1888
|
+
|
|
1889
|
+
# first pixel is start-of-line marker
|
|
1890
|
+
if spectrum[0] != marker:
|
|
1891
|
+
log.warn(f"get_area_scan_hamamatsu {prefix}: first pixel expected 0x{marker:04x}, found 0x{spectrum[0]:04x}")
|
|
1892
|
+
spectrum[0] = spectrum[2] # stomp
|
|
1893
|
+
|
|
1894
|
+
# verify rest of line is clamped to 0xfffe
|
|
1895
|
+
for i, intensity in enumerate(spectrum):
|
|
1896
|
+
if i > 1 and spectrum[i] > clamp:
|
|
1897
|
+
log.warn(f"get_area_scan_hamamatsu {prefix}: WARNING: pixel {i:4d} value 0x{spectrum[i]:04x} exceeds clamp 0x{clamp:04x}")
|
|
1898
|
+
|
|
1899
|
+
# second pixel is line index
|
|
1900
|
+
line_index = spectrum[1]
|
|
1901
|
+
if line_index >= lines:
|
|
1902
|
+
log.warn(f"get_area_scan_hamamatsu {prefix}: WARNING: line_index {line_index} exceeds frame limit")
|
|
1903
|
+
spectrum[1] = spectrum[2] # stomp
|
|
1904
|
+
|
|
1905
|
+
# store line in image
|
|
1906
|
+
self.area_scan_frame[line_index] = spectrum
|
|
1907
|
+
# self.debug(f"{prefix}: stored line {line_index}: {spectrum[:5]}")
|
|
1908
|
+
|
|
1909
|
+
line_count += 1
|
|
1910
|
+
|
|
1911
|
+
########################################################################
|
|
1912
|
+
# process completed frame
|
|
1913
|
+
########################################################################
|
|
1914
|
+
|
|
1915
|
+
# store full-frame image data for Area Scan
|
|
1916
|
+
data = np.array(self.area_scan_frame, dtype=np.float32, copy=True)
|
|
1917
|
+
data -= data.min() # remove baseline
|
|
1918
|
+
asi = AreaScanImage(data=data, width=len(data[0]), height=len(data))
|
|
1919
|
+
|
|
1920
|
+
# vertically bin within vertical ROI for live graph
|
|
1921
|
+
start = self.settings.eeprom.roi_vertical_region_1_start
|
|
1922
|
+
stop = self.settings.eeprom.roi_vertical_region_1_end
|
|
1923
|
+
cropped = data[start:(stop + 1), :]
|
|
1924
|
+
spectrum = np.sum(cropped, axis=0)
|
|
1925
|
+
|
|
1926
|
+
# return image and spectrum in Reading
|
|
1927
|
+
reading = Reading(self.device_id)
|
|
1928
|
+
reading.spectrum = spectrum
|
|
1929
|
+
reading.area_scan_image = asi
|
|
1930
|
+
|
|
1931
|
+
return reading
|
|
1932
|
+
|
|
1933
|
+
# ##########################################################################
|
|
1934
|
+
# ADCs / DACs
|
|
1935
|
+
# ##########################################################################
|
|
1936
|
+
|
|
1937
|
+
# This is a little confusing, so:
|
|
1938
|
+
#
|
|
1939
|
+
# The "primary" ADC (ADC 0) is assumed to return laser temperature.
|
|
1940
|
+
#
|
|
1941
|
+
# The "secondary" ADC (ADC 1) may be wired to various other things for
|
|
1942
|
+
# legacy or OEM systems. For instance, it has been used with an integrated
|
|
1943
|
+
# photodiode for real-time measurement of laser output power (with or
|
|
1944
|
+
# without a calibration to convert the photodiode result to mW, hence
|
|
1945
|
+
# get_secondary_adc_calibrated).
|
|
1946
|
+
#
|
|
1947
|
+
# Most users will never need to explicitly call select_adc, and can simply
|
|
1948
|
+
# call get_laser_temperature_raw and/or get_laser_temperature_degC.
|
|
1949
|
+
|
|
1950
|
+
def get_dac(self, dacIndex: int = 0):
|
|
1951
|
+
return self._get_code(0xd9, wIndex=dacIndex, label="GET_DAC", lsb_len=2)
|
|
1952
|
+
|
|
1953
|
+
def select_adc(self, n: int):
|
|
1954
|
+
log.debug("select_adc -> %d", n)
|
|
1955
|
+
self.settings.state.selected_adc = n
|
|
1956
|
+
result = self._send_code(0xed, n, label="SELECT_ADC")
|
|
1957
|
+
self._get_code(0xd5, wLength=2, label="GET_ADC (throwaway)") # stabilization read
|
|
1958
|
+
return result
|
|
1959
|
+
|
|
1960
|
+
def get_secondary_adc_calibrated(self, raw: float = None):
|
|
1961
|
+
if not self.has_linearity_coeffs():
|
|
1962
|
+
log.debug("secondary_adc_calibrated: no calibration")
|
|
1963
|
+
return SpectrometerResponse(data=None, error_lvl=ErrorLevel.low, error_msg="secondary_adc_calibrated: no calibration")
|
|
1964
|
+
|
|
1965
|
+
if raw is None:
|
|
1966
|
+
response = self.get_secondary_adc_raw()
|
|
1967
|
+
if response.data is None:
|
|
1968
|
+
return response
|
|
1969
|
+
|
|
1970
|
+
raw = float(response.data)
|
|
1971
|
+
|
|
1972
|
+
# use the first 4 linearity coefficients as a 3rd-order polynomial
|
|
1973
|
+
calibrated = float(self.settings.eeprom.linearity_coeffs[0]) \
|
|
1974
|
+
+ float(self.settings.eeprom.linearity_coeffs[1]) * raw \
|
|
1975
|
+
+ float(self.settings.eeprom.linearity_coeffs[2]) * raw * raw \
|
|
1976
|
+
+ float(self.settings.eeprom.linearity_coeffs[3]) * raw * raw * raw
|
|
1977
|
+
log.debug("secondary_adc_calibrated: %f", calibrated)
|
|
1978
|
+
response.data = calibrated
|
|
1979
|
+
return response
|
|
1980
|
+
|
|
1981
|
+
def get_secondary_adc_raw(self):
|
|
1982
|
+
# flip to secondary ADC if needed
|
|
1983
|
+
if self.settings.state.selected_adc is None or self.settings.state.selected_adc != 1:
|
|
1984
|
+
self.select_adc(1)
|
|
1985
|
+
|
|
1986
|
+
result = self._get_code(0xd5, wLength=2, label="GET_ADC", lsb_len=2)
|
|
1987
|
+
log.debug("secondary_adc_raw: 0x%04x", result)
|
|
1988
|
+
return result
|
|
1989
|
+
|
|
1990
|
+
# ##########################################################################
|
|
1991
|
+
# Laser Temperature
|
|
1992
|
+
# ##########################################################################
|
|
1993
|
+
|
|
1994
|
+
def set_laser_tec_mode(self, mode):
|
|
1995
|
+
if not self.settings.is_xs():
|
|
1996
|
+
msg = "Laser TEC mode only applicable on XS"
|
|
1997
|
+
log.error(msg)
|
|
1998
|
+
return SpectrometerResponse(data=None, error_lvl=ErrorLevel.low, error_msg=msg)
|
|
1999
|
+
|
|
2000
|
+
if isinstance(mode, str):
|
|
2001
|
+
mode = mode.upper()
|
|
2002
|
+
if mode == "OFF":
|
|
2003
|
+
mode = 0
|
|
2004
|
+
elif mode == "ON":
|
|
2005
|
+
mode = 1
|
|
2006
|
+
elif mode == "AUTO":
|
|
2007
|
+
mode = 2
|
|
2008
|
+
elif mode == "AUTO_ON":
|
|
2009
|
+
mode = 3
|
|
2010
|
+
else:
|
|
2011
|
+
mode = 0
|
|
2012
|
+
|
|
2013
|
+
mode = min(3, max(0, int(mode)))
|
|
2014
|
+
self.settings.state.laser_tec_mode = mode
|
|
2015
|
+
return self._send_code(0x84, mode, label="SET_LASER_TEC_MODE")
|
|
2016
|
+
|
|
2017
|
+
def get_laser_tec_mode(self):
|
|
2018
|
+
"""
|
|
2019
|
+
The current opcode (0x85) seems to be actually returning
|
|
2020
|
+
GET_LASER_TEC_ENABLED, not GET_LASER_TEC_MODE.
|
|
2021
|
+
"""
|
|
2022
|
+
if not self.settings.is_xs():
|
|
2023
|
+
msg = "Laser TEC mode only applicable on XS"
|
|
2024
|
+
log.error(msg)
|
|
2025
|
+
return SpectrometerResponse(data=None, error_lvl=ErrorLevel.low, error_msg=msg)
|
|
2026
|
+
|
|
2027
|
+
result = self._get_code(0x85, wLength=1, label="GET_LASER_TEC_MODE", lsb_len=1)
|
|
2028
|
+
if result is None or result.data < 0 or result.data > 3:
|
|
2029
|
+
msg = f"get_laser_tec_mode: invalid mode {result}"
|
|
2030
|
+
log.error(msg)
|
|
2031
|
+
return SpectrometerResponse(data=None, error_lvl=ErrorLevel.low, error_msg=msg)
|
|
2032
|
+
|
|
2033
|
+
# mode = result.data
|
|
2034
|
+
# names = [ 'OFF', 'ON', 'AUTO', 'AUTO_ON' ]
|
|
2035
|
+
# log.debug(f"get_laser_tec_mode: mode {names[mode]}")
|
|
2036
|
+
# self.settings.state.laser_tec_mode = mode
|
|
2037
|
+
|
|
2038
|
+
enabled = 0 != result.data
|
|
2039
|
+
# log.debug(f"get_laser_tec_mode: enabled {enabled}")
|
|
2040
|
+
self.settings.state.laser_tec_enabled = enabled
|
|
2041
|
+
|
|
2042
|
+
return result
|
|
2043
|
+
|
|
2044
|
+
##
|
|
2045
|
+
# Laser temperature conversion doesn't use EEPROM coeffs at all.
|
|
2046
|
+
#
|
|
2047
|
+
# @par IPS
|
|
2048
|
+
#
|
|
2049
|
+
# Some Wasatch SML products use an IPS Wavelength-Stabilized TO-56
|
|
2050
|
+
# laser, which internally uses a Betatherm 10K3CG3 thermistor.
|
|
2051
|
+
#
|
|
2052
|
+
# @see https://www.ipslasers.com/data-sheets/SM-TO-56-Data-Sheet-IPS.pdf
|
|
2053
|
+
#
|
|
2054
|
+
# The official IPS conversion from thermistor resistance (in ohms) to degC is:
|
|
2055
|
+
#
|
|
2056
|
+
# \verbatim
|
|
2057
|
+
# 1 / ( C1
|
|
2058
|
+
# + C2 * ln(ohms)
|
|
2059
|
+
# + C3 * pow(ln(ohms), 3)
|
|
2060
|
+
# )
|
|
2061
|
+
# - 273.15
|
|
2062
|
+
#
|
|
2063
|
+
# Where: C1 = 0.00113
|
|
2064
|
+
# C2 = 0.000234
|
|
2065
|
+
# C3 = 8.78e-8
|
|
2066
|
+
# \endverbatim
|
|
2067
|
+
#
|
|
2068
|
+
# That said, on XS the IPS thermistor then goes through a MAX1978ETM-T
|
|
2069
|
+
# TEC IC which buffers the thermistor voltage. This requires the additional
|
|
2070
|
+
# empirically-determined static conversion captured below.
|
|
2071
|
+
#
|
|
2072
|
+
# Regrettably, the laser thermistor voltage on 110280 boards
|
|
2073
|
+
# overflows the ADC range, so we don't currently have valid laser
|
|
2074
|
+
# temperature read-out with those PCBs.
|
|
2075
|
+
#
|
|
2076
|
+
# @par Ondax MML
|
|
2077
|
+
#
|
|
2078
|
+
# On MML units using the Ondax OEM Module, the cable to the 220060
|
|
2079
|
+
# does not seem to pass-through the thermistor pin, so we don't have
|
|
2080
|
+
# laser temperature read-out on those, either. (Nor photodiode...)
|
|
2081
|
+
#
|
|
2082
|
+
# @par RealLight
|
|
2083
|
+
#
|
|
2084
|
+
# RealLight SML thermistors use the following resistance and beta:
|
|
2085
|
+
#
|
|
2086
|
+
# Thermistor Rt (kΩ / β(25°C)): 10±5% / 3450
|
|
2087
|
+
#
|
|
2088
|
+
# Therefore you can convert the raw thermistor ADC reading to degrees Celsius:
|
|
2089
|
+
#
|
|
2090
|
+
# 1. Convert raw ADC to voltage:
|
|
2091
|
+
#
|
|
2092
|
+
# voltage = 2.5 * raw / 4096
|
|
2093
|
+
#
|
|
2094
|
+
# 2. Convert voltage to resistance (assuming 2.5v reference)
|
|
2095
|
+
#
|
|
2096
|
+
# resistance = 21450.0 * voltage / (2.5 - voltage)
|
|
2097
|
+
#
|
|
2098
|
+
# 3. Convert resistance to Kelvin:
|
|
2099
|
+
#
|
|
2100
|
+
# 1/T = (1/T0) + (1/β) * ln(R/R0)
|
|
2101
|
+
# 1 = T((1/T0) + (1/β) * ln(R/10))
|
|
2102
|
+
# T = 1/((1/T0) + (1/β) * ln(R/10))
|
|
2103
|
+
#
|
|
2104
|
+
# Where:
|
|
2105
|
+
#
|
|
2106
|
+
# T: is the temperature in Kelvin
|
|
2107
|
+
# T0: is a reference temperature (usually 25°C or 298.15K)
|
|
2108
|
+
# β: is the beta value of the thermistor (a constant indicating the relationship between resistance and temperature)
|
|
2109
|
+
# R: is the measured resistance of the thermistor
|
|
2110
|
+
# R0: is the resistance of the thermistor at the reference temperature T0
|
|
2111
|
+
#
|
|
2112
|
+
# Kelvin = 1/((1/298.15) + (1/3450) * ln(resistance/10))
|
|
2113
|
+
#
|
|
2114
|
+
# 4. Convert Kelvin to degrees Celsius:
|
|
2115
|
+
#
|
|
2116
|
+
# degC = Kelvin - 273.15
|
|
2117
|
+
#
|
|
2118
|
+
# @param raw the value read from the thermistor's 12-bit ADC (can be
|
|
2119
|
+
# uint12 or SpectrometerResponse)
|
|
2120
|
+
def get_laser_temperature_degC(self, raw=None):
|
|
2121
|
+
if isinstance(raw, SpectrometerResponse):
|
|
2122
|
+
raw = raw.data
|
|
2123
|
+
if raw is None:
|
|
2124
|
+
raw = self.get_laser_temperature_raw()
|
|
2125
|
+
|
|
2126
|
+
if raw is None:
|
|
2127
|
+
msg="get_laser_temperature_degC: error reading raw laser temperature"
|
|
2128
|
+
log.error(msg)
|
|
2129
|
+
return SpectrometerResponse(error_lvl=ErrorLevel.low, error_msg=msg)
|
|
2130
|
+
|
|
2131
|
+
if raw > 0xfff:
|
|
2132
|
+
msg = f"get_laser_temperature_degC: raw value 0x{raw:x} exceeds 12 bits"
|
|
2133
|
+
log.error(msg)
|
|
2134
|
+
return SpectrometerResponse(data=None, error_lvl=ErrorLevel.low, error_msg=msg)
|
|
2135
|
+
|
|
2136
|
+
if raw == 0:
|
|
2137
|
+
msg = "get_laser_temperature_degC: can't take log of raw ADC value 0"
|
|
2138
|
+
# log.debug(msg)
|
|
2139
|
+
return SpectrometerResponse(data=None) # not propogating error_msg for now
|
|
2140
|
+
|
|
2141
|
+
degC = 0
|
|
2142
|
+
try:
|
|
2143
|
+
if self.settings.is_xs():
|
|
2144
|
+
# conversion for 220250 Rev4 MAX1978ETM-T 3V3 buffer -> 12-bit DAC -> degC
|
|
2145
|
+
coeffs = [ 1.5712971947853123e+000,
|
|
2146
|
+
1.4453391889061071e-002,
|
|
2147
|
+
-1.8534086153440592e-006,
|
|
2148
|
+
4.2553356470494626e-010 ]
|
|
2149
|
+
for i, coeff in enumerate(coeffs):
|
|
2150
|
+
degC += coeff * pow(raw, i)
|
|
2151
|
+
# log.debug(f"get_laser_temperature_degC: degC {degC:g} after adding {coeff} x (pow({raw}, {i}) = {pow(raw, i):g})")
|
|
2152
|
+
else:
|
|
2153
|
+
# when not using TEC IC
|
|
2154
|
+
voltage = 2.5 * raw / 4096
|
|
2155
|
+
resistance = 21450.0 * voltage / (2.5 - voltage) # LB confirmed
|
|
2156
|
+
|
|
2157
|
+
if resistance < 0:
|
|
2158
|
+
msg="get_laser_temperature_degC: can't compute degC: raw = 0x%04x, voltage = %f, resistance = %f" % (
|
|
2159
|
+
raw, voltage, resistance)
|
|
2160
|
+
log.error(msg)
|
|
2161
|
+
return SpectrometerResponse(data=None, error_level=ErrorLevel.low, error_msg=msg)
|
|
2162
|
+
|
|
2163
|
+
logVal = math.log(resistance / 10000.0)
|
|
2164
|
+
insideMain = logVal + 3977.0 / (25 + 273.0)
|
|
2165
|
+
degC = 3977.0 / insideMain - 273.0
|
|
2166
|
+
|
|
2167
|
+
log.debug("Laser temperature: %.2f deg C (0x%04x raw)" % (degC, raw))
|
|
2168
|
+
except:
|
|
2169
|
+
msg = "exception computing laser temperature"
|
|
2170
|
+
log.error(msg, exc_info=1)
|
|
2171
|
+
return SpectrometerResponse(data=None, error_level=ErrorLevel.low, error_msg=msg)
|
|
2172
|
+
|
|
2173
|
+
return SpectrometerResponse(data=degC)
|
|
2174
|
+
|
|
2175
|
+
# ##########################################################################
|
|
2176
|
+
# Detector Temperature
|
|
2177
|
+
# ##########################################################################
|
|
2178
|
+
|
|
2179
|
+
## @note big-endian, reverse of get_laser_temperature_raw
|
|
2180
|
+
def get_detector_temperature_raw(self):
|
|
2181
|
+
return self._get_code(0xd7, label="GET_CCD_TEMP", msb_len=2)
|
|
2182
|
+
|
|
2183
|
+
def get_detector_temperature_degC(self, raw: float = None):
|
|
2184
|
+
if raw is None:
|
|
2185
|
+
raw = self.get_detector_temperature_raw().data
|
|
2186
|
+
|
|
2187
|
+
if raw is None:
|
|
2188
|
+
return raw
|
|
2189
|
+
|
|
2190
|
+
degC = self.settings.eeprom.adc_to_degC_coeffs[0] \
|
|
2191
|
+
+ self.settings.eeprom.adc_to_degC_coeffs[1] * raw \
|
|
2192
|
+
+ self.settings.eeprom.adc_to_degC_coeffs[2] * raw * raw
|
|
2193
|
+
log.debug("Detector temperature: %.2f deg C (0x%04x raw)" % (degC, raw))
|
|
2194
|
+
return SpectrometerResponse(data=degC)
|
|
2195
|
+
|
|
2196
|
+
def set_detector_tec_setpoint_degC(self, degC: float):
|
|
2197
|
+
"""
|
|
2198
|
+
Attempt to set the CCD cooler setpoint. Verify that it is within an
|
|
2199
|
+
acceptable range. Ideally this is to prevent condensation and other
|
|
2200
|
+
issues. This value is a default and is hugely dependent on the
|
|
2201
|
+
environmental conditions.
|
|
2202
|
+
"""
|
|
2203
|
+
if not self.settings.eeprom.has_cooling:
|
|
2204
|
+
log.error("unable to control TEC: EEPROM reports no cooling")
|
|
2205
|
+
return SpectrometerResponse(data=False, error_lvl=ErrorLevel.low, error_msg="unable to control TEC: EEPROM reports no cooling")
|
|
2206
|
+
|
|
2207
|
+
if degC < self.settings.eeprom.min_temp_degC:
|
|
2208
|
+
log.critical("set_detector_tec_setpoint_degC: setpoint %f below min %f", degC, self.settings.eeprom.min_temp_degC)
|
|
2209
|
+
return SpectrometerResponse(data=False, error_lvl=ErrorLevel.low, error_msg="setpoint below minimum")
|
|
2210
|
+
|
|
2211
|
+
if degC > self.settings.eeprom.max_temp_degC:
|
|
2212
|
+
log.critical("set_detector_tec_setpoint_degC: setpoint %f exceeds max %f", degC, self.settings.eeprom.max_temp_degC)
|
|
2213
|
+
return SpectrometerResponse(data=False, error_lvl=ErrorLevel.low, error_msg="setpoint beyond max")
|
|
2214
|
+
|
|
2215
|
+
raw = int(round(self.settings.eeprom.degC_to_dac_coeffs[0]
|
|
2216
|
+
+ self.settings.eeprom.degC_to_dac_coeffs[1] * degC
|
|
2217
|
+
+ self.settings.eeprom.degC_to_dac_coeffs[2] * degC * degC))
|
|
2218
|
+
|
|
2219
|
+
# ROUND (don't mask) to 12-bit DAC
|
|
2220
|
+
raw = max(0, min(raw, 0xfff))
|
|
2221
|
+
|
|
2222
|
+
log.debug("Set CCD TEC Setpoint: %.2f deg C (raw ADC 0x%04x)", degC, raw)
|
|
2223
|
+
ok = self._send_code(0xd8, raw, label="SET_DETECTOR_TEC_SETPOINT")
|
|
2224
|
+
self.settings.state.tec_setpoint_degC = degC
|
|
2225
|
+
self.detector_tec_setpoint_has_been_set = True
|
|
2226
|
+
return ok
|
|
2227
|
+
|
|
2228
|
+
def get_detector_tec_setpoint_degC(self):
|
|
2229
|
+
if self.detector_tec_setpoint_has_been_set:
|
|
2230
|
+
return SpectrometerResponse(self.settings.state.tec_setpoint_degC)
|
|
2231
|
+
log.error("Detector TEC setpoint has not yet been applied")
|
|
2232
|
+
return SpectrometerResponse(data=0.0)
|
|
2233
|
+
|
|
2234
|
+
def get_detector_tec_setpoint_raw(self):
|
|
2235
|
+
return self.get_dac(0)
|
|
2236
|
+
|
|
2237
|
+
## @todo rename set_detector_tec_enable
|
|
2238
|
+
def set_tec_enable(self, flag: bool):
|
|
2239
|
+
if not self.settings.eeprom.has_cooling:
|
|
2240
|
+
log.debug("unable to control TEC: EEPROM reports no cooling")
|
|
2241
|
+
return SpectrometerResponse(data=False, error_msg="unable to control TEC: EEPROM reports no cooling")
|
|
2242
|
+
|
|
2243
|
+
if not self.settings.eeprom.has_detector_tec_calibration():
|
|
2244
|
+
log.debug("unable to control TEC: EEPROM missing valid TEC calibration")
|
|
2245
|
+
return SpectrometerResponse(data=False, error_msg="unable to control TEC: EEPROM missing valid TEC calibration")
|
|
2246
|
+
|
|
2247
|
+
value = 1 if flag else 0
|
|
2248
|
+
|
|
2249
|
+
if not self.detector_tec_setpoint_has_been_set:
|
|
2250
|
+
|
|
2251
|
+
# @todo should this not be eeprom.startup_temp_degC
|
|
2252
|
+
log.debug("defaulting TEC setpoint to min %s", self.settings.eeprom.min_temp_degC)
|
|
2253
|
+
self.set_detector_tec_setpoint_degC(self.settings.eeprom.min_temp_degC)
|
|
2254
|
+
|
|
2255
|
+
log.debug("Send detector TEC enable: %s", value)
|
|
2256
|
+
ok = self._send_code(0xd6, value, label="SET_DETECTOR_TEC_ENABLE")
|
|
2257
|
+
if ok.data:
|
|
2258
|
+
self.settings.state.tec_enabled = flag
|
|
2259
|
+
return ok
|
|
2260
|
+
|
|
2261
|
+
# ##########################################################################
|
|
2262
|
+
# Triggering
|
|
2263
|
+
# ##########################################################################
|
|
2264
|
+
|
|
2265
|
+
def set_trigger_source(self, value: bool):
|
|
2266
|
+
"""
|
|
2267
|
+
Set the source for incoming acquisition triggers.
|
|
2268
|
+
@param value either 0 for "internal" or 1 for "external"
|
|
2269
|
+
With internal triggering (the default), the spectrometer expects the
|
|
2270
|
+
USB host to explicitly send a START_ACQUISITION (ACQUIRE) opcode to
|
|
2271
|
+
begin each integration. In external triggering, the spectrometer
|
|
2272
|
+
waits for the rising edge on a signal connected to a pin on the OEM
|
|
2273
|
+
accessory connector.
|
|
2274
|
+
Technically on ARM, the microcontroller is continuously monitoring
|
|
2275
|
+
both the external pin and listening for internal software opcodes.
|
|
2276
|
+
On the FX2 you need to explicitly place the microcontroller into
|
|
2277
|
+
external triggering mode to avail the feature.
|
|
2278
|
+
"""
|
|
2279
|
+
self.settings.state.trigger_source = value
|
|
2280
|
+
log.debug("trigger_source now %s", value)
|
|
2281
|
+
|
|
2282
|
+
# Don't send the opcode on ARM. See issue #2 on WasatchUSB project
|
|
2283
|
+
if self.settings.is_arm():
|
|
2284
|
+
return SpectrometerResponse(data=False)
|
|
2285
|
+
|
|
2286
|
+
msb = 0
|
|
2287
|
+
lsb = value
|
|
2288
|
+
buf = [0] * 8
|
|
2289
|
+
|
|
2290
|
+
# MZ: this is weird...we're sending the buffer on an FX2-only command
|
|
2291
|
+
return self._send_code(0xd2, lsb, msb, buf, label="SET_TRIGGER_SOURCE")
|
|
2292
|
+
|
|
2293
|
+
# ##########################################################################
|
|
2294
|
+
# High-Gain Mode
|
|
2295
|
+
# ##########################################################################
|
|
2296
|
+
|
|
2297
|
+
##
|
|
2298
|
+
# CF_SELECT is configured using bit 2 of the FPGA configuration register
|
|
2299
|
+
# 0x12. This bit can be set using vendor commands 0xeb to SET and 0xec
|
|
2300
|
+
# to GET. Note that the set command is expecting a 5-byte unsigned
|
|
2301
|
+
# value, the highest byte of which we pass as part of an 8-byte buffer.
|
|
2302
|
+
# Not sure why.
|
|
2303
|
+
def set_high_gain_mode_enable(self, flag: bool):
|
|
2304
|
+
log.debug("Set high gain mode: %s", flag)
|
|
2305
|
+
if not self.settings.is_ingaas():
|
|
2306
|
+
log.debug("SET_HIGH_GAIN_MODE_ENABLE only supported on InGaAs")
|
|
2307
|
+
return SpectrometerResponse(data=False, error_msg="High Gain not support on this spectrometer")
|
|
2308
|
+
|
|
2309
|
+
value = 1 if flag else 0
|
|
2310
|
+
|
|
2311
|
+
# this is done automatically on ARM, but for this opcode we do it on FX2 as well
|
|
2312
|
+
buf = [0] * 8
|
|
2313
|
+
|
|
2314
|
+
self.settings.state.high_gain_mode_enabled = flag
|
|
2315
|
+
return self._send_code(0xeb, wValue=value, wIndex=0, data_or_wLength=buf, label="SET_HIGH_GAIN_MODE_ENABLE")
|
|
2316
|
+
|
|
2317
|
+
def get_high_gain_mode_enabled(self):
|
|
2318
|
+
if not self.settings.is_ingaas():
|
|
2319
|
+
log.debug("GET_HIGH_GAIN_MODE_ENABLE only supported on InGaAs")
|
|
2320
|
+
return SpectrometerResponse(self.settings.eeprom.high_gain_mode_enabled)
|
|
2321
|
+
|
|
2322
|
+
self.settings.state.high_gain_mode_enabled = 0 != self._get_code(0xec, lsb_len=1, label="GET_HIGH_GAIN_MODE_ENABLED")
|
|
2323
|
+
return SpectrometerResponse(self.settings.state.high_gain_mode_enabled)
|
|
2324
|
+
|
|
2325
|
+
############################################################################
|
|
2326
|
+
# Laser commands
|
|
2327
|
+
############################################################################
|
|
2328
|
+
|
|
2329
|
+
def get_opt_laser_control(self):
|
|
2330
|
+
return self.get_upper_code(0x09, label="GET_OPT_LASER_CONTROL", msb_len=1)
|
|
2331
|
+
|
|
2332
|
+
def get_opt_has_laser(self):
|
|
2333
|
+
value = self.get_upper_code(0x08, label="GET_OPT_HAS_LASER", msb_len=1).data
|
|
2334
|
+
available = 0 != value
|
|
2335
|
+
if available != self.settings.eeprom.has_laser:
|
|
2336
|
+
log.error("OPT_HAS_LASER opcode result %s != EEPROM has_laser %s (using opcode)",
|
|
2337
|
+
value, self.settings.eeprom.has_laser)
|
|
2338
|
+
return SpectrometerResponse(data=available)
|
|
2339
|
+
|
|
2340
|
+
def get_laser_temperature_raw(self):
|
|
2341
|
+
""" @note little-endian, reverse of get_detector_temperature_raw """
|
|
2342
|
+
# flip to primary ADC if needed
|
|
2343
|
+
if self.settings.state.selected_adc is None or self.settings.state.selected_adc != 0:
|
|
2344
|
+
# MZ: unclear if this is needed on SiG
|
|
2345
|
+
self.select_adc(0)
|
|
2346
|
+
|
|
2347
|
+
result = self._get_code(0xd5, wLength=2, label="GET_ADC", lsb_len=2)
|
|
2348
|
+
if not result.data:
|
|
2349
|
+
log.debug("Unable to read laser temperature")
|
|
2350
|
+
return SpectrometerResponse(0)
|
|
2351
|
+
result.data = result.data & 0xfff # clamp to 12-bit
|
|
2352
|
+
return result
|
|
2353
|
+
|
|
2354
|
+
def set_selected_laser(self, value: int):
|
|
2355
|
+
"""
|
|
2356
|
+
On spectrometers supporting two lasers, select the primary (0) or
|
|
2357
|
+
secondary (1). Laser Enable, laser power etc should all then
|
|
2358
|
+
affect the currently-selected laser.
|
|
2359
|
+
@warning conflicts with GET_RAMAN_MODE_ENABLE
|
|
2360
|
+
"""
|
|
2361
|
+
if not self.settings.eeprom.has_laser:
|
|
2362
|
+
msg = "no laser installed"
|
|
2363
|
+
log.error(msg)
|
|
2364
|
+
return SpectrometerResponse(data=False, error_msg=msg)
|
|
2365
|
+
|
|
2366
|
+
n = 1 if value else 0
|
|
2367
|
+
log.debug("selecting laser %d", n)
|
|
2368
|
+
self.settings.state.selected_laser = n
|
|
2369
|
+
|
|
2370
|
+
return self._send_code(bRequest = 0xff,
|
|
2371
|
+
wValue = 0x15,
|
|
2372
|
+
wIndex = n,
|
|
2373
|
+
data_or_wLength = [0] * 8,
|
|
2374
|
+
label = "SET_SELECTED_LASER")
|
|
2375
|
+
|
|
2376
|
+
def get_selected_laser(self):
|
|
2377
|
+
return SpectrometerResponse(data=self.settings.state.selected_laser)
|
|
2378
|
+
|
|
2379
|
+
def get_laser_enabled(self):
|
|
2380
|
+
if self.settings.is_xs():
|
|
2381
|
+
# MZ: get_laser_enabled not currently robust on SiG :-(
|
|
2382
|
+
return SpectrometerResponse(data=self.settings.state.laser_enabled)
|
|
2383
|
+
|
|
2384
|
+
res = self._get_code(0xe2, label="GET_LASER_ENABLED", msb_len=1)
|
|
2385
|
+
flag = 0 != res.data
|
|
2386
|
+
|
|
2387
|
+
log.debug(f"get_laser_enabled: {flag} (storing to {self.settings.state})")
|
|
2388
|
+
self.settings.state.laser_enabled = flag
|
|
2389
|
+
return SpectrometerResponse(data=flag)
|
|
2390
|
+
|
|
2391
|
+
def set_laser_enable(self, flag: bool):
|
|
2392
|
+
"""
|
|
2393
|
+
Turn the laser on or off.
|
|
2394
|
+
If laser power hasn't yet been externally configured, applies the default
|
|
2395
|
+
of full-power. The new state will be applied immediately.
|
|
2396
|
+
@param flag (Input) bool (True turns laser on, False turns laser off)
|
|
2397
|
+
@returns whether the new state was applied
|
|
2398
|
+
"""
|
|
2399
|
+
if not self.settings.eeprom.has_laser:
|
|
2400
|
+
msg = "no laser installed"
|
|
2401
|
+
log.error(msg)
|
|
2402
|
+
return SpectrometerResponse(data=False, error_msg=msg)
|
|
2403
|
+
|
|
2404
|
+
# ARM seems to require that laser power be set before the laser is enabled
|
|
2405
|
+
if self.next_applied_laser_power is None:
|
|
2406
|
+
self.set_laser_power_perc(100.0)
|
|
2407
|
+
|
|
2408
|
+
self.settings.state.laser_enabled = flag
|
|
2409
|
+
self._set_laser_enable_immediate(flag)
|
|
2410
|
+
return SpectrometerResponse(data=True)
|
|
2411
|
+
|
|
2412
|
+
def _set_laser_enable_immediate(self, flag: bool):
|
|
2413
|
+
"""
|
|
2414
|
+
The user has requested to update the laser firing state (on or off),
|
|
2415
|
+
so apply the new laser state to the spectrometer immediately.
|
|
2416
|
+
|
|
2417
|
+
Because the ability to immediately disable a laser is a safety-related
|
|
2418
|
+
feature (noting that truly safety-critical capabilities should be
|
|
2419
|
+
implemented in hardware, and generally can't be robustly achieved through
|
|
2420
|
+
Python scripts), this function takes the unusual step of looping over
|
|
2421
|
+
multiple attempts to set the laser state until either the command succeeds,
|
|
2422
|
+
or 3 consecutive failures have occured.
|
|
2423
|
+
|
|
2424
|
+
This behavior was added after a developmental, unreleased prototype was
|
|
2425
|
+
found to occasionally drop USB packets, and was therefore susceptible to
|
|
2426
|
+
inadvertently failing to disable the laser upon command.
|
|
2427
|
+
|
|
2428
|
+
@private (as callers are recommended to use set_laser_enable)
|
|
2429
|
+
@param flag (Input) whether the laser should be on (true) or off (false)
|
|
2430
|
+
@returns true if new state was successfully applied
|
|
2431
|
+
"""
|
|
2432
|
+
log.debug("Send laser enable: %s", flag)
|
|
2433
|
+
if flag:
|
|
2434
|
+
self.last_applied_laser_power = 0.0
|
|
2435
|
+
else:
|
|
2436
|
+
self.last_applied_laser_power = self.next_applied_laser_power
|
|
2437
|
+
|
|
2438
|
+
self.set_strobe_enable(flag)
|
|
2439
|
+
|
|
2440
|
+
if self.settings.is_xs():
|
|
2441
|
+
# Series-XS getLaserEnable doesn't provide immediate confirmation
|
|
2442
|
+
# because it's comingled with laserWatchdogSec and probably
|
|
2443
|
+
# laserDelaySec
|
|
2444
|
+
return SpectrometerResponse(data=True)
|
|
2445
|
+
|
|
2446
|
+
# try to verify laserEnable worked
|
|
2447
|
+
tries = 0
|
|
2448
|
+
while True:
|
|
2449
|
+
res = self.get_laser_enabled()
|
|
2450
|
+
if flag == res.data:
|
|
2451
|
+
return SpectrometerResponse(data=True)
|
|
2452
|
+
tries += 1
|
|
2453
|
+
if tries > 3:
|
|
2454
|
+
log.critical("laser_enable %s command failed, giving up", flag)
|
|
2455
|
+
self.queue_message("marquee_error", "laser setting failed")
|
|
2456
|
+
return SpectrometerResponse(data=False, error_msg="laser setting failed", error_lvl=ErrorLevel.medium)
|
|
2457
|
+
else:
|
|
2458
|
+
log.error("laser_enable %s command failed, re-trying", flag)
|
|
2459
|
+
self.set_strobe_enable(flag)
|
|
2460
|
+
|
|
2461
|
+
def set_laser_power_mW(self, mW_in: int):
|
|
2462
|
+
if mW_in is None or not self.settings.eeprom.has_laser_power_calibration():
|
|
2463
|
+
log.error("EEPROM doesn't have laser power calibration")
|
|
2464
|
+
self.settings.state.laser_power_mW = 0
|
|
2465
|
+
self.settings.state.laser_power_perc = 0
|
|
2466
|
+
self.settings.state.use_mW = False
|
|
2467
|
+
return SpectrometerResponse(data=False, error_msg="no laser power calibration")
|
|
2468
|
+
|
|
2469
|
+
mW = min(self.settings.eeprom.max_laser_power_mW, max(self.settings.eeprom.min_laser_power_mW, mW_in))
|
|
2470
|
+
|
|
2471
|
+
perc = self.settings.eeprom.laser_power_mW_to_percent(mW)
|
|
2472
|
+
log.debug("set_laser_power_mW: range (%.2f, %.2f), requested %.2f, approved %.2f, percent = %.2f",
|
|
2473
|
+
self.settings.eeprom.min_laser_power_mW,
|
|
2474
|
+
self.settings.eeprom.max_laser_power_mW,
|
|
2475
|
+
mW_in,
|
|
2476
|
+
mW,
|
|
2477
|
+
perc)
|
|
2478
|
+
self.settings.state.laser_power_mW = mW
|
|
2479
|
+
return self.set_laser_power_perc(perc, set_in_perc=False)
|
|
2480
|
+
|
|
2481
|
+
##
|
|
2482
|
+
# @brief Sets whether the PWM pulse period is 1000µs (high-resolution, default)
|
|
2483
|
+
# or 100µs (low-resolution, legacy).
|
|
2484
|
+
#
|
|
2485
|
+
# @param flag (Input) True for 1000µs PWM period (high-resolution, default),
|
|
2486
|
+
# False for 100µs (low-resolution)
|
|
2487
|
+
#
|
|
2488
|
+
# Laser power is controlled via PWM (Pulse Width Modulation), essentially
|
|
2489
|
+
# a square wave whose "duty cycle" (high vs low), expressed as a percentage,
|
|
2490
|
+
# represents the amount of time the laser is actually firing. If the duty
|
|
2491
|
+
# cycle is 33%, the laser will be firing 1/3 of the time, representing
|
|
2492
|
+
# approximately one-third of "full power".
|
|
2493
|
+
#
|
|
2494
|
+
# (It's not "exactly" one-third because the laser needs a moment to
|
|
2495
|
+
# stablize when energized, and that "start-up" instability is increased
|
|
2496
|
+
# when you're constantly pulsing the laser on-and-off.)
|
|
2497
|
+
#
|
|
2498
|
+
# This function is used to determine the LENGTH (period) of that PWM
|
|
2499
|
+
# square wave. Because the laser's PWM parameters are all specified in
|
|
2500
|
+
# microseconds (µs), a longer period allows greater precision (resolution)
|
|
2501
|
+
# in specifying the duty cycle.
|
|
2502
|
+
#
|
|
2503
|
+
# Historically, Wasatch spectrometers used a PWM period of 100µs, meaning
|
|
2504
|
+
# the pulse width (time each wave was high) could only be set from 1-99µs,
|
|
2505
|
+
# giving an essential laser power resolution of 1%. That behavior can
|
|
2506
|
+
# be restored by setting this value False.
|
|
2507
|
+
#
|
|
2508
|
+
# More recently, we've increased the default PWM pulse period to 1000µs
|
|
2509
|
+
# (1ms). Since the pulse width is still set in µs, that allows an
|
|
2510
|
+
# effective laser power resolution of 0.1%. This is the new default,
|
|
2511
|
+
# and can be explicitly requested by setting this value to True.
|
|
2512
|
+
#
|
|
2513
|
+
# @warning Note that this function is provided for cases where the laser power
|
|
2514
|
+
# is set as a percentage. When setting laser power through milliWatts, using
|
|
2515
|
+
# the onboard laser power calibration, it is important to use the same
|
|
2516
|
+
# resolution as was in effect when the calibration was generated. All Wasatch
|
|
2517
|
+
# laser power calibrations are generated in "high-resolution," so this
|
|
2518
|
+
# function SHOULD NOT be set "False" (low-res) if setting laser power in mW.
|
|
2519
|
+
def set_laser_power_high_resolution(self, flag: bool):
|
|
2520
|
+
self.settings.state.laser_power_high_resolution = True if flag else False
|
|
2521
|
+
return SpectrometerResponse()
|
|
2522
|
+
|
|
2523
|
+
def set_laser_power_require_modulation(self, flag: bool):
|
|
2524
|
+
self.settings.state.laser_power_require_modulation = True if flag else False
|
|
2525
|
+
return SpectrometerResponse()
|
|
2526
|
+
|
|
2527
|
+
##
|
|
2528
|
+
# @todo support floating-point value, as we have a 12-bit ADC and can provide
|
|
2529
|
+
# a bit more precision than 100 discrete steps (goal to support 0.1 - .125% resolution)
|
|
2530
|
+
def set_laser_power_perc(self, value_in: float, set_in_perc: bool = True):
|
|
2531
|
+
if not self.settings.eeprom.has_laser:
|
|
2532
|
+
msg = "no laser installed"
|
|
2533
|
+
log.error(msg)
|
|
2534
|
+
return SpectrometerResponse(data=False, error_msg=msg)
|
|
2535
|
+
|
|
2536
|
+
value = float(max(0, min(100, value_in)))
|
|
2537
|
+
self.settings.state.laser_power_perc = value
|
|
2538
|
+
log.debug("set_laser_power_perc: range (0, 100), requested %.2f, applying %.2f", value_in, value)
|
|
2539
|
+
|
|
2540
|
+
if set_in_perc:
|
|
2541
|
+
# apparently the laser power was explicitly commanded as a percentage
|
|
2542
|
+
# of full, and not "computed from mW" using a calibration, so clear
|
|
2543
|
+
# the mW setpoint
|
|
2544
|
+
self.settings.state.laser_power_mW = 0
|
|
2545
|
+
|
|
2546
|
+
return self.set_laser_power_perc_immediate(value)
|
|
2547
|
+
|
|
2548
|
+
##
|
|
2549
|
+
# Sets laser power (modulated pulse width) which will be used next time the
|
|
2550
|
+
# laser is turned on (or changed immediately, if the laser is already enabled).
|
|
2551
|
+
# Laser power is determined by a combination of the pulse width,
|
|
2552
|
+
# period and modulation being enabled. There are many combinations of
|
|
2553
|
+
# these values that will produce a given percentage of the total laser
|
|
2554
|
+
# power through pulse width modulation. There is no 'get laser power'
|
|
2555
|
+
# control message on the device.
|
|
2556
|
+
#
|
|
2557
|
+
# Some of the goals of Enlighten are for it to be stable, and a reason
|
|
2558
|
+
# we have sales. During spectrometer builds, it was discovered that
|
|
2559
|
+
# the laser power settings were not implemented. During the
|
|
2560
|
+
# implementation process, it was discovered that the laser modulation,
|
|
2561
|
+
# pulse period and pulse width commands do not conform to
|
|
2562
|
+
# specification. Where you can set integration time 100 ms with the
|
|
2563
|
+
# command:
|
|
2564
|
+
#
|
|
2565
|
+
# device.ctrl_transfer(bmRequestType=device_to_host,
|
|
2566
|
+
# bRequest=0xDB,
|
|
2567
|
+
# wValue=100,
|
|
2568
|
+
# wIndex=0,
|
|
2569
|
+
# data_or_wLength=0)
|
|
2570
|
+
#
|
|
2571
|
+
# The laser pulse period must be set where the wValue and
|
|
2572
|
+
# data_or_wLength parameters are equal. So if you wanted a pulse
|
|
2573
|
+
# period of 100, you must specify the value in both places:
|
|
2574
|
+
#
|
|
2575
|
+
# ...
|
|
2576
|
+
# wValue=100,
|
|
2577
|
+
# data_or_wLength=100)
|
|
2578
|
+
# ...
|
|
2579
|
+
#
|
|
2580
|
+
# This in turn implies that the legacy firmware has a long masked
|
|
2581
|
+
# issue when reading the value to update from the data_or_wLength
|
|
2582
|
+
# parameter instead of the wValue field. This is only accurate for the
|
|
2583
|
+
# laser modulation related functions.
|
|
2584
|
+
#
|
|
2585
|
+
# This is backed up by the Dash v3 StrokerControl DLL implementation.
|
|
2586
|
+
# It was discovered that the StrokerControl DLL sets the wValue and
|
|
2587
|
+
# data or wLength parameters to the same value at every control
|
|
2588
|
+
# message write.
|
|
2589
|
+
#
|
|
2590
|
+
# The exciting takeaway here is that Enlighten is stable enough.
|
|
2591
|
+
# Turning the laser on with the data or wLength parameter not set
|
|
2592
|
+
# correctly will cause a hardware failure and complete device lockup
|
|
2593
|
+
# requiring a power cycle.
|
|
2594
|
+
#
|
|
2595
|
+
# fid:
|
|
2596
|
+
# CRITICAL Hardware Failure FID Send Code Problem with
|
|
2597
|
+
# ctrl transfer: [Errno None] 11
|
|
2598
|
+
#
|
|
2599
|
+
# Unlike Dash which may lockup and require killing the application,
|
|
2600
|
+
# Enlighten does not lock up. The Enlighten code base has now been
|
|
2601
|
+
# used to unmask an issue that has been lurking with our legacy
|
|
2602
|
+
# firmware for close to 6 years. We've detected this out of
|
|
2603
|
+
# specification area of the code before it can adversely impact a
|
|
2604
|
+
# customer. """
|
|
2605
|
+
#
|
|
2606
|
+
# As long as laser power is modulated using a period of 100us,
|
|
2607
|
+
# with a necessarily-integral pulse width of 1-99us, then it's
|
|
2608
|
+
# not physically possible to support fractional power levels.
|
|
2609
|
+
#
|
|
2610
|
+
# @todo talk to Jason about changing modulation PERIOD to longer
|
|
2611
|
+
# value (200us? 400? 1000?), OR whether pulse WIDTH can be
|
|
2612
|
+
# in smaller unit (500ns? 100ns?)
|
|
2613
|
+
def set_laser_power_perc_immediate(self, value: float):
|
|
2614
|
+
|
|
2615
|
+
# laser can flicker if we're on the wrong ADC?
|
|
2616
|
+
|
|
2617
|
+
# don't want anything weird when passing over USB
|
|
2618
|
+
value = float(max(0, min(100, value)))
|
|
2619
|
+
if value <= 0:
|
|
2620
|
+
return set_laser_enable(False)
|
|
2621
|
+
|
|
2622
|
+
# If full power (and allowed), disable modulation and exit
|
|
2623
|
+
if value >= 100:
|
|
2624
|
+
if self.settings.state.laser_power_require_modulation:
|
|
2625
|
+
log.debug("100% power requested, yet laser modulation required, so not disabling modulation")
|
|
2626
|
+
else:
|
|
2627
|
+
log.debug("Turning off laser modulation (full power)")
|
|
2628
|
+
self.next_applied_laser_power = 100.0
|
|
2629
|
+
log.debug("next_applied_laser_power = 100.0")
|
|
2630
|
+
return self.set_mod_enable(False)
|
|
2631
|
+
|
|
2632
|
+
period_us = 1000 if self.settings.state.laser_power_high_resolution else 100
|
|
2633
|
+
width_us = int(round(1.0 * value * period_us / 100.0, 0)) # note that value is in range (0, 100) not (0, 1)
|
|
2634
|
+
|
|
2635
|
+
if width_us <= 0:
|
|
2636
|
+
return set_laser_enable(False)
|
|
2637
|
+
|
|
2638
|
+
# pulse width can't be longer than period, or shorter than 1us
|
|
2639
|
+
width_us = max(1, min(width_us, period_us))
|
|
2640
|
+
|
|
2641
|
+
# Change the pulse period. Note that we're not parsing into 40-bit
|
|
2642
|
+
# because this implementation is hard-coded to either 100 or 1000us
|
|
2643
|
+
# (both fitting well within uint16)
|
|
2644
|
+
result = self.set_mod_period_us(period_us)
|
|
2645
|
+
# if result.data is None:
|
|
2646
|
+
# log.critical("Hardware Failure to send laser mod. pulse period")
|
|
2647
|
+
# return SpectrometerResponse(data=False, error_msg="failed to send laser mod")
|
|
2648
|
+
|
|
2649
|
+
# Set the pulse width to the 0-100 percentage of power
|
|
2650
|
+
result = self.set_mod_width_us(width_us)
|
|
2651
|
+
# if result.data is None:
|
|
2652
|
+
# log.critical("Hardware Failure to send pulse width")
|
|
2653
|
+
# return SpectrometerResponse(data=False, error_msg="failed to send pulse width")
|
|
2654
|
+
|
|
2655
|
+
# Enable modulation
|
|
2656
|
+
result = self.set_mod_enable(True)
|
|
2657
|
+
# if result.data is None:
|
|
2658
|
+
# log.critical("Hardware Failure to send laser modulation")
|
|
2659
|
+
# return SpectrometerResponse(data=False, error_msg="failed to send laser modulation")
|
|
2660
|
+
|
|
2661
|
+
log.debug("Laser power set to: %d", value)
|
|
2662
|
+
|
|
2663
|
+
self.next_applied_laser_power = value
|
|
2664
|
+
log.debug("next_applied_laser_power = %s", self.next_applied_laser_power)
|
|
2665
|
+
|
|
2666
|
+
return result
|
|
2667
|
+
|
|
2668
|
+
##
|
|
2669
|
+
# @note never used, provided for OEM
|
|
2670
|
+
def get_laser_temperature_setpoint_raw(self):
|
|
2671
|
+
if not self.settings.eeprom.has_laser:
|
|
2672
|
+
msg = "no laser installed"
|
|
2673
|
+
log.error(msg)
|
|
2674
|
+
return SpectrometerResponse(data=None, error_msg=msg)
|
|
2675
|
+
|
|
2676
|
+
# is this little-endian?
|
|
2677
|
+
lsb_len = 2 if self.settings.is_xs() else 1
|
|
2678
|
+
|
|
2679
|
+
return self._get_code(0xe8, lsb_len=lsb_len, label="GET_LASER_TEC_SETPOINT")
|
|
2680
|
+
|
|
2681
|
+
def set_laser_temperature_setpoint_raw(self, value: int):
|
|
2682
|
+
log.debug(f"Send laser temperature setpoint raw: 0x{value:03x}")
|
|
2683
|
+
return self._send_code(0xe7, value, label="SET_LASER_TEC_SETPOINT")
|
|
2684
|
+
|
|
2685
|
+
def set_laser_warning_delay_sec(self, value):
|
|
2686
|
+
if not self.settings.is_xs():
|
|
2687
|
+
log.error("laser warning delay only configurable on XS")
|
|
2688
|
+
return
|
|
2689
|
+
return self._send_code(0x8a, value, label="SET_LASER_WARNING_DELAY_SEC")
|
|
2690
|
+
|
|
2691
|
+
def get_laser_warning_delay_sec(self):
|
|
2692
|
+
if not self.settings.is_xs():
|
|
2693
|
+
msg = "laser warning delay only configurable on XS"
|
|
2694
|
+
log.error(msg)
|
|
2695
|
+
return SpectrometerResponse(data=None, error_msg=msg)
|
|
2696
|
+
|
|
2697
|
+
if not self.settings.supports_feature("get_laser_warning_delay_sec"):
|
|
2698
|
+
msg = "GET_LASER_WARNING_DELAY_SEC not supported on this firmware"
|
|
2699
|
+
log.error(msg)
|
|
2700
|
+
return SpectrometerResponse(data=None, error_msg=msg)
|
|
2701
|
+
|
|
2702
|
+
result = self._get_code(0x8b, lsb_len=1, label="GET_LASER_WARNING_DELAY_SEC")
|
|
2703
|
+
self.settings.state.laser_warning_delay_sec = result.data
|
|
2704
|
+
return result
|
|
2705
|
+
|
|
2706
|
+
############################################################################
|
|
2707
|
+
# digital pot on 220250 Rev4A+
|
|
2708
|
+
############################################################################
|
|
2709
|
+
|
|
2710
|
+
def set_laser_power_attenuator(self, value):
|
|
2711
|
+
cmd = "SET_LASER_ATTENUATOR"
|
|
2712
|
+
if not self.settings.is_xs():
|
|
2713
|
+
msg = f"{cmd} is only available on XS-Series with 220250 Rev4A+"
|
|
2714
|
+
log.debug(msg)
|
|
2715
|
+
return SpectrometerResponse(data=None, error_msg=msg)
|
|
2716
|
+
|
|
2717
|
+
# digital potentiometer is 8-bit
|
|
2718
|
+
value = max(0, min(0xff, int(round(value))))
|
|
2719
|
+
|
|
2720
|
+
return self._send_code(0x82, value, label=cmd)
|
|
2721
|
+
|
|
2722
|
+
def get_laser_power_attenuator(self):
|
|
2723
|
+
cmd = "GET_LASER_ATTENUATOR"
|
|
2724
|
+
if not self.settings.is_xs():
|
|
2725
|
+
msg = f"{cmd} is only available on XS-Series with 220250 Rev4A+"
|
|
2726
|
+
log.debug(msg)
|
|
2727
|
+
return SpectrometerResponse(data=None, error_msg=msg)
|
|
2728
|
+
|
|
2729
|
+
return self._get_code(0x83, msb_len=1, label=cmd)
|
|
2730
|
+
|
|
2731
|
+
############################################################################
|
|
2732
|
+
# laser interlock
|
|
2733
|
+
############################################################################
|
|
2734
|
+
|
|
2735
|
+
def get_laser_interlock(self):
|
|
2736
|
+
""" Legacy wrapper over can_laser_fire. """
|
|
2737
|
+
return self.can_laser_fire()
|
|
2738
|
+
|
|
2739
|
+
def can_laser_fire(self):
|
|
2740
|
+
"""
|
|
2741
|
+
@note only works on FX2-based spectrometers with FW >= 10.0.0.11
|
|
2742
|
+
@returns True if there is a laser and either the interlock is
|
|
2743
|
+
closed (in firing position), or there is no readable
|
|
2744
|
+
interlock
|
|
2745
|
+
"""
|
|
2746
|
+
if not self.settings.eeprom.has_laser:
|
|
2747
|
+
msg = "no laser installed"
|
|
2748
|
+
log.error(msg)
|
|
2749
|
+
return SpectrometerResponse(data=False, error_msg=msg)
|
|
2750
|
+
|
|
2751
|
+
if not self.settings.eeprom.has_interlock_feedback:
|
|
2752
|
+
# log.debug("CAN_LASER_FIRE requires has_interlock_feedback (defaulting True)")
|
|
2753
|
+
return SpectrometerResponse(data=True)
|
|
2754
|
+
|
|
2755
|
+
res = self._get_code(0xef, label="CAN_LASER_FIRE", msb_len=1)
|
|
2756
|
+
res.data = 0 != res.data
|
|
2757
|
+
return res
|
|
2758
|
+
|
|
2759
|
+
def is_laser_firing(self):
|
|
2760
|
+
"""
|
|
2761
|
+
Check if the laser actually IS firing, independent of laser_enable or
|
|
2762
|
+
can_laser_fire.
|
|
2763
|
+
|
|
2764
|
+
@returns SpectrometerResponse
|
|
2765
|
+
"""
|
|
2766
|
+
if not self.settings.eeprom.has_interlock_feedback:
|
|
2767
|
+
# log.debug("IS_LASER_FIRING requires has_interlock_feedback (defaulting to laser_enabled)")
|
|
2768
|
+
return self.get_laser_enabled()
|
|
2769
|
+
|
|
2770
|
+
res = self.get_upper_code(0x0d, label="IS_LASER_FIRING", msb_len=1)
|
|
2771
|
+
res.data = 0 != res.data
|
|
2772
|
+
return res
|
|
2773
|
+
|
|
2774
|
+
############################################################################
|
|
2775
|
+
# (end of laser commands)
|
|
2776
|
+
############################################################################
|
|
2777
|
+
|
|
2778
|
+
def reset_fpga(self):
|
|
2779
|
+
log.debug("reset_Fpga: start")
|
|
2780
|
+
result = self._send_code(0xb5, label="RESET_FPGA")
|
|
2781
|
+
|
|
2782
|
+
self.queue_message("marquee_error", "resetting FPGA")
|
|
2783
|
+
|
|
2784
|
+
log.debug("reset_fpga: sleeping 3sec")
|
|
2785
|
+
sleep(3)
|
|
2786
|
+
|
|
2787
|
+
log.debug("reset_fpga: re-applying FPGA settings")
|
|
2788
|
+
|
|
2789
|
+
self.set_high_gain_mode_enable (self.settings.state.high_gain_mode_enabled)
|
|
2790
|
+
self.set_integration_time_ms (self.settings.state.integration_time_ms)
|
|
2791
|
+
self.set_detector_gain (self.settings.eeprom.detector_gain)
|
|
2792
|
+
self.set_detector_offset (self.settings.eeprom.detector_offset)
|
|
2793
|
+
self.set_detector_gain_odd (self.settings.eeprom.detector_gain_odd)
|
|
2794
|
+
self.set_detector_offset_odd (self.settings.eeprom.detector_offset_odd)
|
|
2795
|
+
|
|
2796
|
+
self.queue_message("marquee_info", "FPGA reset")
|
|
2797
|
+
|
|
2798
|
+
log.debug("reset_fpga: done")
|
|
2799
|
+
return result
|
|
2800
|
+
|
|
2801
|
+
##
|
|
2802
|
+
# Read the trigger source setting from the device.
|
|
2803
|
+
#
|
|
2804
|
+
# - 0 = internal
|
|
2805
|
+
# - 1 = external
|
|
2806
|
+
#
|
|
2807
|
+
# Use caution when interpreting the larger behavior of
|
|
2808
|
+
# the device as ARM and FX2 implementations differ as of 2017-08-02
|
|
2809
|
+
#
|
|
2810
|
+
# @note never called by ENLIGHTEN - provided for OEMs
|
|
2811
|
+
def get_trigger_source(self):
|
|
2812
|
+
value = self._get_code(0xd3, label="GET_TRIGGER_SOURCE", msb_len=1)
|
|
2813
|
+
self.settings.state.trigger_source = value.data
|
|
2814
|
+
return value
|
|
2815
|
+
|
|
2816
|
+
##
|
|
2817
|
+
# @note not called by ENLIGHTEN
|
|
2818
|
+
# @warning conflicts with GET_SELECTED_LASER
|
|
2819
|
+
def get_raman_mode_enabled_NOT_USED(self):
|
|
2820
|
+
res = self.get_upper_code(0x15, label="GET_RAMAN_MODE_ENABLED", msb_len=1)
|
|
2821
|
+
res.data = 0 != res.data
|
|
2822
|
+
return res
|
|
2823
|
+
|
|
2824
|
+
##
|
|
2825
|
+
# Enable "Raman mode" (automatic laser) in the spectrometer firmware.
|
|
2826
|
+
def set_raman_mode_enable_NOT_USED(self, flag: bool):
|
|
2827
|
+
if not self.settings.is_micro():
|
|
2828
|
+
log.debug("Raman mode only supported on Series-XS")
|
|
2829
|
+
return SpectrometerResponse(data=False, error_msg="raman mode not supported")
|
|
2830
|
+
|
|
2831
|
+
return self._send_code(bRequest = 0xff,
|
|
2832
|
+
wValue = 0x16,
|
|
2833
|
+
wIndex = 1 if flag else 0,
|
|
2834
|
+
data_or_wLength = [0] * 8,
|
|
2835
|
+
label = "SET_RAMAN_MODE_ENABLE")
|
|
2836
|
+
|
|
2837
|
+
def get_raman_delay_ms(self):
|
|
2838
|
+
res = self.get_upper_code(0x19, label="GET_RAMAN_DELAY_MS", msb_len=2)
|
|
2839
|
+
self.settings.state.raman_delay_ms = res.data
|
|
2840
|
+
return res
|
|
2841
|
+
|
|
2842
|
+
def set_raman_delay_ms(self, ms: int):
|
|
2843
|
+
if not self.settings.is_micro():
|
|
2844
|
+
log.debug("Raman delay only supported on Series-XS")
|
|
2845
|
+
return SpectrometerResponse(data=False, error_msg="raman delay not supported")
|
|
2846
|
+
|
|
2847
|
+
# send value as big-endian
|
|
2848
|
+
msb = (ms >> 8) & 0xff
|
|
2849
|
+
lsb = ms & 0xff
|
|
2850
|
+
value = (msb << 8) | lsb
|
|
2851
|
+
|
|
2852
|
+
self.settings.state.raman_delay_ms = ms
|
|
2853
|
+
return self._send_code(bRequest = 0xff,
|
|
2854
|
+
wValue = 0x20,
|
|
2855
|
+
wIndex = value,
|
|
2856
|
+
data_or_wLength = [0] * 8,
|
|
2857
|
+
label = "SET_RAMAN_DELAY_MS")
|
|
2858
|
+
|
|
2859
|
+
def get_laser_watchdog_sec(self):
|
|
2860
|
+
res = self.get_upper_code(0x17, label="GET_LASER_WATCHDOG_SEC", msb_len=2)
|
|
2861
|
+
self.settings.state.laser_watchdog_sec = res.data
|
|
2862
|
+
return res
|
|
2863
|
+
|
|
2864
|
+
# @note until this change is mimick'd in the STM32 (and after that, until the FPGA
|
|
2865
|
+
# logic itself is fixed), always send a DISABLE_LASER before changing the
|
|
2866
|
+
# watchdog period.
|
|
2867
|
+
def set_laser_watchdog_sec(self, sec):
|
|
2868
|
+
if not self.settings.is_micro():
|
|
2869
|
+
log.error("Laser watchdog only supported on Series-XS")
|
|
2870
|
+
return SpectrometerResponse(data=False, error_msg="laser watchdog not supported")
|
|
2871
|
+
|
|
2872
|
+
# remove this call after the Series-XS ARM / FPGA watchdog are fixed
|
|
2873
|
+
# MZ: are they fixed? can I remove this?
|
|
2874
|
+
# self.set_laser_enable(False)
|
|
2875
|
+
|
|
2876
|
+
# send value as big-endian
|
|
2877
|
+
msb = (sec >> 8) & 0xff
|
|
2878
|
+
lsb = sec & 0xff
|
|
2879
|
+
value = (msb << 8) | lsb
|
|
2880
|
+
|
|
2881
|
+
self.settings.state.laser_watchdog_sec = sec
|
|
2882
|
+
return self._send_code(bRequest = 0xff,
|
|
2883
|
+
wValue = 0x18,
|
|
2884
|
+
wIndex = value,
|
|
2885
|
+
data_or_wLength = [0] * 8,
|
|
2886
|
+
label = "SET_LASER_WATCHDOG_SEC")
|
|
2887
|
+
|
|
2888
|
+
def update_laser_watchdog(self):
|
|
2889
|
+
"""
|
|
2890
|
+
Automatically set the laser watchdog long enough to handle the current
|
|
2891
|
+
integration time, assuming we have to perform 6 throwaways on the sensor
|
|
2892
|
+
in case it went to sleep.
|
|
2893
|
+
|
|
2894
|
+
@note we are not currently using this function
|
|
2895
|
+
"""
|
|
2896
|
+
if not self.settings.is_micro() or not self.settings.eeprom.has_laser:
|
|
2897
|
+
return SpectrometerResponse(data=False, error_msg="update laser watchdog not supported")
|
|
2898
|
+
|
|
2899
|
+
int_ms = self.settings.state.integration_time_ms
|
|
2900
|
+
scans = self.settings.state.scans_to_average
|
|
2901
|
+
|
|
2902
|
+
throwaways_sec = int_ms * (8 + scans) / 1000.0
|
|
2903
|
+
watchdog_sec = int(max(10, throwaways_sec)) * 2
|
|
2904
|
+
log.debug(f"updating laser watchdog to {watchdog_sec} based on integration time {int_ms}ms and {scans} averaging")
|
|
2905
|
+
|
|
2906
|
+
return self.set_laser_watchdog_sec(watchdog_sec)
|
|
2907
|
+
|
|
2908
|
+
def update_vertical_roi(self):
|
|
2909
|
+
if self.settings.is_micro():
|
|
2910
|
+
roi = self.settings.get_vertical_roi()
|
|
2911
|
+
if roi is not None:
|
|
2912
|
+
self.set_vertical_roi(roi)
|
|
2913
|
+
|
|
2914
|
+
def set_vertical_roi(self, roi):
|
|
2915
|
+
# check for legacy SiG-VIS since it didn't like vertical binning
|
|
2916
|
+
if self.settings.fpga_firmware_version == "000-008" and self.settings.microcontroller_firmware_version == "0.1.0.7":
|
|
2917
|
+
return SpectrometerResponse(data=False)
|
|
2918
|
+
|
|
2919
|
+
if not self.settings.is_micro():
|
|
2920
|
+
if not self.settings.supports_feature("hamamatsu_vertical_roi"):
|
|
2921
|
+
log.warning("Vertical Binning only configurable on XS and prototype X/XM")
|
|
2922
|
+
return SpectrometerResponse(data=False, error_msg="vertical binning not supported")
|
|
2923
|
+
|
|
2924
|
+
if isinstance(roi, ROI):
|
|
2925
|
+
start, end = roi.start, roi.end
|
|
2926
|
+
elif len(roi) == 2:
|
|
2927
|
+
start, end = roi[0], roi[1]
|
|
2928
|
+
else:
|
|
2929
|
+
log.error("set_vertical_roi: requires an ROI object or tuple of (start, stop) lines")
|
|
2930
|
+
return SpectrometerResponse(data=False, error_msg="invalid start and stop lines")
|
|
2931
|
+
|
|
2932
|
+
if start < 0 or end < 0:
|
|
2933
|
+
log.error("set_vertical_roi: requires POSITIVE (start, stop) lines")
|
|
2934
|
+
return SpectrometerResponse(data=False, error_msg="invalid start and stop lines")
|
|
2935
|
+
|
|
2936
|
+
# enforce ascending order (also, note that stop line is "last line binned + 1", so stop must be > start)
|
|
2937
|
+
if start >= end:
|
|
2938
|
+
# (start, end) = (end, start)
|
|
2939
|
+
log.error("set_vertical_roi: requires ascending order (ignoring %d, %d)", start, end)
|
|
2940
|
+
return SpectrometerResponse(data=False, error_msg="invalid start and stop lines")
|
|
2941
|
+
|
|
2942
|
+
if self.settings.is_xs():
|
|
2943
|
+
# ARM has USB opcodes for start/stop line
|
|
2944
|
+
self._send_code(bRequest=0xff, wValue=0x21, wIndex=start, label="SET_CMOS_START_LINE")
|
|
2945
|
+
self._send_code(bRequest=0xff, wValue=0x23, wIndex=end, label="SET_CMOS_STOP_LINE")
|
|
2946
|
+
else:
|
|
2947
|
+
# FX2 lacks USB opcodes for start/stop line, so use I2C Poke (clamp to 1 byte)
|
|
2948
|
+
start = int(max(0, min(255, start)))
|
|
2949
|
+
end = int(max(0, min(255, end )))
|
|
2950
|
+
self.i2c_write(0x29, [start], label="I2C_POKE(SET_CCD_START_LINE)")
|
|
2951
|
+
self.i2c_write(0x2A, [end], label="I2C_POKE(SET_CCD_STOP_LINE)")
|
|
2952
|
+
|
|
2953
|
+
self.settings.eeprom.roi_vertical_region_1_start = start
|
|
2954
|
+
self.settings.eeprom.roi_vertical_region_1_end = end
|
|
2955
|
+
self.reset_area_scan_frame()
|
|
2956
|
+
|
|
2957
|
+
return SpectrometerResponse(data=True)
|
|
2958
|
+
|
|
2959
|
+
##
|
|
2960
|
+
# @param mode: integral value 0-3
|
|
2961
|
+
#
|
|
2962
|
+
# \verbatim
|
|
2963
|
+
# mode ADC (AD) Pixel Width (OD)
|
|
2964
|
+
# b00 10-bit 10-bit
|
|
2965
|
+
# b01 10-bit 12-bit
|
|
2966
|
+
# b10 12-bit 10-bit
|
|
2967
|
+
# b11 12-bit 12-bit
|
|
2968
|
+
# \endverbatim
|
|
2969
|
+
def set_pixel_mode(self, mode: float):
|
|
2970
|
+
if not self.settings.is_micro():
|
|
2971
|
+
log.debug("Pixel Mode only configurable on Series-XS")
|
|
2972
|
+
return SpectrometerResponse(data=False, error_msg="pixel mode not supported")
|
|
2973
|
+
|
|
2974
|
+
# we only care about the two least-significant bits
|
|
2975
|
+
mode = int(round(mode)) & 0x3
|
|
2976
|
+
|
|
2977
|
+
result = self._send_code(bRequest = 0xfd,
|
|
2978
|
+
wValue = mode,
|
|
2979
|
+
label = "SET_PIXEL_MODE")
|
|
2980
|
+
|
|
2981
|
+
log.debug("waiting 1sec...")
|
|
2982
|
+
sleep(1)
|
|
2983
|
+
|
|
2984
|
+
return result
|
|
2985
|
+
|
|
2986
|
+
def clear_regions(self):
|
|
2987
|
+
x1 = self.settings.eeprom.active_pixels_horizontal
|
|
2988
|
+
y1 = self.settings.eeprom.active_pixels_vertical
|
|
2989
|
+
log.debug(f"resettings detector to full ({x1}, {y1}) extent")
|
|
2990
|
+
|
|
2991
|
+
self.settings.state.region = None
|
|
2992
|
+
self.settings.update_wavecal()
|
|
2993
|
+
|
|
2994
|
+
return self.set_detector_roi([0, 0, y1, 0, x1], store=False)
|
|
2995
|
+
|
|
2996
|
+
def set_single_region(self, n: int):
|
|
2997
|
+
"""
|
|
2998
|
+
This function uses the the multi-region feature to select just a single
|
|
2999
|
+
pre-configured region at a time. Whichever region is selected, that
|
|
3000
|
+
region's parameters are written to "region 0" of the spectrometer, and
|
|
3001
|
+
the global wavecal is updated to use that region's calibration.
|
|
3002
|
+
|
|
3003
|
+
@todo consider clear_region() function to restore physical ROI to
|
|
3004
|
+
(0, active_vertical_pixels, 0, active_horizontal_pixels)
|
|
3005
|
+
(leave wavecal alone?)
|
|
3006
|
+
"""
|
|
3007
|
+
if self.settings.state.detector_regions is None:
|
|
3008
|
+
log.debug(f"no detector regions configured")
|
|
3009
|
+
return SpectrometerResponse(data=False, error_msg="no regions configured")
|
|
3010
|
+
|
|
3011
|
+
roi = self.settings.state.detector_regions.get_roi(n)
|
|
3012
|
+
if roi is None:
|
|
3013
|
+
log.debug(f"unconfigured region {n} (max {self.settings.eeprom.region_count}")
|
|
3014
|
+
return SpectrometerResponse(data=False, error_msg="unconfigured region")
|
|
3015
|
+
|
|
3016
|
+
log.debug(f"set_single_region: applying region {n}: {roi}")
|
|
3017
|
+
self.settings.set_single_region(n)
|
|
3018
|
+
|
|
3019
|
+
# send a "fake" ROI downstream, overriding to position 0
|
|
3020
|
+
self.set_detector_roi([0, roi.y0, roi.y1, roi.x0, roi.x1], store=False)
|
|
3021
|
+
return SpectrometerResponse()
|
|
3022
|
+
|
|
3023
|
+
def set_detector_roi(self, args: list[float], store: bool = True):
|
|
3024
|
+
"""
|
|
3025
|
+
Note this only sends the ROI downstream to the spectrometer (and stores
|
|
3026
|
+
it in DetectorRegions). If you want to update the wavecal and store
|
|
3027
|
+
the "selected" region index, use set_region() instead (which calls this).
|
|
3028
|
+
|
|
3029
|
+
You should use set_region() if you are selecting one of the standard
|
|
3030
|
+
regions already configured on the EEPROM. You should use set_detector_roi()
|
|
3031
|
+
if you're making ad-hoc ROIs which aren't configured on the EEPROM.
|
|
3032
|
+
|
|
3033
|
+
@param args: either a DetectorROI or a tuple of (region, y0, y1, x0, x1)
|
|
3034
|
+
"""
|
|
3035
|
+
if not self.settings.is_micro():
|
|
3036
|
+
log.debug("Detector ROI only configurable on Series-XS")
|
|
3037
|
+
return SpectrometerResponse(data=False, error_msg="Detector ROI not configurable")
|
|
3038
|
+
|
|
3039
|
+
if isinstance(args, DetectorROI):
|
|
3040
|
+
roi = args
|
|
3041
|
+
log.debug(f"passed DetectorROI: {roi}")
|
|
3042
|
+
else:
|
|
3043
|
+
# convert args to ROI
|
|
3044
|
+
log.debug(f"creating DetectorROI from args: {args}")
|
|
3045
|
+
|
|
3046
|
+
if len(args) != 5:
|
|
3047
|
+
log.error(f"invalid detector roi args: {args}")
|
|
3048
|
+
return SpectrometerResponse(data=False, error_msg="invalid roi args")
|
|
3049
|
+
|
|
3050
|
+
region = int(round(args[0]))
|
|
3051
|
+
y0 = int(round(args[1]))
|
|
3052
|
+
y1 = int(round(args[2]))
|
|
3053
|
+
x0 = int(round(args[3]))
|
|
3054
|
+
x1 = int(round(args[4]))
|
|
3055
|
+
|
|
3056
|
+
if not (0 <= region <= 3 and
|
|
3057
|
+
y0 < y1 and
|
|
3058
|
+
x0 < x1 and
|
|
3059
|
+
y0 >= 0 and
|
|
3060
|
+
x0 >= 0 and
|
|
3061
|
+
y1 <= self.settings.eeprom.active_pixels_horizontal and
|
|
3062
|
+
x1 <= self.settings.eeprom.active_pixels_horizontal):
|
|
3063
|
+
log.error(f"invalid detector roi: {args}")
|
|
3064
|
+
return SpectrometerResponse(data=False, error_msg="invalid roi args")
|
|
3065
|
+
roi = DetectorROI(region, y0, y1, x0, x1)
|
|
3066
|
+
log.debug(f"created DetectorROI: {roi}")
|
|
3067
|
+
|
|
3068
|
+
# determine previous total pixels
|
|
3069
|
+
self.prev_pixels = self.settings.pixels()
|
|
3070
|
+
log.debug(f"prev_pixels = {self.prev_pixels}")
|
|
3071
|
+
|
|
3072
|
+
if store:
|
|
3073
|
+
if self.settings.state.detector_regions is None:
|
|
3074
|
+
log.debug("creating DetectorRegions")
|
|
3075
|
+
self.settings.state.detector_regions = DetectorRegions()
|
|
3076
|
+
|
|
3077
|
+
# this is a no-op if it's already present and unchanged
|
|
3078
|
+
log.debug("saving DetectorROI in DetectorRegions")
|
|
3079
|
+
self.settings.state.detector_regions.add(roi)
|
|
3080
|
+
|
|
3081
|
+
log.debug(f"total_pixels now {self.settings.pixels()}")
|
|
3082
|
+
|
|
3083
|
+
buf = utils.uint16_to_little_endian([ roi.y0, roi.y1, roi.x0, roi.x1 ])
|
|
3084
|
+
log.debug("would send buf: %s", buf)
|
|
3085
|
+
|
|
3086
|
+
result = self._send_code(bRequest = 0xff,
|
|
3087
|
+
wValue = 0x25,
|
|
3088
|
+
wIndex = roi.region,
|
|
3089
|
+
data_or_wLength = buf,
|
|
3090
|
+
label = "SET_DETECTOR_ROI")
|
|
3091
|
+
|
|
3092
|
+
log.debug("waiting 1sec...")
|
|
3093
|
+
sleep(1)
|
|
3094
|
+
|
|
3095
|
+
# Just in case, flows the updated DetectorRegions object upstream
|
|
3096
|
+
# so caller has access to it.
|
|
3097
|
+
if store:
|
|
3098
|
+
self.queue_message("detector_regions", self.settings.state.detector_regions)
|
|
3099
|
+
|
|
3100
|
+
return result
|
|
3101
|
+
|
|
3102
|
+
def get_fpga_configuration_register(self, label: str = ""):
|
|
3103
|
+
raw = self._get_code(0xb3, lsb_len=2, label="GET_FPGA_CONFIGURATION_REGISTER")
|
|
3104
|
+
log.debug(f"FPGA Configuration Register: 0x{raw:04x} ({label})")
|
|
3105
|
+
return raw
|
|
3106
|
+
|
|
3107
|
+
# ##########################################################################
|
|
3108
|
+
#
|
|
3109
|
+
# Accessory Connector
|
|
3110
|
+
#
|
|
3111
|
+
# ##########################################################################
|
|
3112
|
+
|
|
3113
|
+
# ##########################################################################
|
|
3114
|
+
# Accessory Enable
|
|
3115
|
+
# ##########################################################################
|
|
3116
|
+
|
|
3117
|
+
## @todo change opcode (conflicts with GET_DETECTOR_START_LINE)
|
|
3118
|
+
def set_accessory_enable(self, flag: bool):
|
|
3119
|
+
if not self.settings.is_gen15():
|
|
3120
|
+
log.debug("accessory requires Gen 1.5")
|
|
3121
|
+
return SpectrometerResponse(data=False, error_msg="requires gen1.5")
|
|
3122
|
+
value = 1 if flag else 0
|
|
3123
|
+
return self._send_code(bRequest = 0x22,
|
|
3124
|
+
wValue = value,
|
|
3125
|
+
wIndex = 0,
|
|
3126
|
+
data_or_wLength = [0] * 8,
|
|
3127
|
+
label = "SET_ACCESSORY_ENABLE")
|
|
3128
|
+
|
|
3129
|
+
## @todo find out opcode
|
|
3130
|
+
def get_discretes_enabled(self):
|
|
3131
|
+
if not self.settings.is_gen15():
|
|
3132
|
+
log.error("accessory requires Gen 1.5")
|
|
3133
|
+
return SpectrometerResponse(data=False, error_msg="requires gen1.5")
|
|
3134
|
+
# return self._get_code(0x37, label="GET_ACCESSORY_ENABLED", msb_len=1)
|
|
3135
|
+
|
|
3136
|
+
# ##########################################################################
|
|
3137
|
+
# Fan
|
|
3138
|
+
# ##########################################################################
|
|
3139
|
+
|
|
3140
|
+
def set_fan_enable(self, flag: bool):
|
|
3141
|
+
if not self.settings.is_gen15():
|
|
3142
|
+
log.debug("fan requires Gen 1.5")
|
|
3143
|
+
return SpectrometerResponse(data=False, error_msg="fan requires gen15.")
|
|
3144
|
+
value = 1 if flag else 0
|
|
3145
|
+
return self._send_code(bRequest = 0x36,
|
|
3146
|
+
wValue = value,
|
|
3147
|
+
wIndex = 0,
|
|
3148
|
+
data_or_wLength = [0] * 8,
|
|
3149
|
+
label = "SET_FAN_ENABLE")
|
|
3150
|
+
|
|
3151
|
+
def get_fan_enabled(self):
|
|
3152
|
+
if not self.settings.is_gen15():
|
|
3153
|
+
log.error("fan requires Gen 1.5")
|
|
3154
|
+
return SpectrometerResponse(data=False, error_msg="fan requires gen1.5")
|
|
3155
|
+
return SpectrometerResponse(data=0 != self._get_code(0x37, label="GET_FAN_ENABLED", msb_len=1))
|
|
3156
|
+
|
|
3157
|
+
# ##########################################################################
|
|
3158
|
+
# Lamp
|
|
3159
|
+
# ##########################################################################
|
|
3160
|
+
|
|
3161
|
+
def set_lamp_enable(self, flag: bool):
|
|
3162
|
+
if not self.settings.is_gen15():
|
|
3163
|
+
log.debug("lamp requires Gen 1.5")
|
|
3164
|
+
return SpectrometerResponse(data=False, error_msg="lamp requires gen1.5")
|
|
3165
|
+
value = 1 if flag else 0
|
|
3166
|
+
return self._send_code(bRequest = 0x32,
|
|
3167
|
+
wValue = value,
|
|
3168
|
+
wIndex = 0,
|
|
3169
|
+
data_or_wLength = [0] * 8,
|
|
3170
|
+
label = "SET_LAMP_ENABLE")
|
|
3171
|
+
|
|
3172
|
+
def get_lamp_enabled(self):
|
|
3173
|
+
if not self.settings.is_gen15():
|
|
3174
|
+
log.error("lamp requires Gen 1.5")
|
|
3175
|
+
return SpectrometerResponse(data=False, error_msg="lamp requires gen1.5")
|
|
3176
|
+
res = self._get_code(0x33, label="GET_LAMP_ENABLED", msb_len=1)
|
|
3177
|
+
res.data = 0 != res.data
|
|
3178
|
+
return res
|
|
3179
|
+
|
|
3180
|
+
# ##########################################################################
|
|
3181
|
+
# Shutter
|
|
3182
|
+
# ##########################################################################
|
|
3183
|
+
|
|
3184
|
+
def set_shutter_enable(self, flag: bool):
|
|
3185
|
+
if not (self.settings.is_gen15() and self.settings.eeprom.has_shutter):
|
|
3186
|
+
log.debug("shutter requires Gen 1.5 and has_shutter flag")
|
|
3187
|
+
return SpectrometerResponse(data=False, error_msg="shutter requires gen1.5")
|
|
3188
|
+
value = 1 if flag else 0
|
|
3189
|
+
return self._send_code(bRequest = 0x30,
|
|
3190
|
+
wValue = value,
|
|
3191
|
+
wIndex = 0,
|
|
3192
|
+
data_or_wLength = [0] * 8,
|
|
3193
|
+
label = "SET_SHUTTER_ENABLE")
|
|
3194
|
+
|
|
3195
|
+
def get_shutter_enabled(self):
|
|
3196
|
+
if not (self.settings.is_gen15() and self.settings.eeprom.has_shutter):
|
|
3197
|
+
log.debug("shutter requires Gen 1.5 and has_shutter flag")
|
|
3198
|
+
return SpectrometerResponse(data=False, error_msg="shutter requires gen1.5")
|
|
3199
|
+
res = SpectrometerResponse(data=0 != self._get_code(0x31, label="GET_SHUTTER_ENABLED", msb_len=1))
|
|
3200
|
+
res.data = 0 != res.data
|
|
3201
|
+
return res
|
|
3202
|
+
|
|
3203
|
+
# ##########################################################################
|
|
3204
|
+
# Laser Modulation and Continuous Strobe
|
|
3205
|
+
# ##########################################################################
|
|
3206
|
+
|
|
3207
|
+
def set_mod_enable(self, flag: bool):
|
|
3208
|
+
self.settings.state.mod_enabled = flag
|
|
3209
|
+
value = 1 if flag else 0
|
|
3210
|
+
return self._send_code(0xbd, value, label="SET_MOD_ENABLE")
|
|
3211
|
+
|
|
3212
|
+
def get_mod_enabled(self):
|
|
3213
|
+
res = self._get_code(0xe3, label="GET_MOD_ENABLED", msb_len=1)
|
|
3214
|
+
if res.error_msg != '':
|
|
3215
|
+
return res
|
|
3216
|
+
flag = 0 != res.data
|
|
3217
|
+
self.settings.state.mod_enabled = flag
|
|
3218
|
+
return SpectrometerResponse(data=flag)
|
|
3219
|
+
|
|
3220
|
+
def set_mod_period_us(self, us: float):
|
|
3221
|
+
self.settings.state.mod_period_us = us
|
|
3222
|
+
(lsw, msw, buf) = self._to40bit(us)
|
|
3223
|
+
return self._send_code(0xc7, lsw, msw, buf, label="SET_MOD_PERIOD")
|
|
3224
|
+
|
|
3225
|
+
def get_mod_period_us(self):
|
|
3226
|
+
result = self._get_code(0xcb, label="GET_MOD_PERIOD", lsb_len=5)
|
|
3227
|
+
self.settings.state.mod_period_us = result
|
|
3228
|
+
return result
|
|
3229
|
+
|
|
3230
|
+
def set_mod_width_us(self, us: float):
|
|
3231
|
+
self.settings.state.mod_width_us = us
|
|
3232
|
+
(lsw, msw, buf) = self._to40bit(us)
|
|
3233
|
+
return self._send_code(0xdb, lsw, msw, buf, label="SET_MOD_WIDTH")
|
|
3234
|
+
|
|
3235
|
+
def get_mod_width_us(self):
|
|
3236
|
+
result = self._get_code(0xdc, label="GET_MOD_WIDTH", lsb_len=5)
|
|
3237
|
+
self.settings.state.mod_width_us = result.data
|
|
3238
|
+
return result
|
|
3239
|
+
|
|
3240
|
+
def set_mod_delay_us(self, us: float):
|
|
3241
|
+
self.settings.state.mod_delay_us = us
|
|
3242
|
+
(lsw, msw, buf) = self._to40bit(us)
|
|
3243
|
+
return self._send_code(0xc6, lsw, msw, buf, label="SET_MOD_DELAY")
|
|
3244
|
+
|
|
3245
|
+
def get_mod_delay_us(self):
|
|
3246
|
+
result = self._get_code(0xca, label="GET_MOD_DELAY", lsb_len=5)
|
|
3247
|
+
self.settings.state.mod_delay_us = result.data
|
|
3248
|
+
return result
|
|
3249
|
+
|
|
3250
|
+
def set_mod_duration_us_NOT_USED(self, us: float):
|
|
3251
|
+
self.settings.state.mod_duration_us = us
|
|
3252
|
+
(lsw, msw, buf) = self._to40bit(us)
|
|
3253
|
+
return self._send_code(0xb9, lsw, msw, buf, label="SET_MOD_DURATION")
|
|
3254
|
+
|
|
3255
|
+
def get_mod_duration_us(self):
|
|
3256
|
+
result = self._get_code(0xc3, label="GET_MOD_DURATION", lsb_len=5)
|
|
3257
|
+
self.settings.state.mod_duration_us = result.data
|
|
3258
|
+
return result
|
|
3259
|
+
|
|
3260
|
+
## this is a synonym for _set_laser_enable_immediate(), but without side-effects
|
|
3261
|
+
def set_strobe_enable(self, flag: bool):
|
|
3262
|
+
value = 1 if flag else 0
|
|
3263
|
+
return self._send_code(0xbe, value, label="SET_STROBE_ENABLE")
|
|
3264
|
+
|
|
3265
|
+
## a literal pass-through to get_laser_enabled()
|
|
3266
|
+
def get_strobe_enabled(self):
|
|
3267
|
+
return self.get_laser_enabled()
|
|
3268
|
+
|
|
3269
|
+
# ##########################################################################
|
|
3270
|
+
# Ambient Temperature
|
|
3271
|
+
# ##########################################################################
|
|
3272
|
+
|
|
3273
|
+
def get_ambient_temperature_degC(self):
|
|
3274
|
+
if self.settings.is_gen15():
|
|
3275
|
+
return self.get_ambient_temperature_degC_gen15()
|
|
3276
|
+
elif self.settings.is_xs():
|
|
3277
|
+
return self.get_ambient_temperature_degC_arm()
|
|
3278
|
+
else:
|
|
3279
|
+
log.debug("ambient temperature requires XS or Gen 1.5")
|
|
3280
|
+
return
|
|
3281
|
+
|
|
3282
|
+
def get_ambient_temperature_degC_arm(self):
|
|
3283
|
+
if not self.settings.is_xs():
|
|
3284
|
+
msg = "ambient temperature ARM requires XS"
|
|
3285
|
+
log.debug(msg)
|
|
3286
|
+
return
|
|
3287
|
+
|
|
3288
|
+
if self.settings.microcontroller_firmware_version == "1.0.2.9":
|
|
3289
|
+
msg = "ambient temperature ARM requires newer firmware"
|
|
3290
|
+
log.debug(msg)
|
|
3291
|
+
return
|
|
3292
|
+
|
|
3293
|
+
result = self._get_code(0xff, 0x2a, label="GET_AMBIENT_TEMPERATURE_DEGC_ARM", msb_len=1)
|
|
3294
|
+
if result is None or result.data is None:
|
|
3295
|
+
msg = f"failed to read ambient temperature"
|
|
3296
|
+
log.error(msg)
|
|
3297
|
+
return SpectrometerResponse(error_msg=msg)
|
|
3298
|
+
degC = result.data
|
|
3299
|
+
|
|
3300
|
+
log.debug("ambient temperature degC ARM: %s", degC)
|
|
3301
|
+
return SpectrometerResponse(data=degC)
|
|
3302
|
+
|
|
3303
|
+
def get_ambient_temperature_degC_gen15(self):
|
|
3304
|
+
""" @see https://www.nxp.com/docs/en/data-sheet/LM75B.pdf """
|
|
3305
|
+
if not self.settings.is_gen15():
|
|
3306
|
+
msg = "ambient temperature Gen1.5 requires Gen 1.5"
|
|
3307
|
+
log.error(msg)
|
|
3308
|
+
return SpectrometerResponse(error_msg=msg)
|
|
3309
|
+
|
|
3310
|
+
result = self._get_code(0x35, label="GET_AMBIENT_TEMPERATURE_GEN15", msb_len=2)
|
|
3311
|
+
if result is None or len(result) != 2:
|
|
3312
|
+
msg = f"failed to read ambient temperature"
|
|
3313
|
+
log.error(msg)
|
|
3314
|
+
return SpectrometerResponse(error_msg=msg)
|
|
3315
|
+
log.debug("ambient temperature raw: %s", result)
|
|
3316
|
+
|
|
3317
|
+
raw = result.data
|
|
3318
|
+
raw = raw >> 5
|
|
3319
|
+
degC = 0.125 * utils.twos_comp(raw, 11)
|
|
3320
|
+
|
|
3321
|
+
log.debug("parsed ambient temperature from raw %s to %.3f degC", result, degC)
|
|
3322
|
+
return SpectrometerResponse(data=degC)
|
|
3323
|
+
|
|
3324
|
+
# ##########################################################################
|
|
3325
|
+
# added for wasatch-shell
|
|
3326
|
+
# ##########################################################################
|
|
3327
|
+
|
|
3328
|
+
def get_tec_enabled(self):
|
|
3329
|
+
if not self.settings.eeprom.has_cooling:
|
|
3330
|
+
log.error("unable to control TEC: EEPROM reports no cooling")
|
|
3331
|
+
return SpectrometerResponse(data=False, error_msg="no cooling reported")
|
|
3332
|
+
res = self._get_code(0xda, label="GET_CCD_TEC_ENABLED", msb_len=1)
|
|
3333
|
+
res.data = 0 != res.data
|
|
3334
|
+
if res.error_msg != '':
|
|
3335
|
+
return res
|
|
3336
|
+
return res
|
|
3337
|
+
|
|
3338
|
+
def get_actual_frames(self):
|
|
3339
|
+
return self._get_code(0xe4, label="GET_ACTUAL_FRAMES", lsb_len=2)
|
|
3340
|
+
|
|
3341
|
+
def get_actual_integration_time_us(self):
|
|
3342
|
+
return self._get_code(0xdf, label="GET_ACTUAL_INTEGRATION_TIME_US", lsb_len=3)
|
|
3343
|
+
|
|
3344
|
+
def get_detector_offset(self):
|
|
3345
|
+
value = self._get_code(0xc4, label="GET_DETECTOR_OFFSET", lsb_len=2)
|
|
3346
|
+
self.settings.eeprom.detector_offset = value.data
|
|
3347
|
+
return SpectrometerResponse(data=value)
|
|
3348
|
+
|
|
3349
|
+
def get_detector_offset_odd(self):
|
|
3350
|
+
if not self.settings.is_ingaas():
|
|
3351
|
+
log.debug("GET_DETECTOR_OFFSET_ODD only supported on InGaAs")
|
|
3352
|
+
return SpectrometerResponse(data=self.settings.eeprom.detector_offset_odd)
|
|
3353
|
+
|
|
3354
|
+
value = self._get_code(0x9e, label="GET_DETECTOR_OFFSET_ODD", lsb_len=2)
|
|
3355
|
+
if value.error_msg != '':
|
|
3356
|
+
return value
|
|
3357
|
+
self.settings.eeprom.detector_offset_odd = value.data
|
|
3358
|
+
return SpectrometerResponse(data=value)
|
|
3359
|
+
|
|
3360
|
+
def get_ccd_sensing_threshold(self):
|
|
3361
|
+
return self._get_code(0xd1, label="GET_CCD_SENSING_THRESHOLD", lsb_len=2)
|
|
3362
|
+
|
|
3363
|
+
def get_ccd_threshold_sensing_mode(self):
|
|
3364
|
+
return self._get_code(0xcf, label="GET_CCD_THRESHOLD_SENSING_MODE", msb_len=1)
|
|
3365
|
+
|
|
3366
|
+
def get_external_trigger_output(self):
|
|
3367
|
+
return self._get_code(0xe1, label="GET_EXTERNAL_TRIGGER_OUTPUT", msb_len=1)
|
|
3368
|
+
|
|
3369
|
+
def set_mod_linked_to_integration(self, flag: bool):
|
|
3370
|
+
value = 1 if flag else 0
|
|
3371
|
+
return self._send_code(0xdd, value, label="SET_MOD_LINKED_TO_INTEGRATION")
|
|
3372
|
+
|
|
3373
|
+
def get_mod_linked_to_integration(self):
|
|
3374
|
+
res = self._get_code(0xde, label="GET_MOD_LINKED_TO_INTEGRATION", msb_len=1)
|
|
3375
|
+
if res.error_msg != '':
|
|
3376
|
+
return res
|
|
3377
|
+
res.data = 0 != res.data
|
|
3378
|
+
return res
|
|
3379
|
+
|
|
3380
|
+
def get_selected_adc(self):
|
|
3381
|
+
value = self._get_code(0xee, label="GET_SELECTED_ADC", msb_len=1)
|
|
3382
|
+
if value.error_msg != '':
|
|
3383
|
+
return value
|
|
3384
|
+
if self.settings.state.selected_adc != value.data:
|
|
3385
|
+
log.error("GET_SELECTED_ADC %d != state.selected_adc %d", value, self.settings.state.selected_adc)
|
|
3386
|
+
self.settings.state.selected_adc = value.data
|
|
3387
|
+
return value
|
|
3388
|
+
|
|
3389
|
+
def set_trigger_delay(self, half_us: float):
|
|
3390
|
+
"""
|
|
3391
|
+
A confenurable delay from when an inbound trigger signal is
|
|
3392
|
+
received by the spectrometer, until the triggered acquisition actually starts.
|
|
3393
|
+
|
|
3394
|
+
Default value is 0us.
|
|
3395
|
+
|
|
3396
|
+
Unit is in 0.5 microseconds (500ns), so value of 25 would represent 12.5us.
|
|
3397
|
+
|
|
3398
|
+
Value is 24bit, so max value is 16777216 (8.388608 sec).
|
|
3399
|
+
|
|
3400
|
+
Like triggering, only currently supported on ARM.
|
|
3401
|
+
"""
|
|
3402
|
+
if not self.settings.is_arm():
|
|
3403
|
+
log.error("SET_TRIGGER_DELAY only supported on ARM")
|
|
3404
|
+
return False
|
|
3405
|
+
lsw = half_us & 0xffff
|
|
3406
|
+
msb = (half_us >> 16) & 0xff
|
|
3407
|
+
return self._send_code(0xaa, wValue=lsw, wIndex=msb, label="SET_TRIGGER_DELAY")
|
|
3408
|
+
|
|
3409
|
+
# not tested
|
|
3410
|
+
def get_trigger_delay(self):
|
|
3411
|
+
if not self.settings.is_arm():
|
|
3412
|
+
msg = "GET_TRIGGER_DELAY only supported on ARM"
|
|
3413
|
+
log.error(msg)
|
|
3414
|
+
return SpectrometerResponse(error_lvl=ErrorLevel.low, error_msg=msg)
|
|
3415
|
+
return self._get_code(0xe4, label="GET_TRIGGER_DELAY", lsb_len=3) # not sure about LSB
|
|
3416
|
+
|
|
3417
|
+
def get_vr_continuous_ccd(self):
|
|
3418
|
+
res = self._get_code(0xcc, label="GET_VR_CONTINUOUS_CCD", msb_len=1)
|
|
3419
|
+
if res.error_msg != '':
|
|
3420
|
+
return res
|
|
3421
|
+
res.data = 0 != res.data
|
|
3422
|
+
return res
|
|
3423
|
+
|
|
3424
|
+
def get_vr_num_frames(self):
|
|
3425
|
+
return self._get_code(0xcd, label="GET_VR_NUM_FRAMES", msb_len=1)
|
|
3426
|
+
|
|
3427
|
+
def get_opt_actual_integration_time(self):
|
|
3428
|
+
res = self.get_upper_code(0x0b, label="GET_OPT_ACT_INT_TIME", msb_len=1)
|
|
3429
|
+
if res.error_msg != '':
|
|
3430
|
+
return res
|
|
3431
|
+
res.data = 0 != res.data
|
|
3432
|
+
return res
|
|
3433
|
+
|
|
3434
|
+
def get_opt_area_scan(self):
|
|
3435
|
+
res = self.get_upper_code(0x0a, label="GET_OPT_AREA_SCAN", msb_len=1)
|
|
3436
|
+
if res.error_msg != '':
|
|
3437
|
+
return res
|
|
3438
|
+
res.data = 0 != res.data
|
|
3439
|
+
return res
|
|
3440
|
+
|
|
3441
|
+
def get_opt_cf_select(self):
|
|
3442
|
+
res = self.get_upper_code(0x07, label="GET_OPT_CF_SELECT", msb_len=1)
|
|
3443
|
+
if res.error_msg != '':
|
|
3444
|
+
return res
|
|
3445
|
+
res.data = 0 != res.data
|
|
3446
|
+
return res
|
|
3447
|
+
|
|
3448
|
+
def get_opt_data_header_tab(self):
|
|
3449
|
+
return self.get_upper_code(0x06, label="GET_OPT_DATA_HEADER_TAB", msb_len=1)
|
|
3450
|
+
|
|
3451
|
+
def get_opt_horizontal_binning(self):
|
|
3452
|
+
return self.get_upper_code(0x0c, label="GET_OPT_HORIZONTAL_BINNING", msb_len=1)
|
|
3453
|
+
|
|
3454
|
+
def get_opt_integration_time_resolution(self):
|
|
3455
|
+
return self.get_upper_code(0x05, label="GET_OPT_INTEGRATION_TIME_RESOLUTION", msb_len=1)
|
|
3456
|
+
|
|
3457
|
+
def update_firmware_log(self):
|
|
3458
|
+
if not self.settings.state.firmware_logging_enabled or not self.settings.is_xs():
|
|
3459
|
+
return
|
|
3460
|
+
|
|
3461
|
+
# read all pending lines
|
|
3462
|
+
lines = []
|
|
3463
|
+
start_time = datetime.datetime.now()
|
|
3464
|
+
max_ms = int(max(100, self.settings.state.integration_time_ms / 2))
|
|
3465
|
+
while True:
|
|
3466
|
+
response = self._get_code(0x81, label="GET_LOG")
|
|
3467
|
+
if response is None:
|
|
3468
|
+
break
|
|
3469
|
+
|
|
3470
|
+
data = response.data
|
|
3471
|
+
if data is None or len(data) == 0:
|
|
3472
|
+
break
|
|
3473
|
+
|
|
3474
|
+
line = ""
|
|
3475
|
+
for c in data[1:]:
|
|
3476
|
+
if c == 0:
|
|
3477
|
+
break
|
|
3478
|
+
line += chr(c)
|
|
3479
|
+
|
|
3480
|
+
if 0 == len(line):
|
|
3481
|
+
break
|
|
3482
|
+
|
|
3483
|
+
lines.append(line)
|
|
3484
|
+
|
|
3485
|
+
if (datetime.datetime.now() - start_time).total_seconds() * 1000 > max_ms:
|
|
3486
|
+
log.debug("update_firmware_log: enough for now")
|
|
3487
|
+
break
|
|
3488
|
+
|
|
3489
|
+
if len(lines) > 0:
|
|
3490
|
+
for line in lines:
|
|
3491
|
+
log.debug(f"update_firmware_log: {line}")
|
|
3492
|
+
self.queue_message("firmware_log", lines)
|
|
3493
|
+
|
|
3494
|
+
# ##########################################################################
|
|
3495
|
+
# Analog output
|
|
3496
|
+
# ##########################################################################
|
|
3497
|
+
|
|
3498
|
+
## @param value (Input) tuple of (bool enable, int mode)
|
|
3499
|
+
def set_analog_output_mode(self, value: tuple[bool, int]):
|
|
3500
|
+
if not self.settings.is_gen2():
|
|
3501
|
+
log.error("analog output only available on Gen2")
|
|
3502
|
+
return SpectrometerResponse(data=False, error_msg="analog output unsupported")
|
|
3503
|
+
|
|
3504
|
+
wIndex = 0
|
|
3505
|
+
|
|
3506
|
+
# parse enable and mode from value tuple
|
|
3507
|
+
try:
|
|
3508
|
+
if isinstance(value[0], bool):
|
|
3509
|
+
enable = value[0]
|
|
3510
|
+
else:
|
|
3511
|
+
enable = value[0] != 0
|
|
3512
|
+
|
|
3513
|
+
mode = int(value[1])
|
|
3514
|
+
if (mode != 0 and mode != 1):
|
|
3515
|
+
log.error("invalid analog output mode 0x%02x, disabling", mode)
|
|
3516
|
+
enable = False
|
|
3517
|
+
mode = 0
|
|
3518
|
+
|
|
3519
|
+
if enable:
|
|
3520
|
+
wIndex = 0x02 | mode # 0x02 sets the "enable" bit
|
|
3521
|
+
self.state.analog_out_enable = True
|
|
3522
|
+
self.state.analog_out_mode = mode
|
|
3523
|
+
self.state.analog_out_value = 0 if mode == 0 else 40 # 4mA default current in deci-mA
|
|
3524
|
+
else:
|
|
3525
|
+
wIndex = 0
|
|
3526
|
+
self.state.analog_out_enable = False
|
|
3527
|
+
self.state.analog_out_mode = 0
|
|
3528
|
+
self.state.analog_out_value = 0
|
|
3529
|
+
|
|
3530
|
+
except:
|
|
3531
|
+
log.error("set_analog_output_mode takes tuple of (bool, int), disabling")
|
|
3532
|
+
wIndex = 0
|
|
3533
|
+
|
|
3534
|
+
return self._send_code(bRequest = 0xff,
|
|
3535
|
+
wValue = 0x11,
|
|
3536
|
+
wIndex = wIndex,
|
|
3537
|
+
label = "SET_ANALOG_OUT_MODE")
|
|
3538
|
+
|
|
3539
|
+
def set_analog_output_value(self, value: int):
|
|
3540
|
+
if not self.settings.is_gen2():
|
|
3541
|
+
log.error("analog output only available on Gen2")
|
|
3542
|
+
return SpectrometerResponse(data=False, error_msg="analog output unsupported")
|
|
3543
|
+
|
|
3544
|
+
# spectrometer should range-limit, but just to codify:
|
|
3545
|
+
if self.state.analog_out_mode == 0:
|
|
3546
|
+
# voltage (decivolts, range 0-50 (0-5V))
|
|
3547
|
+
if value < 0:
|
|
3548
|
+
value = 0
|
|
3549
|
+
if value > 50:
|
|
3550
|
+
value = 50
|
|
3551
|
+
elif self.state.analog_out_mode == 1:
|
|
3552
|
+
# current (deci-mA, range 40-200 (4-20mA))
|
|
3553
|
+
if value < 40:
|
|
3554
|
+
value = 40
|
|
3555
|
+
elif value > 200:
|
|
3556
|
+
value = 200
|
|
3557
|
+
else:
|
|
3558
|
+
log.error("invalid analog out mode %d, ignoring value", self.state.analog_out_mode)
|
|
3559
|
+
return SpectrometerResponse(data=False, error_msg="invalid mode")
|
|
3560
|
+
|
|
3561
|
+
return self._send_code(bRequest = 0xff,
|
|
3562
|
+
wValue = 0x12,
|
|
3563
|
+
wIndex = value,
|
|
3564
|
+
label = "SET_ANALOG_OUT_VALUE")
|
|
3565
|
+
|
|
3566
|
+
def get_analog_output_state(self):
|
|
3567
|
+
if not self.settings.is_gen2():
|
|
3568
|
+
log.error("analog output only available on Gen2")
|
|
3569
|
+
return SpectrometerResponse(data=False, error_msg="analog output unsupported")
|
|
3570
|
+
|
|
3571
|
+
res = self.get_upper_code(0x1a, wLength=3, label="GET_ANALOG_OUT_STATE")
|
|
3572
|
+
if res.error_msg != '':
|
|
3573
|
+
return res
|
|
3574
|
+
data = res.data
|
|
3575
|
+
if (data is None or len(data) != 3):
|
|
3576
|
+
log.error("invalid analog out state read: %s", data)
|
|
3577
|
+
return SpectrometerResponse(data=False, error_msg="invalid analog out state read")
|
|
3578
|
+
|
|
3579
|
+
if data[0] != 0 and data[0] != 1:
|
|
3580
|
+
log.error("received invalid analog out enable: %d", data[0])
|
|
3581
|
+
if data[1] != 0 and data[1] != 1:
|
|
3582
|
+
log.error("received invalid analog out mode: %d", data[1])
|
|
3583
|
+
|
|
3584
|
+
self.state.analog_out_enable = data[0] != 0
|
|
3585
|
+
self.state.analog_out_mode = data[1]
|
|
3586
|
+
self.state.analog_out_value = data[2] # no range-checking applied
|
|
3587
|
+
data = (self.state.analog_out_enable,
|
|
3588
|
+
self.state.analog_out_mode,
|
|
3589
|
+
self.state.analog_out_value)
|
|
3590
|
+
return SpectrometerResponse(data=data)
|
|
3591
|
+
|
|
3592
|
+
# @returns decivolts
|
|
3593
|
+
def get_analog_input_value(self):
|
|
3594
|
+
if not self.settings.is_gen2():
|
|
3595
|
+
log.error("analog input only available on Gen2")
|
|
3596
|
+
return SpectrometerResponse(data=False, error_msg="analog input unsupported")
|
|
3597
|
+
return self.get_upper_code(0x1b, lsb_len=1, label="GET_ANALOG_IN_VALUE")
|
|
3598
|
+
|
|
3599
|
+
# ##########################################################################
|
|
3600
|
+
# EEPROM Cruft
|
|
3601
|
+
# ##########################################################################
|
|
3602
|
+
|
|
3603
|
+
def update_session_eeprom(self, pair: tuple[str, EEPROM]):
|
|
3604
|
+
"""
|
|
3605
|
+
Given a (serial_number, EEPROM) pair, update this process's "session"
|
|
3606
|
+
EEPROM with just the EDITABLE fields of the passed EEPROM.
|
|
3607
|
+
"""
|
|
3608
|
+
log.debug("fid.update_session_eeprom: %s updating EEPROM instance", self.settings.eeprom.serial_number)
|
|
3609
|
+
|
|
3610
|
+
if not self.eeprom_backup:
|
|
3611
|
+
self.eeprom_backup = copy.deepcopy(self.settings.eeprom)
|
|
3612
|
+
|
|
3613
|
+
self.settings.eeprom.update_editable(pair[1])
|
|
3614
|
+
return SpectrometerResponse(data=True)
|
|
3615
|
+
|
|
3616
|
+
def replace_session_eeprom(self, pair: tuple[str, EEPROM]):
|
|
3617
|
+
"""
|
|
3618
|
+
Given a (serial_number, EEPROM) pair, replace this process's "session"
|
|
3619
|
+
EEPROM with the passed EEPROM.
|
|
3620
|
+
"""
|
|
3621
|
+
log.debug("fid.replace_session_eeprom: %s replacing EEPROM instance", self.settings.eeprom.serial_number)
|
|
3622
|
+
|
|
3623
|
+
if not self.eeprom_backup:
|
|
3624
|
+
self.eeprom_backup = copy.deepcopy(self.settings.eeprom)
|
|
3625
|
+
|
|
3626
|
+
self.settings.eeprom = pair[1]
|
|
3627
|
+
self.settings.eeprom.dump()
|
|
3628
|
+
return SpectrometerResponse()
|
|
3629
|
+
|
|
3630
|
+
## Actually store the current session EEPROM fields to the spectrometer.
|
|
3631
|
+
def write_eeprom(self):
|
|
3632
|
+
if not self.eeprom_backup:
|
|
3633
|
+
log.critical("expected to update or replace EEPROM object before write command")
|
|
3634
|
+
self.queue_message("marquee_error", "Failed to write EEPROM")
|
|
3635
|
+
return SpectrometerResponse(data=False, error_msg="failed to write eeprom")
|
|
3636
|
+
|
|
3637
|
+
# backup contents of previous EEPROM in log
|
|
3638
|
+
log.debug("Original EEPROM contents")
|
|
3639
|
+
self.eeprom_backup.dump()
|
|
3640
|
+
log.debug("Original EEPROM buffers: %s", self.eeprom_backup.buffers)
|
|
3641
|
+
|
|
3642
|
+
try:
|
|
3643
|
+
self.settings.eeprom.generate_write_buffers()
|
|
3644
|
+
except:
|
|
3645
|
+
log.critical("failed to render EEPROM write buffers", exc_info=1)
|
|
3646
|
+
self.queue_message("marquee_error", "Failed to write EEPROM")
|
|
3647
|
+
return SpectrometerResponse(data=False, error_msg="failed to generate eeprom")
|
|
3648
|
+
|
|
3649
|
+
log.debug("Would write new buffers: %s", self.settings.eeprom.write_buffers)
|
|
3650
|
+
|
|
3651
|
+
for page in range(EEPROM.MAX_PAGES):
|
|
3652
|
+
if self.settings.is_arm():
|
|
3653
|
+
log.debug("writing page %d: %s", page, self.settings.eeprom.write_buffers[page])
|
|
3654
|
+
self._send_code(bRequest = 0xff, # second-tier
|
|
3655
|
+
wValue = 0x02,
|
|
3656
|
+
wIndex = page,
|
|
3657
|
+
data_or_wLength = self.settings.eeprom.write_buffers[page],
|
|
3658
|
+
label = "WRITE_EEPROM")
|
|
3659
|
+
else:
|
|
3660
|
+
DATA_START = 0x3c00
|
|
3661
|
+
offset = DATA_START + page * 64
|
|
3662
|
+
log.debug("writing page %d at offset 0x%04x: %s", page, offset, self.settings.eeprom.write_buffers[page])
|
|
3663
|
+
self._send_code(bRequest = 0xa2, # dangerous
|
|
3664
|
+
wValue = offset, # arguably an index but hey
|
|
3665
|
+
wIndex = 0,
|
|
3666
|
+
data_or_wLength = self.settings.eeprom.write_buffers[page],
|
|
3667
|
+
label = "WRITE_EEPROM")
|
|
3668
|
+
|
|
3669
|
+
self.queue_message("marquee_info", "EEPROM successfully updated")
|
|
3670
|
+
|
|
3671
|
+
# any value in doing this?
|
|
3672
|
+
self.settings.eeprom.buffers = self.settings.eeprom.write_buffers
|
|
3673
|
+
|
|
3674
|
+
return SpectrometerResponse(data=True)
|
|
3675
|
+
|
|
3676
|
+
# ##########################################################################
|
|
3677
|
+
# Intraprocess Communications
|
|
3678
|
+
# ##########################################################################
|
|
3679
|
+
|
|
3680
|
+
# @todo move string-to-enum converter to AppLog
|
|
3681
|
+
def set_log_level(self, s: str):
|
|
3682
|
+
lvl = logging.DEBUG if s == "DEBUG" else logging.INFO
|
|
3683
|
+
log.debug("fid.set_log_level: setting to %s", lvl)
|
|
3684
|
+
logging.getLogger().setLevel(lvl)
|
|
3685
|
+
return SpectrometerResponse()
|
|
3686
|
+
|
|
3687
|
+
def queue_message(self, setting, value):
|
|
3688
|
+
"""
|
|
3689
|
+
If an upstream queue is defined, send the name-value pair. Does nothing
|
|
3690
|
+
if the caller hasn't provided a queue.
|
|
3691
|
+
|
|
3692
|
+
"setting" is application (caller) dependent, but ENLIGHTEN currently uses
|
|
3693
|
+
"marquee_info" and "marquee_error".
|
|
3694
|
+
"""
|
|
3695
|
+
if self.message_queue is None:
|
|
3696
|
+
return SpectrometerResponse(data=False)
|
|
3697
|
+
|
|
3698
|
+
msg = StatusMessage(setting, value)
|
|
3699
|
+
try:
|
|
3700
|
+
self.message_queue.put(msg)
|
|
3701
|
+
except:
|
|
3702
|
+
log.error("failed to enqueue StatusMessage (%s, %s)", setting, value, exc_info=1)
|
|
3703
|
+
return SpectrometerResponse(data=False, error_msg="failed to enqueue messsage")
|
|
3704
|
+
return SpectrometerResponse(data=True)
|
|
3705
|
+
|
|
3706
|
+
def check_alert(self, s):
|
|
3707
|
+
log.debug(f"checking for alert {s}")
|
|
3708
|
+
self.refresh_alerts()
|
|
3709
|
+
if s in self.alerts:
|
|
3710
|
+
log.debug(f"found {s} (clearing)")
|
|
3711
|
+
self.alerts.remove(s)
|
|
3712
|
+
return True
|
|
3713
|
+
|
|
3714
|
+
def refresh_alerts(self):
|
|
3715
|
+
if self.alert_queue is None:
|
|
3716
|
+
return
|
|
3717
|
+
|
|
3718
|
+
if self.alert_queue.empty():
|
|
3719
|
+
return
|
|
3720
|
+
|
|
3721
|
+
while not self.alert_queue.empty():
|
|
3722
|
+
alert = self.alert_queue.get_nowait()
|
|
3723
|
+
if alert is None:
|
|
3724
|
+
continue
|
|
3725
|
+
elif isinstance(alert, ControlObject):
|
|
3726
|
+
if alert.value:
|
|
3727
|
+
log.debug(f"raised alert {alert.setting}")
|
|
3728
|
+
self.alerts.add(alert.setting)
|
|
3729
|
+
else:
|
|
3730
|
+
log.debug(f"cleared alert {alert.setting}")
|
|
3731
|
+
self.alerts.discard(alert.setting)
|
|
3732
|
+
else:
|
|
3733
|
+
log.error(f"non-ControlObject found in alerts_queue: {alert}")
|
|
3734
|
+
|
|
3735
|
+
def _init_process_funcs(self):
|
|
3736
|
+
"""
|
|
3737
|
+
Is it the expectation that all of these functions will return
|
|
3738
|
+
SpectrometerResponse? If so, that should be made explicit.
|
|
3739
|
+
"""
|
|
3740
|
+
process_f = {}
|
|
3741
|
+
|
|
3742
|
+
for fn_name in [
|
|
3743
|
+
"can_laser_fire",
|
|
3744
|
+
"clear_regions",
|
|
3745
|
+
"connect",
|
|
3746
|
+
"disconnect",
|
|
3747
|
+
"get_actual_frames",
|
|
3748
|
+
"get_actual_integration_time_us",
|
|
3749
|
+
"get_ambient_temperature_degC",
|
|
3750
|
+
"get_analog_input_value",
|
|
3751
|
+
"get_analog_output_state",
|
|
3752
|
+
"get_battery_charging",
|
|
3753
|
+
"get_battery_percentage",
|
|
3754
|
+
"get_battery_register",
|
|
3755
|
+
"get_battery_state_raw",
|
|
3756
|
+
"get_ble_firmware_version",
|
|
3757
|
+
"get_ccd_sensing_threshold",
|
|
3758
|
+
"get_ccd_threshold_sensing_mode",
|
|
3759
|
+
"get_dac",
|
|
3760
|
+
"get_detector_gain",
|
|
3761
|
+
"get_detector_gain_odd",
|
|
3762
|
+
"get_detector_offset",
|
|
3763
|
+
"get_detector_offset_odd",
|
|
3764
|
+
"get_detector_tec_setpoint_degC",
|
|
3765
|
+
"get_detector_tec_setpoint_raw",
|
|
3766
|
+
"get_detector_temperature_degC",
|
|
3767
|
+
"get_detector_temperature_raw",
|
|
3768
|
+
"get_discretes_enabled",
|
|
3769
|
+
"get_external_trigger_output",
|
|
3770
|
+
"get_fan_enabled",
|
|
3771
|
+
"get_fpga_configuration_register",
|
|
3772
|
+
"get_fpga_firmware_version",
|
|
3773
|
+
"get_high_gain_mode_enabled",
|
|
3774
|
+
"get_integration_time_ms",
|
|
3775
|
+
"get_lamp_enabled",
|
|
3776
|
+
"get_laser_enabled",
|
|
3777
|
+
"get_laser_interlock",
|
|
3778
|
+
"get_laser_tec_mode",
|
|
3779
|
+
"get_laser_temperature_degC",
|
|
3780
|
+
"get_laser_temperature_raw",
|
|
3781
|
+
"get_laser_temperature_setpoint_raw",
|
|
3782
|
+
"get_laser_warning_delay_sec",
|
|
3783
|
+
"get_laser_watchdog_sec",
|
|
3784
|
+
"get_line",
|
|
3785
|
+
"get_spectrum",
|
|
3786
|
+
"get_microcontroller_firmware_version",
|
|
3787
|
+
"get_mod_delay_us",
|
|
3788
|
+
"get_mod_duration_us",
|
|
3789
|
+
"get_mod_enabled",
|
|
3790
|
+
"get_mod_period_us",
|
|
3791
|
+
"get_mod_width_us",
|
|
3792
|
+
"get_opt_actual_integration_time",
|
|
3793
|
+
"get_opt_area_scan",
|
|
3794
|
+
"get_opt_cf_select",
|
|
3795
|
+
"get_opt_data_header_tab",
|
|
3796
|
+
"get_opt_has_laser",
|
|
3797
|
+
"get_opt_horizontal_binning",
|
|
3798
|
+
"get_opt_integration_time_resolution",
|
|
3799
|
+
"get_opt_laser_control",
|
|
3800
|
+
"get_raman_delay_ms",
|
|
3801
|
+
"get_secondary_adc_calibrated",
|
|
3802
|
+
"get_secondary_adc_raw",
|
|
3803
|
+
"get_selected_adc",
|
|
3804
|
+
"get_selected_laser",
|
|
3805
|
+
"get_sensor_line_length",
|
|
3806
|
+
"get_shutter_enabled",
|
|
3807
|
+
"get_strobe_enabled",
|
|
3808
|
+
"get_tec_enabled",
|
|
3809
|
+
"get_trigger_delay",
|
|
3810
|
+
"get_trigger_source",
|
|
3811
|
+
"get_vr_continuous_ccd",
|
|
3812
|
+
"get_vr_num_frames",
|
|
3813
|
+
"is_laser_firing",
|
|
3814
|
+
"queue_message",
|
|
3815
|
+
"replace_session_eeprom",
|
|
3816
|
+
"reset_fpga",
|
|
3817
|
+
"select_adc",
|
|
3818
|
+
"set_accessory_enable",
|
|
3819
|
+
"set_analog_output_mode",
|
|
3820
|
+
"set_analog_output_value",
|
|
3821
|
+
"set_area_scan_enable",
|
|
3822
|
+
"set_detector_gain",
|
|
3823
|
+
"set_detector_gain_odd",
|
|
3824
|
+
"set_detector_offset",
|
|
3825
|
+
"set_detector_offset_odd",
|
|
3826
|
+
"set_detector_roi",
|
|
3827
|
+
"set_detector_tec_setpoint_degC",
|
|
3828
|
+
"set_dfu_enable",
|
|
3829
|
+
"set_fan_enable",
|
|
3830
|
+
"set_high_gain_mode_enable",
|
|
3831
|
+
"set_integration_time_ms",
|
|
3832
|
+
"set_lamp_enable",
|
|
3833
|
+
"set_laser_enable",
|
|
3834
|
+
"set_laser_power_high_resolution",
|
|
3835
|
+
"set_laser_power_mW",
|
|
3836
|
+
"set_laser_power_perc",
|
|
3837
|
+
"set_laser_power_perc_immediate",
|
|
3838
|
+
"set_laser_power_require_modulation",
|
|
3839
|
+
"set_laser_tec_mode",
|
|
3840
|
+
"set_laser_temperature_setpoint_raw",
|
|
3841
|
+
"set_laser_watchdog_sec",
|
|
3842
|
+
"set_log_level",
|
|
3843
|
+
"set_mod_delay_us",
|
|
3844
|
+
"set_mod_enable",
|
|
3845
|
+
"set_mod_linked_to_integration",
|
|
3846
|
+
"set_mod_period_us",
|
|
3847
|
+
"set_mod_width_us",
|
|
3848
|
+
"set_pixel_mode",
|
|
3849
|
+
"set_raman_delay_ms",
|
|
3850
|
+
"set_selected_laser",
|
|
3851
|
+
"set_shutter_enable",
|
|
3852
|
+
"set_single_region",
|
|
3853
|
+
"set_strobe_enable",
|
|
3854
|
+
"set_tec_enable",
|
|
3855
|
+
"set_trigger_delay",
|
|
3856
|
+
"set_trigger_source",
|
|
3857
|
+
"set_vertical_roi",
|
|
3858
|
+
"update_laser_watchdog",
|
|
3859
|
+
"update_session_eeprom",
|
|
3860
|
+
"write_eeprom",
|
|
3861
|
+
]:
|
|
3862
|
+
process_f[fn_name] = getattr(self, fn_name)
|
|
3863
|
+
|
|
3864
|
+
##################################################################
|
|
3865
|
+
# What follows is the old init-lambdas that are squashed into process_f
|
|
3866
|
+
# Long term, the upstream requests should be changed to match the new format
|
|
3867
|
+
# This is an easy fix for the time being to make things behave
|
|
3868
|
+
##################################################################
|
|
3869
|
+
# spectrometer control
|
|
3870
|
+
process_f["laser_enable"] = lambda x: self.set_laser_enable(bool(x))
|
|
3871
|
+
process_f["integration_time_ms"] = lambda x: self.set_integration_time_ms(x)
|
|
3872
|
+
|
|
3873
|
+
process_f["detector_tec_setpoint_degC"] = lambda x: self.set_detector_tec_setpoint_degC(int(round(x)))
|
|
3874
|
+
process_f["detector_tec_enable"] = lambda x: self.set_tec_enable(bool(x))
|
|
3875
|
+
process_f["detector_gain"] = lambda x: self.set_detector_gain(float(x))
|
|
3876
|
+
process_f["detector_offset"] = lambda x: self.set_detector_offset(int(round(x)))
|
|
3877
|
+
process_f["detector_gain_odd"] = lambda x: self.set_detector_gain_odd(float(x))
|
|
3878
|
+
process_f["detector_offset_odd"] = lambda x: self.set_detector_offset_odd(int(round(x)))
|
|
3879
|
+
process_f["degC_to_dac_coeffs"] = lambda x: self.settings.eeprom.set("degC_to_dac_coeffs", x)
|
|
3880
|
+
|
|
3881
|
+
process_f["laser_power_perc"] = lambda x: self.set_laser_power_perc(x)
|
|
3882
|
+
process_f["laser_power_mW"] = lambda x: self.set_laser_power_mW(x)
|
|
3883
|
+
process_f["laser_temperature_setpoint_raw"] = lambda x: self.set_laser_temperature_setpoint_raw(int(round(x)))
|
|
3884
|
+
process_f["laser_power_attenuator"] = lambda x: self.set_laser_power_attenuator(int(round(x)))
|
|
3885
|
+
process_f["laser_power_high_resolution"] = lambda x: self.set_laser_power_high_resolution(x)
|
|
3886
|
+
process_f["laser_power_require_modulation"] = lambda x: self.set_laser_power_require_modulation(x)
|
|
3887
|
+
process_f["selected_laser"] = lambda x: self.set_selected_laser(int(x))
|
|
3888
|
+
process_f["set_laser_warning_delay_sec"] = lambda x: self.set_laser_warning_delay_sec(int(x))
|
|
3889
|
+
|
|
3890
|
+
process_f["high_gain_mode_enable"] = lambda x: self.set_high_gain_mode_enable(bool(x))
|
|
3891
|
+
process_f["trigger_source"] = lambda x: self.set_trigger_source(int(x))
|
|
3892
|
+
process_f["enable_secondary_adc"] = lambda x: self.settings.state.set("secondary_adc_enabled", bool(x))
|
|
3893
|
+
process_f["area_scan_enable"] = lambda x: self.set_area_scan_enable(bool(x))
|
|
3894
|
+
process_f["area_scan_line_step"] = lambda x: self.set_area_scan_line_step(int(x))
|
|
3895
|
+
process_f["area_scan_fast"] = lambda x: self.settings.state.set("area_scan_fast", bool(x))
|
|
3896
|
+
|
|
3897
|
+
process_f["bad_pixel_mode"] = lambda x: self.settings.state.set("bad_pixel_mode", int(x))
|
|
3898
|
+
process_f["min_usb_interval_ms"] = lambda x: self.settings.state.set("min_usb_interval_ms", int(round(x)))
|
|
3899
|
+
process_f["max_usb_interval_ms"] = lambda x: self.settings.state.set("max_usb_interval_ms", int(round(x)))
|
|
3900
|
+
|
|
3901
|
+
process_f["accessory_enable"] = lambda x: self.set_accessory_enable(bool(x))
|
|
3902
|
+
process_f["fan_enable"] = lambda x: self.set_fan_enable(bool(x))
|
|
3903
|
+
process_f["lamp_enable"] = lambda x: self.set_lamp_enable(bool(x))
|
|
3904
|
+
process_f["shutter_enable"] = lambda x: self.set_shutter_enable(bool(x))
|
|
3905
|
+
process_f["strobe_enable"] = lambda x: self.set_strobe_enable(bool(x))
|
|
3906
|
+
process_f["mod_enable"] = lambda x: self.set_mod_enable(bool(x))
|
|
3907
|
+
process_f["mod_period_us"] = lambda x: self.set_mod_period_us(int(round(x)))
|
|
3908
|
+
process_f["mod_width_us"] = lambda x: self.set_mod_width_us(int(round(x)))
|
|
3909
|
+
|
|
3910
|
+
# BatchCollection
|
|
3911
|
+
process_f["take_one_request"] = lambda x: self.settings.state.set("take_one_request", x)
|
|
3912
|
+
|
|
3913
|
+
# Series-XS
|
|
3914
|
+
#f["raman_mode_enable"] = lambda x: self.set_raman_mode_enable(bool(x))
|
|
3915
|
+
process_f["raman_delay_ms"] = lambda x: self.set_raman_delay_ms(int(round(x)))
|
|
3916
|
+
process_f["laser_watchdog_sec"] = lambda x: self.set_laser_watchdog_sec(int(round(x)))
|
|
3917
|
+
|
|
3918
|
+
# regions
|
|
3919
|
+
process_f["vertical_binning"] = lambda x: self.set_vertical_roi(x) # legacy alias
|
|
3920
|
+
process_f["vertical_roi"] = lambda x: self.set_vertical_roi(x)
|
|
3921
|
+
process_f["update_vertical_roi"] = lambda x: self.update_vertical_roi()
|
|
3922
|
+
process_f["single_region"] = lambda x: self.set_single_region(int(round(x)))
|
|
3923
|
+
process_f["clear_regions"] = lambda x: self.clear_regions()
|
|
3924
|
+
process_f["detector_roi"] = lambda x: self.set_detector_roi(x)
|
|
3925
|
+
process_f["pixel_mode"] = lambda x: self.set_pixel_mode(x)
|
|
3926
|
+
|
|
3927
|
+
# EEPROM updates
|
|
3928
|
+
process_f["update_eeprom"] = lambda x: self.update_session_eeprom(x)
|
|
3929
|
+
process_f["replace_eeprom"] = lambda x: self.replace_session_eeprom(x)
|
|
3930
|
+
process_f["write_eeprom"] = lambda x: self.write_eeprom()
|
|
3931
|
+
|
|
3932
|
+
# manufacturing
|
|
3933
|
+
process_f["reset_fpga"] = lambda x: self.reset_fpga()
|
|
3934
|
+
process_f["dfu_enable"] = lambda x: self.set_dfu_enable()
|
|
3935
|
+
|
|
3936
|
+
# legacy
|
|
3937
|
+
process_f["allow_default_gain_reset"] = lambda x: setattr(self, "allow_default_gain_reset", bool(x))
|
|
3938
|
+
|
|
3939
|
+
# experimental (R&D)
|
|
3940
|
+
#
|
|
3941
|
+
# MZ: note that anything calling self.settings.state.set() can probably
|
|
3942
|
+
# now be simplified as ENLIGHTEN has access to the same SpectrometerState object
|
|
3943
|
+
# (see firmware_logging_enabled)
|
|
3944
|
+
process_f["graph_alternating_pixels"] = lambda x: self.settings.state.set("graph_alternating_pixels", bool(x))
|
|
3945
|
+
process_f["swap_alternating_pixels"] = lambda x: self.settings.state.set("swap_alternating_pixels", bool(x))
|
|
3946
|
+
process_f["edc_enable"] = lambda x: self.settings.state.set("edc_enabled", bool(x))
|
|
3947
|
+
process_f["invert_x_axis"] = lambda x: self.settings.eeprom.set("invert_x_axis", bool(x))
|
|
3948
|
+
process_f["horiz_binning_enable"] = lambda x: self.settings.eeprom.set("horiz_binning_enabled", bool(x))
|
|
3949
|
+
process_f["wavenumber_correction"] = lambda x: self.settings.set_wavenumber_correction(float(x))
|
|
3950
|
+
process_f["linear_pixel_calibration"] = lambda x: self.settings.set_linear_pixel_calibration(x)
|
|
3951
|
+
process_f["onboard_scans_to_average"] = lambda x: self.set_onboard_scans_to_average(int(x))
|
|
3952
|
+
|
|
3953
|
+
# heartbeats & connection data
|
|
3954
|
+
process_f["raise_exceptions"] = lambda x: setattr(self, "raise_exceptions", bool(x))
|
|
3955
|
+
process_f["log_level"] = lambda x: self.set_log_level(x)
|
|
3956
|
+
process_f["num_connected_devices"] = lambda x: self.settings.set_num_connected_devices(int(x))
|
|
3957
|
+
process_f["subprocess_timeout_sec"] = lambda x: None
|
|
3958
|
+
process_f["heartbeat"] = lambda x: None
|
|
3959
|
+
process_f["reset"] = self.reset
|
|
3960
|
+
|
|
3961
|
+
return process_f
|