wasatch 2.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wasatch/AbstractUSBDevice.py +29 -0
- wasatch/AndorDevice.py +910 -0
- wasatch/AreaScanImage.py +19 -0
- wasatch/AutoRaman.py +457 -0
- wasatch/AutoRamanRequest.py +96 -0
- wasatch/BLEDevice.py +1032 -0
- wasatch/BalanceAcquisition.py +136 -0
- wasatch/CSVLoader.py +188 -0
- wasatch/CommandSettings.py +95 -0
- wasatch/ControlObject.py +17 -0
- wasatch/DetectorROI.py +36 -0
- wasatch/DetectorRegions.py +158 -0
- wasatch/DeviceFinderBLE.py +107 -0
- wasatch/DeviceFinderTCP.py +20 -0
- wasatch/DeviceFinderUSB.py +236 -0
- wasatch/DeviceID.py +344 -0
- wasatch/EEPROM.py +1450 -0
- wasatch/EEPROM_multical.sav +48 -0
- wasatch/FPGAOptions.py +125 -0
- wasatch/FeatureIdentificationDevice.py +3961 -0
- wasatch/FirmwareRequirements.py +76 -0
- wasatch/HardwareInfo.py +36 -0
- wasatch/IDSCamera.py +916 -0
- wasatch/IDSDevice.py +329 -0
- wasatch/IMX385.py +179 -0
- wasatch/InterfaceDevice.py +40 -0
- wasatch/MockUSBDevice.py +353 -0
- wasatch/OceanDevice.py +215 -0
- wasatch/PollStatus.py +15 -0
- wasatch/ProcessedReading.py +352 -0
- wasatch/ROI.py +33 -0
- wasatch/Reading.py +80 -0
- wasatch/RealUSBDevice.py +64 -0
- wasatch/SPIDevice.py +628 -0
- wasatch/SpectrometerRequest.py +15 -0
- wasatch/SpectrometerResponse.py +38 -0
- wasatch/SpectrometerSettings.py +446 -0
- wasatch/SpectrometerState.py +255 -0
- wasatch/StatusMessage.py +14 -0
- wasatch/TCPDevice.py +409 -0
- wasatch/TakeOneRequest.py +43 -0
- wasatch/WasatchBus.py +87 -0
- wasatch/WasatchDevice.py +1060 -0
- wasatch/WasatchDeviceWrapper.py +452 -0
- wasatch/WrapperWorker.py +304 -0
- wasatch/__init__.py +4 -0
- wasatch/applog.py +177 -0
- wasatch/simulation_protocol.py +227 -0
- wasatch/utils.py +731 -0
- wasatch-2.3.2.dist-info/LICENSE +21 -0
- wasatch-2.3.2.dist-info/METADATA +347 -0
- wasatch-2.3.2.dist-info/RECORD +53 -0
- wasatch-2.3.2.dist-info/WHEEL +4 -0
wasatch/EEPROM.py
ADDED
|
@@ -0,0 +1,1450 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
import struct
|
|
4
|
+
import array
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from . import utils
|
|
10
|
+
|
|
11
|
+
from .ROI import ROI
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# This class encapsulates the post-read parsing, pre-write marshalling, and current
|
|
17
|
+
# state of the 8-page EEPROM used to store non-volatile configuration data in Wasatch
|
|
18
|
+
# Photonics spectrometers. It is essential to keep this class synchronized (in naming,
|
|
19
|
+
# datatype / datasize and sequence) with the ENG-0034 customer-facing documentation.
|
|
20
|
+
#
|
|
21
|
+
# This class is normally accessed as an attribute of SpectrometerSettings.
|
|
22
|
+
#
|
|
23
|
+
# @see ENG-0034
|
|
24
|
+
# @see AT24C256C for ARM (32KB, http://ww1.microchip.com/downloads/en/DeviceDoc/20006270A.pdf)
|
|
25
|
+
# @see 24LC128 for FX2 (16KB, https://www.microchip.com/en-us/product/24LC128)
|
|
26
|
+
class EEPROM:
|
|
27
|
+
|
|
28
|
+
LATEST_REV = 17
|
|
29
|
+
|
|
30
|
+
MAX_PAGES = 8
|
|
31
|
+
PAGE_LENGTH = 64
|
|
32
|
+
SUBPAGE_COUNT = 4 # used for BLE
|
|
33
|
+
RAMAN_INTENSITY_CALIBRATION_ORDER = 5
|
|
34
|
+
MAX_BAD_PIXELS = 15
|
|
35
|
+
|
|
36
|
+
DEFAULT_LASER_WATCHDOG_SEC = 10
|
|
37
|
+
|
|
38
|
+
EEPROM_FIELDS = [
|
|
39
|
+
((0, 0, 16), "s", "model"),
|
|
40
|
+
((0, 16, 16), "s", "serial_number"),
|
|
41
|
+
((0, 32, 4), "I", "baud_rate"),
|
|
42
|
+
((0, 36, 1), "?", "has_cooling"),
|
|
43
|
+
((0, 37, 1), "?", "has_battery"),
|
|
44
|
+
((0, 38, 1), "?", "has_laser"),
|
|
45
|
+
((0, 39, 2), "H", "feature_mask"),
|
|
46
|
+
((0, 41, 2), "H", "slit_size_um"),
|
|
47
|
+
((0, 43, 2), "H", "startup_integration_time_ms"),
|
|
48
|
+
((0, 45, 2), "h", "startup_temp_degC"),
|
|
49
|
+
((0, 47, 1), "B", "startup_triggering_scheme"),
|
|
50
|
+
((0, 48, 4), "f", "detector_gain"),
|
|
51
|
+
((0, 52, 2), "h", "detector_offset"),
|
|
52
|
+
((0, 54, 4), "f", "detector_gain_odd"),
|
|
53
|
+
((0, 58, 2), "h", "detector_offset_odd"),
|
|
54
|
+
((0, 63, 1), "B", "format"),
|
|
55
|
+
((1, 0, 4), "f", "wavecal_c0"),
|
|
56
|
+
((1, 4, 4), "f", "wavecal_c1"),
|
|
57
|
+
((1, 8, 4), "f", "wavecal_c2"),
|
|
58
|
+
((1, 12, 4), "f", "wavecal_c3"),
|
|
59
|
+
((1, 16, 4), "f", "degCtoDAC_c0"),
|
|
60
|
+
((1, 20, 4), "f", "degCtoDAC_c1"),
|
|
61
|
+
((1, 24, 4), "f", "degCtoDAC_c2"),
|
|
62
|
+
((1, 28, 2), "h", "max_temp_degC"),
|
|
63
|
+
((1, 30, 2), "h", "min_temp_degC"),
|
|
64
|
+
((1, 32, 4), "f", "adcToDegC_c0"),
|
|
65
|
+
((1, 36, 4), "f", "adcToDegC_c1"),
|
|
66
|
+
((1, 40, 4), "f", "adcToDegC_c2"),
|
|
67
|
+
((1, 44, 2), "h", "tec_r298"),
|
|
68
|
+
((1, 46, 2), "h", "tec_beta"),
|
|
69
|
+
((1, 48, 12), "s", "calibration_date"),
|
|
70
|
+
((1, 60, 3), "s", "calibrated_by"),
|
|
71
|
+
((2, 0, 16), "s", "detector"),
|
|
72
|
+
((2, 16, 2), "H", "active_pixels_horizontal"),
|
|
73
|
+
((2, 18, 1), "B", "laser_warmup_sec"),
|
|
74
|
+
((2, 19, 2), "H", "active_pixels_vertical"),
|
|
75
|
+
((2, 21, 4), "f", "wavecal_c4"),
|
|
76
|
+
((2, 25, 2), "H", "actual_pixels_horizontal"),
|
|
77
|
+
((2, 27, 2), "H", "roi_horizontal_start"),
|
|
78
|
+
((2, 29, 2), "H", "roi_horizontal_end"),
|
|
79
|
+
((2, 31, 2), "H", "roi_vertical_region_1_start"),
|
|
80
|
+
((2, 33, 2), "H", "roi_vertical_region_1_end"),
|
|
81
|
+
((2, 35, 2), "H", "roi_vertical_region_2_start"),
|
|
82
|
+
((2, 37, 2), "H", "roi_vertical_region_2_end"),
|
|
83
|
+
((2, 39, 2), "H", "roi_vertical_region_3_start"),
|
|
84
|
+
((2, 41, 2), "H", "roi_vertical_region_3_end"),
|
|
85
|
+
((2, 43, 4), "f", "linearity_c0"),
|
|
86
|
+
((2, 47, 4), "f", "linearity_c1"),
|
|
87
|
+
((2, 51, 4), "f", "linearity_c2"),
|
|
88
|
+
((2, 55, 4), "f", "linearity_c3"),
|
|
89
|
+
((2, 59, 4), "f", "linearity_c4"),
|
|
90
|
+
((3, 12, 4), "f", "laser_power_c0"),
|
|
91
|
+
((3, 16, 4), "f", "laser_power_c1"),
|
|
92
|
+
((3, 20, 4), "f", "laser_power_c2"),
|
|
93
|
+
((3, 24, 4), "f", "laser_power_c3"),
|
|
94
|
+
((3, 28, 4), "f", "max_laser_power_mW"),
|
|
95
|
+
((3, 32, 4), "f", "min_laser_power_mW"),
|
|
96
|
+
((3, 36, 4), "f", "excitation_nm_float"),
|
|
97
|
+
((3, 40, 4), "I", "min_integration_time_ms"),
|
|
98
|
+
((3, 44, 4), "I", "max_integration_time_ms"),
|
|
99
|
+
((3, 48, 4), "f", "avg_resolution"),
|
|
100
|
+
((3, 52, 2), "H", "laser_watchdog_sec"),
|
|
101
|
+
((3, 55, 2), "H", "power_timeout_sec"),
|
|
102
|
+
((3, 57, 2), "H", "detector_timeout_sec"),
|
|
103
|
+
((3, 59, 1), "B", "horiz_binning_mode"),
|
|
104
|
+
((4, 0, 64), "s", "user_data"),
|
|
105
|
+
((5, 30, 16), "s", "product_configuration"),
|
|
106
|
+
((5, 63, 1), "B", "subformat")
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
def __init__(self):
|
|
110
|
+
|
|
111
|
+
self.model = None
|
|
112
|
+
self.serial_number = None
|
|
113
|
+
self.baud_rate = 0
|
|
114
|
+
self.has_cooling = False # explicitly means detector TEC, not laser
|
|
115
|
+
self.has_battery = False
|
|
116
|
+
self.has_laser = False
|
|
117
|
+
self.feature_mask = 0
|
|
118
|
+
self.invert_x_axis = False
|
|
119
|
+
self.horiz_binning_enabled = False
|
|
120
|
+
self.gen15 = False
|
|
121
|
+
self.cutoff_filter_installed = False
|
|
122
|
+
self.hardware_even_odd = False
|
|
123
|
+
self.sig_laser_tec = False # better: sig_sml_laser_tec
|
|
124
|
+
self.has_interlock_feedback = False
|
|
125
|
+
self.laser_interlock_excluded = False # True if the unit has a laser, but the normal safety interlock has been removed or otherwise disabled (OEM-only)
|
|
126
|
+
self.has_shutter = False
|
|
127
|
+
self.disable_ble_power = False
|
|
128
|
+
self.disable_laser_armed_indicator = False
|
|
129
|
+
self.excitation_nm = 0.0
|
|
130
|
+
self.excitation_nm_float = 0.0
|
|
131
|
+
self.slit_size_um = 0
|
|
132
|
+
self.startup_integration_time_ms = 10
|
|
133
|
+
self.startup_temp_degC = 15 # normally used for detector TEC; now also used for raw laser TEC on SiG (needs updated in ENG-0034)
|
|
134
|
+
self.startup_triggering_scheme = 0
|
|
135
|
+
self.startup_laser_tec_setpoint = 0
|
|
136
|
+
self.detector_gain = 1.9
|
|
137
|
+
self.detector_offset = 0
|
|
138
|
+
self.detector_gain_odd = 1.9
|
|
139
|
+
self.detector_offset_odd = 0
|
|
140
|
+
self.laser_warmup_sec = 0
|
|
141
|
+
self.laser_watchdog_sec = 0
|
|
142
|
+
self.light_source_type = 0
|
|
143
|
+
self.power_timeout_sec = 0
|
|
144
|
+
self.detector_timeout_sec = 0
|
|
145
|
+
self.horiz_binning_mode = 0
|
|
146
|
+
|
|
147
|
+
self.wavelength_coeffs = []
|
|
148
|
+
self.degC_to_dac_coeffs = []
|
|
149
|
+
self.adc_to_degC_coeffs = []
|
|
150
|
+
self.max_temp_degC = 20
|
|
151
|
+
self.min_temp_degC = 10
|
|
152
|
+
self.tec_r298 = 0
|
|
153
|
+
self.tec_beta = 0
|
|
154
|
+
self.calibration_date = None
|
|
155
|
+
self.calibrated_by = None
|
|
156
|
+
|
|
157
|
+
self.detector = None
|
|
158
|
+
self.detector_serial_number = None
|
|
159
|
+
self.active_pixels_horizontal = 1024 # number of pixels AFTER horizontal binning, BEFORE roi_horizontal cropping
|
|
160
|
+
self.active_pixels_vertical = 0
|
|
161
|
+
self.min_integration_time_ms = 10
|
|
162
|
+
self.max_integration_time_ms = 60000
|
|
163
|
+
self.actual_pixels_horizontal = 0 # number of pixels read-out over USB
|
|
164
|
+
self.actual_pixels_vertical = 0 # not a real EEPROM field, though it should be
|
|
165
|
+
self.roi_horizontal_start = 0
|
|
166
|
+
self.roi_horizontal_end = 0
|
|
167
|
+
self.roi_vertical_region_1_start = 0
|
|
168
|
+
self.roi_vertical_region_1_end = 0
|
|
169
|
+
self.roi_vertical_region_2_start = 0
|
|
170
|
+
self.roi_vertical_region_2_end = 0
|
|
171
|
+
self.roi_vertical_region_3_start = 0
|
|
172
|
+
self.roi_vertical_region_3_end = 0
|
|
173
|
+
self.linearity_coeffs = []
|
|
174
|
+
|
|
175
|
+
self.max_laser_power_mW = 0.0
|
|
176
|
+
self.min_laser_power_mW = 0.0
|
|
177
|
+
self.laser_power_coeffs = []
|
|
178
|
+
self.avg_resolution = 0.0
|
|
179
|
+
|
|
180
|
+
self.user_data = None
|
|
181
|
+
self.user_text = None
|
|
182
|
+
|
|
183
|
+
self.bad_pixels = [] # should be set, not list (but this works with EEPROMEditor)
|
|
184
|
+
self.product_configuration = None
|
|
185
|
+
|
|
186
|
+
self.format = EEPROM.LATEST_REV
|
|
187
|
+
self.subformat = 0 # determines format of pages 6-7
|
|
188
|
+
|
|
189
|
+
self.buffers = []
|
|
190
|
+
self.write_buffers = []
|
|
191
|
+
self.digest = None
|
|
192
|
+
self.stubbed = False
|
|
193
|
+
|
|
194
|
+
self.init_fields()
|
|
195
|
+
|
|
196
|
+
self.editable = [
|
|
197
|
+
"avg_resolution",
|
|
198
|
+
"bad_pixels",
|
|
199
|
+
"horiz_binning_enabled",
|
|
200
|
+
"calibrated_by",
|
|
201
|
+
"calibration_date",
|
|
202
|
+
"cutoff_filter_installed",
|
|
203
|
+
"detector_gain",
|
|
204
|
+
"detector_gain_odd",
|
|
205
|
+
"detector_offset",
|
|
206
|
+
"detector_offset_odd",
|
|
207
|
+
"excitation_nm",
|
|
208
|
+
"excitation_nm_float",
|
|
209
|
+
"gen15",
|
|
210
|
+
"has_shutter",
|
|
211
|
+
"laser_power_coeffs",
|
|
212
|
+
"laser_warmup_sec",
|
|
213
|
+
"laser_watchdog_sec",
|
|
214
|
+
"linearity_coeffs",
|
|
215
|
+
"max_laser_power_mW",
|
|
216
|
+
"min_laser_power_mW",
|
|
217
|
+
"raman_intensity_coeffs",
|
|
218
|
+
"roi_horizontal_end",
|
|
219
|
+
"roi_horizontal_start",
|
|
220
|
+
"roi_vertical_region_1_end",
|
|
221
|
+
"roi_vertical_region_1_start",
|
|
222
|
+
"roi_vertical_region_2_end",
|
|
223
|
+
"roi_vertical_region_2_start",
|
|
224
|
+
"roi_vertical_region_3_end",
|
|
225
|
+
"roi_vertical_region_3_start",
|
|
226
|
+
"user_text",
|
|
227
|
+
"wavelength_coeffs",
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
self.init_raman_intensity_calibration()
|
|
231
|
+
self.init_spline()
|
|
232
|
+
self.init_untethered()
|
|
233
|
+
self.multi_wavelength_calibration = MultiWavelengthCalibration(self)
|
|
234
|
+
|
|
235
|
+
def init_fields(self):
|
|
236
|
+
""" name-based lookup of page, offset, length and datatype """
|
|
237
|
+
self.fields = {}
|
|
238
|
+
for rec in self.EEPROM_FIELDS:
|
|
239
|
+
pos, data_type, name = rec
|
|
240
|
+
self.fields[name] = EEPROMField(pos, data_type, name)
|
|
241
|
+
|
|
242
|
+
# This flags on arrays
|
|
243
|
+
# if not hasattr(self, name):
|
|
244
|
+
# log.error(f"EEPROMField {name} not found in EEPROM?")
|
|
245
|
+
|
|
246
|
+
## whether the given field is normally editable by users via ENLIGHTEN
|
|
247
|
+
#
|
|
248
|
+
# @return False otherwise (don't trust in None's truthiness, as you can't
|
|
249
|
+
# pass None to Qt's setEnabled)
|
|
250
|
+
def is_editable(self, name): # -> bool
|
|
251
|
+
s = name.lower()
|
|
252
|
+
for field in self.editable:
|
|
253
|
+
if s == field.lower():
|
|
254
|
+
return True
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
##
|
|
258
|
+
# passed a temporary copy of another EEPROM object, copy-over any
|
|
259
|
+
# "editable" fields to this one
|
|
260
|
+
def update_editable(self, new_eeprom):
|
|
261
|
+
for field in self.editable:
|
|
262
|
+
old = getattr(self, field)
|
|
263
|
+
new = copy.deepcopy(getattr(new_eeprom, field))
|
|
264
|
+
if old == new:
|
|
265
|
+
log.debug(" %s: no change (%s == %s)", field, old, new)
|
|
266
|
+
else:
|
|
267
|
+
setattr(self, field, new)
|
|
268
|
+
log.debug(" %s: changed %s --> %s", field, old, new)
|
|
269
|
+
|
|
270
|
+
# ##########################################################################
|
|
271
|
+
# #
|
|
272
|
+
# Read EEPROM #
|
|
273
|
+
# #
|
|
274
|
+
# ##########################################################################
|
|
275
|
+
|
|
276
|
+
##
|
|
277
|
+
# given a set of the 8 buffers read from a spectrometer via USB,
|
|
278
|
+
# parse those into the approrpriate fields and datatypes
|
|
279
|
+
def parse(self, buffers):
|
|
280
|
+
if len(buffers) < EEPROM.MAX_PAGES:
|
|
281
|
+
log.error("EEPROM.parse expects at least %d buffers", EEPROM.MAX_PAGES)
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
# store these locally so self.unpack() can access them
|
|
285
|
+
self.buffers = buffers
|
|
286
|
+
self.digest = self.generate_digest()
|
|
287
|
+
|
|
288
|
+
# handy for debugs
|
|
289
|
+
self.hexbuf = [" ".join([f"{v:02x}" for v in buf]) for buf in buffers]
|
|
290
|
+
|
|
291
|
+
# check for known ne'er-do-well
|
|
292
|
+
bad = r"c2 47 05 31 21 00 00 04 00 03 00 00 02 31 a5 00 03 00 33 02 39 0f 00 03 00 43 02 2f 00 00 03 00 " \
|
|
293
|
+
+ r"4b 02 2b 23 00 03 00 53 02 2f 00 03 ff 01 00 90 e6 78 e0 54 10 ff c4 54 0f 44 50 f5 09 13 e4"
|
|
294
|
+
for i, s in enumerate(self.hexbuf):
|
|
295
|
+
if bad in s:
|
|
296
|
+
log.error(f"bad string found in EEPROM page {i}")
|
|
297
|
+
|
|
298
|
+
# unpack all the fields we know about
|
|
299
|
+
try:
|
|
300
|
+
self.read_eeprom()
|
|
301
|
+
return True
|
|
302
|
+
except:
|
|
303
|
+
log.error("failed to parse EEPROM", exc_info=1)
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
##
|
|
307
|
+
# Assuming a set of 8 buffers have been passed in via parse(), actually
|
|
308
|
+
# unpack (deserialize / unmarshall) the binary data into the appropriate
|
|
309
|
+
# fields and datatypes.
|
|
310
|
+
#
|
|
311
|
+
# @todo update to use self.fields
|
|
312
|
+
#
|
|
313
|
+
# @see https://docs.python.org/2/library/struct.html#format-characters
|
|
314
|
+
# (capitals are unsigned)
|
|
315
|
+
def read_eeprom(self):
|
|
316
|
+
self.format = self.unpack((0, 63, 1), "B", "format")
|
|
317
|
+
log.debug("parsing EEPROM format %d", self.format)
|
|
318
|
+
|
|
319
|
+
# ######################################################################
|
|
320
|
+
# Page 0
|
|
321
|
+
# ######################################################################
|
|
322
|
+
|
|
323
|
+
self.model = self.unpack((0, 0, 16), "s", "model")
|
|
324
|
+
self.serial_number = self.unpack((0, 16, 16), "s", "serial")
|
|
325
|
+
self.baud_rate = self.unpack((0, 32, 4), "I", "baud")
|
|
326
|
+
self.has_cooling = self.unpack((0, 36, 1), "?", "cooling")
|
|
327
|
+
self.has_battery = self.unpack((0, 37, 1), "?", "battery")
|
|
328
|
+
self.has_laser = self.unpack((0, 38, 1), "?", "laser")
|
|
329
|
+
if self.format > 9:
|
|
330
|
+
self.feature_mask = self.unpack((0, 39, 2), "H", "feature_mask")
|
|
331
|
+
elif self.format >= 3:
|
|
332
|
+
self.excitation_nm = self.unpack((0, 39, 2), "H", "excitation_nm (unsigned)")
|
|
333
|
+
else:
|
|
334
|
+
self.excitation_nm = self.unpack((0, 39, 2), "h", "excitation_nm (signed)")
|
|
335
|
+
|
|
336
|
+
if self.format >= 4:
|
|
337
|
+
self.slit_size_um = self.unpack((0, 41, 2), "H", "slit (unsigned)")
|
|
338
|
+
else:
|
|
339
|
+
self.slit_size_um = self.unpack((0, 41, 2), "h", "slit (signed)")
|
|
340
|
+
|
|
341
|
+
# NOTE: the new InGaAs detector gain/offset won't be usable from
|
|
342
|
+
# EEPROM until we start bumping production spectrometers to
|
|
343
|
+
# EEPROM Page 0 Revision 3!
|
|
344
|
+
if self.format >= 3:
|
|
345
|
+
self.startup_integration_time_ms = self.unpack((0, 43, 2), "H", "startup_integration_time_ms")
|
|
346
|
+
self.startup_temp_degC = self.unpack((0, 45, 2), "h", "startup_temp_degC")
|
|
347
|
+
self.startup_triggering_scheme = self.unpack((0, 47, 1), "B", "startup_triggering_scheme")
|
|
348
|
+
self.detector_gain = self.unpack((0, 48, 4), "f", "detector_gain") # "even pixels" for InGaAs
|
|
349
|
+
self.detector_offset = self.unpack((0, 52, 2), "h", "detector_offset") # "even pixels" for InGaAs
|
|
350
|
+
self.detector_gain_odd = self.unpack((0, 54, 4), "f", "detector_gain_odd") # InGaAs-only
|
|
351
|
+
self.detector_offset_odd = self.unpack((0, 58, 2), "h", "detector_offset_odd") # InGaAs-only
|
|
352
|
+
|
|
353
|
+
if self.format >= 16:
|
|
354
|
+
self.startup_laser_tec_setpoint = self.unpack((0, 60, 2), "H", "startup_laser_tec_setpoint") & 0xfff # XS-only
|
|
355
|
+
|
|
356
|
+
# ######################################################################
|
|
357
|
+
# Page 1
|
|
358
|
+
# ######################################################################
|
|
359
|
+
|
|
360
|
+
self.wavelength_coeffs = []
|
|
361
|
+
self.wavelength_coeffs .append(self.unpack((1, 0, 4), "f", "wavecal_coeff_0"))
|
|
362
|
+
self.wavelength_coeffs .append(self.unpack((1, 4, 4), "f"))
|
|
363
|
+
self.wavelength_coeffs .append(self.unpack((1, 8, 4), "f"))
|
|
364
|
+
self.wavelength_coeffs .append(self.unpack((1, 12, 4), "f"))
|
|
365
|
+
self.degC_to_dac_coeffs = []
|
|
366
|
+
self.degC_to_dac_coeffs .append(self.unpack((1, 16, 4), "f", "degCtoDAC_coeff_0"))
|
|
367
|
+
self.degC_to_dac_coeffs .append(self.unpack((1, 20, 4), "f"))
|
|
368
|
+
self.degC_to_dac_coeffs .append(self.unpack((1, 24, 4), "f"))
|
|
369
|
+
self.max_temp_degC = self.unpack((1, 28, 2), "h", "max_temp_degC")
|
|
370
|
+
self.min_temp_degC = self.unpack((1, 30, 2), "h", "min_temp_degC")
|
|
371
|
+
self.adc_to_degC_coeffs = []
|
|
372
|
+
self.adc_to_degC_coeffs .append(self.unpack((1, 32, 4), "f", "adcToDegC_coeff_0"))
|
|
373
|
+
self.adc_to_degC_coeffs .append(self.unpack((1, 36, 4), "f"))
|
|
374
|
+
self.adc_to_degC_coeffs .append(self.unpack((1, 40, 4), "f"))
|
|
375
|
+
self.tec_r298 = self.unpack((1, 44, 2), "h", "tec_r298")
|
|
376
|
+
self.tec_beta = self.unpack((1, 46, 2), "h", "tec_beta")
|
|
377
|
+
self.calibration_date = self.unpack((1, 48, 12), "s", "calibration_date")
|
|
378
|
+
self.calibrated_by = self.unpack((1, 60, 3), "s", "calibrated_by")
|
|
379
|
+
|
|
380
|
+
# ######################################################################
|
|
381
|
+
# Page 2
|
|
382
|
+
# ######################################################################
|
|
383
|
+
|
|
384
|
+
self.detector = self.unpack((2, 0, 16), "s", "detector")
|
|
385
|
+
self.active_pixels_horizontal = self.unpack((2, 16, 2), "H", "active_pixels_horizontal")
|
|
386
|
+
if self.format >= 10:
|
|
387
|
+
self.laser_warmup_sec = self.unpack((2, 18, 1), "B", "laser_warmup_sec")
|
|
388
|
+
self.active_pixels_vertical = self.unpack((2, 19, 2), "H" if self.format >= 4 else "h")
|
|
389
|
+
|
|
390
|
+
if self.format >= 8:
|
|
391
|
+
self.wavelength_coeffs .append(self.unpack((2, 21, 4), "f", "wavecal_coeff_4"))
|
|
392
|
+
else:
|
|
393
|
+
# just go ahead and initialize the 5th coeff to zero
|
|
394
|
+
self.wavelength_coeffs.append(0)
|
|
395
|
+
if self.format < 5:
|
|
396
|
+
self.min_integration_time_ms = self.unpack((2, 21, 2), "H", "min_integ(ushort)")
|
|
397
|
+
self.max_integration_time_ms = self.unpack((2, 23, 2), "H", "max_integ(ushort)")
|
|
398
|
+
|
|
399
|
+
self.actual_pixels_horizontal = self.unpack((2, 25, 2), "H" if self.format >= 4 else "h", "actual_pixels_horizontal")
|
|
400
|
+
self.actual_pixels_vertical = self.active_pixels_vertical # approximate for now
|
|
401
|
+
self.roi_horizontal_start = self.unpack((2, 27, 2), "H" if self.format >= 4 else "h")
|
|
402
|
+
self.roi_horizontal_end = self.unpack((2, 29, 2), "H" if self.format >= 4 else "h")
|
|
403
|
+
self.roi_vertical_region_1_start = self.unpack((2, 31, 2), "H" if self.format >= 4 else "h")
|
|
404
|
+
self.roi_vertical_region_1_end = self.unpack((2, 33, 2), "H" if self.format >= 4 else "h")
|
|
405
|
+
self.roi_vertical_region_2_start = self.unpack((2, 35, 2), "H" if self.format >= 4 else "h")
|
|
406
|
+
self.roi_vertical_region_2_end = self.unpack((2, 37, 2), "H" if self.format >= 4 else "h")
|
|
407
|
+
self.roi_vertical_region_3_start = self.unpack((2, 39, 2), "H" if self.format >= 4 else "h")
|
|
408
|
+
self.roi_vertical_region_3_end = self.unpack((2, 41, 2), "H" if self.format >= 4 else "h")
|
|
409
|
+
self.linearity_coeffs = []
|
|
410
|
+
self.linearity_coeffs .append(self.unpack((2, 43, 4), "f", "linearity_coeff_0")) # overloading for secondary ADC
|
|
411
|
+
self.linearity_coeffs .append(self.unpack((2, 47, 4), "f"))
|
|
412
|
+
self.linearity_coeffs .append(self.unpack((2, 51, 4), "f"))
|
|
413
|
+
self.linearity_coeffs .append(self.unpack((2, 55, 4), "f"))
|
|
414
|
+
self.linearity_coeffs .append(self.unpack((2, 59, 4), "f"))
|
|
415
|
+
|
|
416
|
+
# ######################################################################
|
|
417
|
+
# Page 3
|
|
418
|
+
# ######################################################################
|
|
419
|
+
|
|
420
|
+
self.laser_power_coeffs = []
|
|
421
|
+
self.laser_power_coeffs .append(self.unpack((3, 12, 4), "f", "laser_power_coeff_0"))
|
|
422
|
+
self.laser_power_coeffs .append(self.unpack((3, 16, 4), "f"))
|
|
423
|
+
self.laser_power_coeffs .append(self.unpack((3, 20, 4), "f"))
|
|
424
|
+
self.laser_power_coeffs .append(self.unpack((3, 24, 4), "f"))
|
|
425
|
+
self.max_laser_power_mW = self.unpack((3, 28, 4), "f", "max_laser_power_mW")
|
|
426
|
+
self.min_laser_power_mW = self.unpack((3, 32, 4), "f", "min_laser_power_mW")
|
|
427
|
+
|
|
428
|
+
if self.format >= 4:
|
|
429
|
+
self.excitation_nm_float = self.unpack((3, 36, 4), "f", "excitation(float)")
|
|
430
|
+
else:
|
|
431
|
+
self.excitation_nm_float = self.excitation_nm
|
|
432
|
+
|
|
433
|
+
if self.format >= 5:
|
|
434
|
+
self.min_integration_time_ms = self.unpack((3, 40, 4), "I", "min_integration_time_ms")
|
|
435
|
+
self.max_integration_time_ms = self.unpack((3, 44, 4), "I", "max_integration_time_ms")
|
|
436
|
+
|
|
437
|
+
if self.format >= 7:
|
|
438
|
+
self.avg_resolution = self.unpack((3, 48, 4), "f", "avg_resolution")
|
|
439
|
+
|
|
440
|
+
if self.format >= 15:
|
|
441
|
+
self.laser_watchdog_sec = self.unpack((3, 52, 2), "H", "laser_watchdog_sec")
|
|
442
|
+
self.light_source_type = self.unpack((3, 54, 1), "B", "light_source_type")
|
|
443
|
+
|
|
444
|
+
if self.format >= 16:
|
|
445
|
+
self.power_timeout_sec = self.unpack((3, 55, 2), "H", "power_timeout_sec")
|
|
446
|
+
self.detector_timeout_sec = self.unpack((3, 57, 2), "H", "detector_timeout_sec")
|
|
447
|
+
self.horiz_binning_mode = self.unpack((3, 59, 1), "B", "horiz_binning_mode")
|
|
448
|
+
|
|
449
|
+
# ######################################################################
|
|
450
|
+
# Page 4
|
|
451
|
+
# ######################################################################
|
|
452
|
+
|
|
453
|
+
self.user_data = self.buffers[4][:63]
|
|
454
|
+
self.user_text = self.printable(self.user_data)
|
|
455
|
+
|
|
456
|
+
# ######################################################################
|
|
457
|
+
# Page 5
|
|
458
|
+
# ######################################################################
|
|
459
|
+
|
|
460
|
+
bad = set()
|
|
461
|
+
for count in range(15):
|
|
462
|
+
pixel = self.unpack((5, count * 2, 2), "h")
|
|
463
|
+
if pixel != -1:
|
|
464
|
+
bad.add(pixel)
|
|
465
|
+
self.bad_pixels = list(bad)
|
|
466
|
+
self.bad_pixels.sort()
|
|
467
|
+
|
|
468
|
+
if self.format >= 5:
|
|
469
|
+
self.product_configuration = self.unpack((5, 30, 16), "s", "product_configuration")
|
|
470
|
+
if self.format >= 7:
|
|
471
|
+
self.subformat = self.unpack((5, 63, 1), "B", "subformat")
|
|
472
|
+
|
|
473
|
+
# ######################################################################
|
|
474
|
+
# Page 6-7
|
|
475
|
+
# ######################################################################
|
|
476
|
+
|
|
477
|
+
if self.subformat in [1, 5]:
|
|
478
|
+
self.read_raman_intensity_calibration()
|
|
479
|
+
|
|
480
|
+
if self.subformat == 2:
|
|
481
|
+
self.read_spline()
|
|
482
|
+
|
|
483
|
+
if self.subformat == 3:
|
|
484
|
+
self.read_untethered()
|
|
485
|
+
|
|
486
|
+
# Now that all other data has been loaded, re-initialize and load Multi-
|
|
487
|
+
# Wavelength Calibration (this way it can copy the "baseline" Raman
|
|
488
|
+
# intensity calibration, plus append new ones).
|
|
489
|
+
self.multi_wavelength_calibration.initialize()
|
|
490
|
+
if self.subformat == 5:
|
|
491
|
+
self.multi_wavelength_calibration.read(calibration=1)
|
|
492
|
+
|
|
493
|
+
# ######################################################################
|
|
494
|
+
# feature mask
|
|
495
|
+
# ######################################################################
|
|
496
|
+
|
|
497
|
+
if self.format >= 9:
|
|
498
|
+
self.invert_x_axis = 0 != self.feature_mask & 0x0001
|
|
499
|
+
self.horiz_binning_enabled = 0 != self.feature_mask & 0x0002
|
|
500
|
+
self.gen15 = 0 != self.feature_mask & 0x0004
|
|
501
|
+
self.cutoff_filter_installed = 0 != self.feature_mask & 0x0008
|
|
502
|
+
self.hardware_even_odd = 0 != self.feature_mask & 0x0010
|
|
503
|
+
self.sig_laser_tec = 0 != self.feature_mask & 0x0020
|
|
504
|
+
self.has_interlock_feedback = 0 != self.feature_mask & 0x0040
|
|
505
|
+
self.has_shutter = 0 != self.feature_mask & 0x0080
|
|
506
|
+
self.disable_ble_power = 0 != self.feature_mask & 0x0100
|
|
507
|
+
self.disable_laser_armed_indicator = 0 != self.feature_mask & 0x0200
|
|
508
|
+
self.laser_interlock_excluded = 0 != self.feature_mask & 0x0400
|
|
509
|
+
else:
|
|
510
|
+
self.invert_x_axis = False
|
|
511
|
+
self.horiz_binning_enabled = False
|
|
512
|
+
self.gen15 = False
|
|
513
|
+
self.cutoff_filter_installed = False
|
|
514
|
+
self.hardware_even_odd = False
|
|
515
|
+
self.sig_laser_tec = False
|
|
516
|
+
self.has_interlock_feedback = False
|
|
517
|
+
self.has_shutter = False
|
|
518
|
+
self.disable_ble_power = False
|
|
519
|
+
self.disable_laser_armed_indicator = False
|
|
520
|
+
|
|
521
|
+
# ######################################################################
|
|
522
|
+
# sanity checks
|
|
523
|
+
# ######################################################################
|
|
524
|
+
|
|
525
|
+
utils.clean_nan(self.wavelength_coeffs)
|
|
526
|
+
|
|
527
|
+
if self.min_integration_time_ms == 0xffff:
|
|
528
|
+
self.min_integration_time_ms = 1
|
|
529
|
+
self.max_integration_time_ms = 60000
|
|
530
|
+
|
|
531
|
+
if self.min_integration_time_ms > self.max_integration_time_ms:
|
|
532
|
+
(self.min_integration_time_ms, self.max_integration_time_ms) = \
|
|
533
|
+
(self.max_integration_time_ms, self.min_integration_time_ms)
|
|
534
|
+
|
|
535
|
+
if self.startup_integration_time_ms < self.min_integration_time_ms:
|
|
536
|
+
self.startup_integration_time_ms = self.min_integration_time_ms
|
|
537
|
+
|
|
538
|
+
if self.min_temp_degC > self.max_temp_degC:
|
|
539
|
+
(self.min_temp_degC, self.max_temp_degC) = \
|
|
540
|
+
(self.max_temp_degC, self.min_temp_degC)
|
|
541
|
+
|
|
542
|
+
if self.min_laser_power_mW > self.max_laser_power_mW:
|
|
543
|
+
(self.min_laser_power_mW, self.max_laser_power_mW) = \
|
|
544
|
+
(self.max_laser_power_mW, self.min_laser_power_mW)
|
|
545
|
+
|
|
546
|
+
############################################################################
|
|
547
|
+
# #
|
|
548
|
+
# Write EEPROM #
|
|
549
|
+
# #
|
|
550
|
+
############################################################################
|
|
551
|
+
|
|
552
|
+
def generate_feature_mask(self):
|
|
553
|
+
mask = 0
|
|
554
|
+
mask |= 0x0001 if self.invert_x_axis else 0
|
|
555
|
+
mask |= 0x0002 if self.horiz_binning_enabled else 0
|
|
556
|
+
mask |= 0x0004 if self.gen15 else 0
|
|
557
|
+
mask |= 0x0008 if self.cutoff_filter_installed else 0
|
|
558
|
+
mask |= 0x0010 if self.hardware_even_odd else 0
|
|
559
|
+
mask |= 0x0020 if self.sig_laser_tec else 0
|
|
560
|
+
mask |= 0x0040 if self.has_interlock_feedback else 0
|
|
561
|
+
mask |= 0x0080 if self.has_shutter else 0
|
|
562
|
+
mask |= 0x0100 if self.disable_ble_power else 0
|
|
563
|
+
mask |= 0x0200 if self.disable_laser_armed_indicator else 0
|
|
564
|
+
mask |= 0x0400 if self.laser_interlock_excluded else 0
|
|
565
|
+
return mask
|
|
566
|
+
|
|
567
|
+
##
|
|
568
|
+
# Call this to populate an internal array of "write buffers" which may be written back
|
|
569
|
+
# to spectrometers (or used to generate the digest of what WOULD be written).
|
|
570
|
+
#
|
|
571
|
+
# @todo update to use self.fields
|
|
572
|
+
def generate_write_buffers(self):
|
|
573
|
+
# stub-out 8 blank buffers
|
|
574
|
+
self.write_buffers = []
|
|
575
|
+
for page in range(EEPROM.MAX_PAGES):
|
|
576
|
+
self.write_buffers.append(array.array('B', [0] * 64))
|
|
577
|
+
|
|
578
|
+
# Eventually we'll stop worrying about the legacy per-page format versions, but
|
|
579
|
+
# for now maximize compatibility with StrokerConsole/ModelConfigurationFormat.cs
|
|
580
|
+
# by making its expected format values the default for each page:
|
|
581
|
+
revs = { 0: 1,
|
|
582
|
+
1: 1,
|
|
583
|
+
2: 2,
|
|
584
|
+
3: 255,
|
|
585
|
+
4: 1,
|
|
586
|
+
5: 1,
|
|
587
|
+
6: 0 }
|
|
588
|
+
for page in list(revs.keys()):
|
|
589
|
+
self.write_buffers[page][63] = revs[page]
|
|
590
|
+
|
|
591
|
+
# ...but the truth is that we don't really care about the old per-page formats,
|
|
592
|
+
# and all modern code should just be looking at this one byte:
|
|
593
|
+
self.write_buffers[0][63] = EEPROM.LATEST_REV
|
|
594
|
+
|
|
595
|
+
# ######################################################################
|
|
596
|
+
# Page 0
|
|
597
|
+
# ######################################################################
|
|
598
|
+
|
|
599
|
+
self.pack((0, 0, 16), "s", self.model)
|
|
600
|
+
self.pack((0, 16, 16), "s", self.serial_number)
|
|
601
|
+
self.pack((0, 32, 4), "I", self.baud_rate)
|
|
602
|
+
self.pack((0, 36, 1), "?", self.has_cooling)
|
|
603
|
+
self.pack((0, 37, 1), "?", self.has_battery)
|
|
604
|
+
self.pack((0, 38, 1), "?", self.has_laser)
|
|
605
|
+
self.pack((0, 39, 2), "H", self.generate_feature_mask(), "FeatureMask")
|
|
606
|
+
self.pack((0, 41, 2), "H", self.slit_size_um)
|
|
607
|
+
self.pack((0, 43, 2), "H", self.startup_integration_time_ms)
|
|
608
|
+
self.pack((0, 45, 2), "h", self.startup_temp_degC)
|
|
609
|
+
self.pack((0, 47, 1), "B", self.startup_triggering_scheme)
|
|
610
|
+
self.pack((0, 48, 4), "f", self.detector_gain)
|
|
611
|
+
self.pack((0, 52, 2), "h", self.detector_offset)
|
|
612
|
+
self.pack((0, 54, 4), "f", self.detector_gain_odd)
|
|
613
|
+
self.pack((0, 58, 2), "h", self.detector_offset_odd)
|
|
614
|
+
self.pack((0, 60, 2), "H", self.startup_laser_tec_setpoint)
|
|
615
|
+
|
|
616
|
+
# ######################################################################
|
|
617
|
+
# Page 1
|
|
618
|
+
# ######################################################################
|
|
619
|
+
|
|
620
|
+
wavelength_coeffs = self.multi_wavelength_calibration.get("wavelength_coeffs")
|
|
621
|
+
if wavelength_coeffs is not None:
|
|
622
|
+
for i in range(min(4, len(wavelength_coeffs))):
|
|
623
|
+
self.pack((1, 0 + i * 4, 4), "f", wavelength_coeffs[i])
|
|
624
|
+
|
|
625
|
+
if self.degC_to_dac_coeffs is not None:
|
|
626
|
+
for i in range(min(3, len(self.degC_to_dac_coeffs))):
|
|
627
|
+
self.pack((1, 16 + i * 4, 4), "f", self.degC_to_dac_coeffs[i])
|
|
628
|
+
|
|
629
|
+
if self.adc_to_degC_coeffs is not None:
|
|
630
|
+
for i in range(min(3, len(self.adc_to_degC_coeffs))):
|
|
631
|
+
self.pack((1, 32 + i * 4, 4), "f", self.adc_to_degC_coeffs[i])
|
|
632
|
+
|
|
633
|
+
self.pack((1, 28, 2), "h", self.max_temp_degC)
|
|
634
|
+
self.pack((1, 30, 2), "h", self.min_temp_degC)
|
|
635
|
+
self.pack((1, 44, 2), "h", self.tec_r298)
|
|
636
|
+
self.pack((1, 46, 2), "h", self.tec_beta)
|
|
637
|
+
self.pack((1, 48, 12), "s", self.calibration_date)
|
|
638
|
+
self.pack((1, 60, 3), "s", self.calibrated_by)
|
|
639
|
+
|
|
640
|
+
# ######################################################################
|
|
641
|
+
# Page 2
|
|
642
|
+
# ######################################################################
|
|
643
|
+
|
|
644
|
+
self.pack((2, 0, 16), "s", self.detector)
|
|
645
|
+
self.pack((2, 16, 2), "H", self.active_pixels_horizontal)
|
|
646
|
+
self.pack((2, 18, 1), "B", self.laser_warmup_sec)
|
|
647
|
+
self.pack((2, 19, 2), "H", self.active_pixels_vertical)
|
|
648
|
+
if self.format < 7:
|
|
649
|
+
self.pack((2, 21, 2), "H", max(0xffff, self.min_integration_time_ms))
|
|
650
|
+
self.pack((2, 23, 2), "H", max(0xffff, self.max_integration_time_ms))
|
|
651
|
+
else:
|
|
652
|
+
coeff = 0.0
|
|
653
|
+
if len(wavelength_coeffs) > 4:
|
|
654
|
+
coeff = wavelength_coeffs[4]
|
|
655
|
+
self.pack((2, 21, 4), "f", coeff)
|
|
656
|
+
self.pack((2, 25, 2), "H", self.actual_pixels_horizontal)
|
|
657
|
+
self.pack((2, 27, 2), "H", self.multi_wavelength_calibration.get("roi_horizontal_start"))
|
|
658
|
+
self.pack((2, 29, 2), "H", self.multi_wavelength_calibration.get("roi_horizontal_end"))
|
|
659
|
+
self.pack((2, 31, 2), "H", self.roi_vertical_region_1_start)
|
|
660
|
+
self.pack((2, 33, 2), "H", self.roi_vertical_region_1_end)
|
|
661
|
+
self.pack((2, 35, 2), "H", self.roi_vertical_region_2_start)
|
|
662
|
+
self.pack((2, 37, 2), "H", self.roi_vertical_region_2_end)
|
|
663
|
+
self.pack((2, 39, 2), "H", self.roi_vertical_region_3_start)
|
|
664
|
+
self.pack((2, 41, 2), "H", self.roi_vertical_region_3_end)
|
|
665
|
+
|
|
666
|
+
if self.linearity_coeffs is not None:
|
|
667
|
+
for i in range(min(5, len(self.linearity_coeffs))):
|
|
668
|
+
self.pack((2, 43 + i * 4, 4), "f", self.linearity_coeffs[i])
|
|
669
|
+
|
|
670
|
+
# ######################################################################
|
|
671
|
+
# Page 3
|
|
672
|
+
# ######################################################################
|
|
673
|
+
|
|
674
|
+
if self.laser_power_coeffs is not None:
|
|
675
|
+
for i in range(min(4, len(self.laser_power_coeffs))):
|
|
676
|
+
self.pack((3, 12 + i * 4, 4), "f", self.laser_power_coeffs[i])
|
|
677
|
+
|
|
678
|
+
self.pack((3, 28, 4), "f", self.max_laser_power_mW)
|
|
679
|
+
self.pack((3, 32, 4), "f", self.min_laser_power_mW)
|
|
680
|
+
self.pack((3, 36, 4), "f", self.multi_wavelength_calibration.get("excitation_nm_float"))
|
|
681
|
+
self.pack((3, 40, 4), "I", self.min_integration_time_ms)
|
|
682
|
+
self.pack((3, 44, 4), "I", self.max_integration_time_ms)
|
|
683
|
+
self.pack((3, 48, 4), "f", self.multi_wavelength_calibration.get("avg_resolution"))
|
|
684
|
+
self.pack((3, 52, 2), "H", self.laser_watchdog_sec)
|
|
685
|
+
self.pack((3, 54, 1), "B", self.light_source_type)
|
|
686
|
+
self.pack((3, 55, 2), "H", self.power_timeout_sec)
|
|
687
|
+
self.pack((3, 57, 2), "H", self.detector_timeout_sec)
|
|
688
|
+
self.pack((3, 59, 1), "B", self.multi_wavelength_calibration.get("horiz_binning_mode"))
|
|
689
|
+
|
|
690
|
+
# ######################################################################
|
|
691
|
+
# Page 4
|
|
692
|
+
# ######################################################################
|
|
693
|
+
|
|
694
|
+
self.pack((4, 0, 63), "s", self.user_text)
|
|
695
|
+
|
|
696
|
+
# ######################################################################
|
|
697
|
+
# Page 5
|
|
698
|
+
# ######################################################################
|
|
699
|
+
|
|
700
|
+
bad_pixel_set = set()
|
|
701
|
+
for i in self.bad_pixels:
|
|
702
|
+
if i >= 0:
|
|
703
|
+
bad_pixel_set.add(i)
|
|
704
|
+
bad_pixels = list(bad_pixel_set)
|
|
705
|
+
bad_pixels.sort()
|
|
706
|
+
for i in range(self.MAX_BAD_PIXELS):
|
|
707
|
+
if i < len(bad_pixels):
|
|
708
|
+
value = bad_pixels[i]
|
|
709
|
+
else:
|
|
710
|
+
value = -1
|
|
711
|
+
self.pack((5, i * 2, 2), "h", value)
|
|
712
|
+
|
|
713
|
+
self.pack((5, 30, 16), "s", self.product_configuration)
|
|
714
|
+
self.pack((5, 63, 1), "B", self.subformat)
|
|
715
|
+
|
|
716
|
+
# ######################################################################
|
|
717
|
+
# Page 6-7
|
|
718
|
+
# ######################################################################
|
|
719
|
+
|
|
720
|
+
if self.subformat == 0:
|
|
721
|
+
pass
|
|
722
|
+
elif self.subformat == 1:
|
|
723
|
+
self.write_raman_intensity_calibration()
|
|
724
|
+
elif self.subformat == 2:
|
|
725
|
+
self.write_spline()
|
|
726
|
+
elif self.subformat == 3:
|
|
727
|
+
self.write_untethered()
|
|
728
|
+
elif self.subformat == 4:
|
|
729
|
+
log.critical("Subformat 4 is deprecated")
|
|
730
|
+
elif self.subformat == 5:
|
|
731
|
+
self.write_raman_intensity_calibration()
|
|
732
|
+
self.multi_wavelength_calibration.write()
|
|
733
|
+
else:
|
|
734
|
+
log.error(f"Unwriteable EEPROM subformat {self.subformat}")
|
|
735
|
+
|
|
736
|
+
# ##########################################################################
|
|
737
|
+
# #
|
|
738
|
+
# Utility methods #
|
|
739
|
+
# #
|
|
740
|
+
# ##########################################################################
|
|
741
|
+
|
|
742
|
+
## make a printable ASCII string out of possibly-binary data
|
|
743
|
+
def printable(self, buf):
|
|
744
|
+
s = ""
|
|
745
|
+
for c in buf:
|
|
746
|
+
if 31 < c < 127:
|
|
747
|
+
s += chr(c)
|
|
748
|
+
elif c == 0:
|
|
749
|
+
break
|
|
750
|
+
else:
|
|
751
|
+
s += '.'
|
|
752
|
+
return s
|
|
753
|
+
|
|
754
|
+
def set(self, name, value):
|
|
755
|
+
setattr(self, name, value)
|
|
756
|
+
|
|
757
|
+
##
|
|
758
|
+
# Convert a floating-point value into the big-endian 16-bit "funky float"
|
|
759
|
+
# used for detector gain in the FPGA on both Hamamatsu and IMX sensors.
|
|
760
|
+
#
|
|
761
|
+
# Note that this TRUNCATES (takes the floor) the fractional portion rather
|
|
762
|
+
# than rounding, which I believe matches WasatchNET.FunkyFloat.fromFloat().
|
|
763
|
+
#
|
|
764
|
+
# Conversely, it DOES provide minor rounding on the INTEGRAL portion, to
|
|
765
|
+
# avoid this problem:
|
|
766
|
+
#
|
|
767
|
+
# \verbatim
|
|
768
|
+
# dB = 0
|
|
769
|
+
# while dB < 2.1:
|
|
770
|
+
# self.set_detector_gain(dB) # old code treats 1.0 as 0.0 because actually sends 0.99999999
|
|
771
|
+
# dB += 0.1
|
|
772
|
+
# \endverbatim
|
|
773
|
+
#
|
|
774
|
+
# @see https://wasatchphotonics.com/api/Wasatch.NET/class_wasatch_n_e_t_1_1_funky_float.html
|
|
775
|
+
def float_to_uint16(self, gain):
|
|
776
|
+
msb = int(round(gain, 5)) & 0xff
|
|
777
|
+
# if self.format >= 13: lsb = round((gain - msb) * 256) & 0xff
|
|
778
|
+
lsb = int((gain - msb) * 256) & 0xff
|
|
779
|
+
raw = (msb << 8) | lsb
|
|
780
|
+
log.debug("float_to_uint16: %f -> 0x%04x", gain, raw)
|
|
781
|
+
return raw
|
|
782
|
+
|
|
783
|
+
##
|
|
784
|
+
# Unpack a single field at a given buffer offset of the given datatype.
|
|
785
|
+
#
|
|
786
|
+
# @param address a tuple of the form (buf, offset, len)
|
|
787
|
+
# @param data_type see https://docs.python.org/2/library/struct.html#format-characters
|
|
788
|
+
# @param label if provided, is included in debug log output
|
|
789
|
+
def unpack(self, address, data_type, label=None):
|
|
790
|
+
page = address[0]
|
|
791
|
+
start_byte = address[1]
|
|
792
|
+
length = address[2]
|
|
793
|
+
end_byte = start_byte + length
|
|
794
|
+
|
|
795
|
+
if page > len(self.buffers):
|
|
796
|
+
log.error("error unpacking EEPROM page %d, offset %d, len %d as %s: invalid page (label %s)",
|
|
797
|
+
page, start_byte, length, data_type, label, exc_info=1)
|
|
798
|
+
return
|
|
799
|
+
|
|
800
|
+
buf = self.buffers[page]
|
|
801
|
+
if buf is None or end_byte > len(buf):
|
|
802
|
+
log.error("error unpacking EEPROM page %d, offset %d, len %d as %s: buf is %s (label %s)",
|
|
803
|
+
page, start_byte, length, data_type, buf, label, exc_info=1)
|
|
804
|
+
return
|
|
805
|
+
|
|
806
|
+
if data_type == "s":
|
|
807
|
+
# This stops at the first NULL, so is not appropriate for binary data (user_data).
|
|
808
|
+
# OTOH, it doesn't currently enforce "printable" characters either (nor support Unicode).
|
|
809
|
+
unpack_result = ""
|
|
810
|
+
for c in buf[start_byte:end_byte]:
|
|
811
|
+
if c == 0:
|
|
812
|
+
break
|
|
813
|
+
unpack_result += chr(c)
|
|
814
|
+
else:
|
|
815
|
+
unpack_result = 0
|
|
816
|
+
try:
|
|
817
|
+
unpack_result = struct.unpack(data_type, buf[start_byte:end_byte])[0]
|
|
818
|
+
except:
|
|
819
|
+
log.error("error unpacking EEPROM page %d, offset %d, len %d as %s", page, start_byte, length, data_type, exc_info=1)
|
|
820
|
+
|
|
821
|
+
if label is None:
|
|
822
|
+
log.debug("Unpacked [%s]: %s", data_type, unpack_result)
|
|
823
|
+
else:
|
|
824
|
+
log.debug("Unpacked [%s]: %s (%s)", data_type, unpack_result, label)
|
|
825
|
+
return unpack_result
|
|
826
|
+
|
|
827
|
+
##
|
|
828
|
+
# Marshall or serialize a single field at a given buffer offset of the given datatype.
|
|
829
|
+
#
|
|
830
|
+
# @param address a tuple of the form (buf, offset, len)
|
|
831
|
+
# @param data_type see https://docs.python.org/2/library/struct.html#format-characters
|
|
832
|
+
# @param value value to serialize
|
|
833
|
+
def pack(self, address, data_type, value, label=None):
|
|
834
|
+
page = address[0]
|
|
835
|
+
start_byte = address[1]
|
|
836
|
+
length = address[2]
|
|
837
|
+
end_byte = start_byte + length
|
|
838
|
+
|
|
839
|
+
if page > len(self.write_buffers):
|
|
840
|
+
log.error("error unpacking EEPROM page %d, offset %d, len %d as %s: invalid page (label %s)",
|
|
841
|
+
page, start_byte, length, data_type, label, exc_info=1)
|
|
842
|
+
return
|
|
843
|
+
|
|
844
|
+
# don't try to write negatives to unsigned types
|
|
845
|
+
if data_type in ["H", "I"] and value < 0:
|
|
846
|
+
log.error("rounding negative to zero when writing to unsigned field (address %s, data_type %s, value %s)", address, data_type, value)
|
|
847
|
+
value = 0
|
|
848
|
+
|
|
849
|
+
buf = self.write_buffers[page]
|
|
850
|
+
if buf is None or end_byte > 64:
|
|
851
|
+
raise Exception("error packing EEPROM page %d, offset %2d, len %2d as %s: buf is %s" % (
|
|
852
|
+
page, start_byte, length, data_type, buf))
|
|
853
|
+
|
|
854
|
+
if data_type == "s":
|
|
855
|
+
if value is None:
|
|
856
|
+
value = ""
|
|
857
|
+
for i in range(min(length, len(value))):
|
|
858
|
+
if i < len(value):
|
|
859
|
+
buf[start_byte + i] = ord(value[i])
|
|
860
|
+
else:
|
|
861
|
+
buf[start_byte + i] = 0
|
|
862
|
+
else:
|
|
863
|
+
struct.pack_into(data_type, buf, start_byte, value)
|
|
864
|
+
|
|
865
|
+
if False:
|
|
866
|
+
extra = "" if label is None else (" (%s)" % label)
|
|
867
|
+
log.debug("Packed (%d, %2d, %2d) '%s' value %s -> %s%s",
|
|
868
|
+
page, start_byte, length, data_type, value, buf[start_byte:end_byte], extra)
|
|
869
|
+
|
|
870
|
+
##
|
|
871
|
+
# If asked to regenerate, return a digest of the contents that WOULD BE
|
|
872
|
+
# WRITTEN from current settings in memory.
|
|
873
|
+
def generate_digest(self, regenerate=False):
|
|
874
|
+
buffers = self.buffers
|
|
875
|
+
digest = 'invalid'
|
|
876
|
+
try:
|
|
877
|
+
if regenerate:
|
|
878
|
+
self.generate_write_buffers()
|
|
879
|
+
buffers = self.write_buffers
|
|
880
|
+
|
|
881
|
+
h = hashlib.new("md5")
|
|
882
|
+
for buf in buffers:
|
|
883
|
+
h.update(bytes(buf))
|
|
884
|
+
digest = h.hexdigest()
|
|
885
|
+
except:
|
|
886
|
+
log.error(f"exception generating EEPROM digest...using '{digest}'", exc_info=1)
|
|
887
|
+
|
|
888
|
+
# log.debug(f"EEPROM MD5 digest {digest} (regenerate {regenerate})")
|
|
889
|
+
return digest
|
|
890
|
+
|
|
891
|
+
def to_dict(self):
|
|
892
|
+
d = {}
|
|
893
|
+
for k, v in self.__dict__.items():
|
|
894
|
+
if k not in ["user_data", "buffers", "write_buffers", "editable"]:
|
|
895
|
+
d[k] = v
|
|
896
|
+
return d
|
|
897
|
+
|
|
898
|
+
## render the attributes of this object as a JSON string
|
|
899
|
+
#
|
|
900
|
+
# @note some callers may prefer SpectrometerSettings.to_dict() or to_json()
|
|
901
|
+
def json(self, allow_nan=True):
|
|
902
|
+
tmp_buf = self.buffers
|
|
903
|
+
tmp_data = self.user_data
|
|
904
|
+
tmp_fields = self.fields
|
|
905
|
+
tmp_mwc = self.multi_wavelength_calibration
|
|
906
|
+
|
|
907
|
+
self.buffers = str(self.buffers)
|
|
908
|
+
self.user_data = str(self.user_data)
|
|
909
|
+
self.fields = None
|
|
910
|
+
self.multi_wavelength_calibration = self.multi_wavelength_calibration.toJSON()
|
|
911
|
+
|
|
912
|
+
# this does take an allow_nan argument, but it throws an exception on NaN,
|
|
913
|
+
# rather than replacing with null :-(
|
|
914
|
+
# https://stackoverflow.com/questions/6601812/sending-nan-in-json
|
|
915
|
+
s = json.dumps(self.__dict__, indent=2, sort_keys=True)
|
|
916
|
+
if not allow_nan:
|
|
917
|
+
s = re.sub(r"\bNaN\b", "null", s)
|
|
918
|
+
|
|
919
|
+
self.fields = tmp_fields
|
|
920
|
+
self.buffers = tmp_buf
|
|
921
|
+
self.user_data = tmp_data
|
|
922
|
+
self.multi_wavelength_calibration = tmp_mwc
|
|
923
|
+
|
|
924
|
+
return s
|
|
925
|
+
|
|
926
|
+
## log this object
|
|
927
|
+
def dump(self):
|
|
928
|
+
log.debug("EEPROM settings:")
|
|
929
|
+
log.debug(" Model: %s", self.model)
|
|
930
|
+
log.debug(" Serial Number: %s", self.serial_number)
|
|
931
|
+
log.debug(" Baud Rate: %d", self.baud_rate)
|
|
932
|
+
log.debug(" Has Cooling: %s", self.has_cooling)
|
|
933
|
+
log.debug(" Has Battery: %s", self.has_battery)
|
|
934
|
+
log.debug(" Has Laser: %s", self.has_laser)
|
|
935
|
+
log.debug(" Invert X-Axis: %s", self.invert_x_axis)
|
|
936
|
+
log.debug(" Horiz Bin Enable: %s", self.horiz_binning_enabled)
|
|
937
|
+
log.debug(" Gen 1.5: %s", self.gen15)
|
|
938
|
+
log.debug(" Cutoff Filter: %s", self.cutoff_filter_installed)
|
|
939
|
+
log.debug(" HW Even/Odd: %s", self.hardware_even_odd)
|
|
940
|
+
log.debug(" SiG Laser TEC: %s", self.sig_laser_tec)
|
|
941
|
+
log.debug(" Int'Lck Feedback: %s", self.has_interlock_feedback)
|
|
942
|
+
log.debug(" Shutter: %s", self.has_shutter)
|
|
943
|
+
log.debug(" Disable BLE Power:%s", self.disable_ble_power)
|
|
944
|
+
log.debug(" Dis Laser Arm Ind:%s", self.disable_laser_armed_indicator)
|
|
945
|
+
log.debug(" Excitation (f): %.2f nm", self.multi_wavelength_calibration.get("excitation_nm_float", default=0))
|
|
946
|
+
log.debug(" Laser Warmup Sec: %d", self.laser_warmup_sec)
|
|
947
|
+
log.debug(" Laser Watchdog: %d", self.laser_watchdog_sec)
|
|
948
|
+
log.debug(" Light Source: %d", self.light_source_type)
|
|
949
|
+
log.debug(" Power Timeout: %d", self.power_timeout_sec)
|
|
950
|
+
log.debug(" Detector Timeout: %d", self.detector_timeout_sec)
|
|
951
|
+
log.debug(" Horiz Bin Mode: %d", self.multi_wavelength_calibration.get("horiz_binning_mode", default=0))
|
|
952
|
+
log.debug(" Slit size: %s um", self.slit_size_um)
|
|
953
|
+
log.debug(" Start Integ Time: %d ms", self.startup_integration_time_ms)
|
|
954
|
+
log.debug(" Start Temp: %.2f degC", self.startup_temp_degC)
|
|
955
|
+
log.debug(" Start Triggering: 0x%04x", self.startup_triggering_scheme)
|
|
956
|
+
log.debug(" Det Gain: %f", self.detector_gain)
|
|
957
|
+
log.debug(" Det Offset: %d", self.detector_offset)
|
|
958
|
+
log.debug(" Det Gain Odd: %f", self.detector_gain_odd)
|
|
959
|
+
log.debug(" Det Offset Odd: %d", self.detector_offset_odd)
|
|
960
|
+
log.debug(" Start Laser TEC: %d (raw)", self.startup_laser_tec_setpoint)
|
|
961
|
+
log.debug("")
|
|
962
|
+
log.debug(" Wavecal coeffs: %s", self.multi_wavelength_calibration.get("wavelength_coeffs"))
|
|
963
|
+
log.debug(" degCToDAC coeffs: %s", self.degC_to_dac_coeffs)
|
|
964
|
+
log.debug(" adcToDegC coeffs: %s", self.adc_to_degC_coeffs)
|
|
965
|
+
log.debug(" Det temp max: %s degC", self.max_temp_degC)
|
|
966
|
+
log.debug(" Det temp min: %s degC", self.min_temp_degC)
|
|
967
|
+
log.debug(" TEC R298: %s", self.tec_r298)
|
|
968
|
+
log.debug(" TEC beta: %s", self.tec_beta)
|
|
969
|
+
log.debug(" Calibration Date: %s", self.calibration_date)
|
|
970
|
+
log.debug(" Calibration By: %s", self.calibrated_by)
|
|
971
|
+
log.debug("")
|
|
972
|
+
log.debug(" Detector name: %s", self.detector)
|
|
973
|
+
log.debug(" Active Px Horiz: %d", self.active_pixels_horizontal)
|
|
974
|
+
log.debug(" Active Px Vert: %d", self.active_pixels_vertical)
|
|
975
|
+
log.debug(" Actual Px Horiz: %d", self.actual_pixels_horizontal)
|
|
976
|
+
log.debug(" Actual Px Vert: %d", self.actual_pixels_vertical)
|
|
977
|
+
log.debug(" Min integration: %d ms", self.min_integration_time_ms)
|
|
978
|
+
log.debug(" Max integration: %d ms", self.max_integration_time_ms)
|
|
979
|
+
log.debug(" ROI Horiz Start: %d", self.multi_wavelength_calibration.get("roi_horizontal_start", default=-1))
|
|
980
|
+
log.debug(" ROI Horiz End: %d", self.multi_wavelength_calibration.get("roi_horizontal_end", default=-1))
|
|
981
|
+
log.debug(" ROI Vert Reg 1: (%d, %d)", self.roi_vertical_region_1_start, self.roi_vertical_region_1_end)
|
|
982
|
+
log.debug(" ROI Vert Reg 2: (%d, %d)", self.roi_vertical_region_2_start, self.roi_vertical_region_2_end)
|
|
983
|
+
log.debug(" ROI Vert Reg 3: (%d, %d)", self.roi_vertical_region_3_start, self.roi_vertical_region_3_end)
|
|
984
|
+
log.debug(" Linearity Coeffs: %s", self.linearity_coeffs)
|
|
985
|
+
log.debug("")
|
|
986
|
+
log.debug(" Laser coeffs: %s", self.laser_power_coeffs)
|
|
987
|
+
log.debug(" Max Laser Power: %s mW", self.max_laser_power_mW)
|
|
988
|
+
log.debug(" Min Laser Power: %s mW", self.min_laser_power_mW)
|
|
989
|
+
log.debug(" Avg Resolution: %.2f", self.multi_wavelength_calibration.get("avg_resolution", default=0))
|
|
990
|
+
log.debug("")
|
|
991
|
+
log.debug(" User Text: %s", self.user_text)
|
|
992
|
+
log.debug("")
|
|
993
|
+
log.debug(" Bad Pixels: %s", self.bad_pixels)
|
|
994
|
+
log.debug(" Product Config: %s", self.product_configuration)
|
|
995
|
+
|
|
996
|
+
if self.subformat == 1:
|
|
997
|
+
self.dump_raman_intensity_calibration()
|
|
998
|
+
elif self.subformat == 2:
|
|
999
|
+
self.dump_spline()
|
|
1000
|
+
elif self.subformat == 3:
|
|
1001
|
+
self.dump_raman_intensity_calibration()
|
|
1002
|
+
self.dump_untethered()
|
|
1003
|
+
elif self.subformat == 5:
|
|
1004
|
+
self.multi_wavelength_calibration.dump()
|
|
1005
|
+
|
|
1006
|
+
# ##########################################################################
|
|
1007
|
+
# #
|
|
1008
|
+
# Convenience accessors #
|
|
1009
|
+
# #
|
|
1010
|
+
# ##########################################################################
|
|
1011
|
+
|
|
1012
|
+
def latest_rev(self):
|
|
1013
|
+
return EEPROM.LATEST_REV
|
|
1014
|
+
|
|
1015
|
+
def get_horizontal_roi(self):
|
|
1016
|
+
start = self.multi_wavelength_calibration.get("roi_horizontal_start", default=-1)
|
|
1017
|
+
end = self.multi_wavelength_calibration.get("roi_horizontal_end", default=-1)
|
|
1018
|
+
|
|
1019
|
+
if 0 <= start and start < end and end < self.active_pixels_horizontal:
|
|
1020
|
+
return ROI(start, end)
|
|
1021
|
+
|
|
1022
|
+
##
|
|
1023
|
+
# On a 1024-pixel detector, note the expected / correct result based on the
|
|
1024
|
+
# roi_horizontal_start/stop fields:
|
|
1025
|
+
#
|
|
1026
|
+
# - (0, 1024) FALSE (last pixel invalid)
|
|
1027
|
+
# - (0, 1023) TRUE (even though no vignetting is occuring)
|
|
1028
|
+
# - (0, 1022) TRUE (crops last pixel)
|
|
1029
|
+
# - (1, 1023) TRUE (crops first pixel)
|
|
1030
|
+
# - (1, 1024) FALSE (last pixel invalid)
|
|
1031
|
+
# - (1, 1) FALSE (start must < end)
|
|
1032
|
+
# - (1, 2) TRUE (valid 2-pixel spectrum)
|
|
1033
|
+
# - (2, 1) FALSE (start must < end)
|
|
1034
|
+
#
|
|
1035
|
+
# @return whether a valid horizontal ROI is configured
|
|
1036
|
+
def has_horizontal_roi(self): # -> bool
|
|
1037
|
+
return self.get_horizontal_roi() is not None
|
|
1038
|
+
|
|
1039
|
+
def has_laser_power_calibration(self): # -> bool
|
|
1040
|
+
if self.max_laser_power_mW <= 0:
|
|
1041
|
+
return False
|
|
1042
|
+
return utils.coeffs_look_valid(self.laser_power_coeffs, count=4)
|
|
1043
|
+
|
|
1044
|
+
def has_detector_tec_calibration(self): # -> bool
|
|
1045
|
+
""" simplified version of WasatchNET.Util.validTECCal """
|
|
1046
|
+
|
|
1047
|
+
if not utils.coeffs_look_valid(self.degC_to_dac_coeffs, count=3):
|
|
1048
|
+
return False
|
|
1049
|
+
|
|
1050
|
+
# check it's not the "default"
|
|
1051
|
+
if self.degC_to_dac_coeffs[0] == 2700 and \
|
|
1052
|
+
self.degC_to_dac_coeffs[1] == 0 and \
|
|
1053
|
+
self.degC_to_dac_coeffs[2] == 0:
|
|
1054
|
+
return False
|
|
1055
|
+
|
|
1056
|
+
return True
|
|
1057
|
+
|
|
1058
|
+
def has_raman_intensity_calibration(self):
|
|
1059
|
+
if self.format < 6:
|
|
1060
|
+
log.debug(f"has_raman_intensity_calibration: false because format {self.format}")
|
|
1061
|
+
return False
|
|
1062
|
+
|
|
1063
|
+
# could check subformat too
|
|
1064
|
+
|
|
1065
|
+
coeffs = self.multi_wavelength_calibration.get("raman_intensity_coeffs")
|
|
1066
|
+
if not utils.coeffs_look_valid(coeffs):
|
|
1067
|
+
log.debug(f"has_raman_intensity_calibration: false because coeffs look weird")
|
|
1068
|
+
return False
|
|
1069
|
+
|
|
1070
|
+
return True
|
|
1071
|
+
|
|
1072
|
+
def has_sml(self):
|
|
1073
|
+
""" @todo include light_source_type """
|
|
1074
|
+
return self.has_laser and self.max_laser_power_mW < 150
|
|
1075
|
+
|
|
1076
|
+
def has_mml(self):
|
|
1077
|
+
""" @todo include light_source_type """
|
|
1078
|
+
return self.has_laser and self.max_laser_power_mW >= 150
|
|
1079
|
+
|
|
1080
|
+
## convert the given laser output power from milliwatts to percentage
|
|
1081
|
+
# using the configured calibration
|
|
1082
|
+
def laser_power_mW_to_percent(self, mW):
|
|
1083
|
+
if not self.has_laser_power_calibration():
|
|
1084
|
+
return 0
|
|
1085
|
+
|
|
1086
|
+
perc = self.laser_power_coeffs[0] \
|
|
1087
|
+
+ self.laser_power_coeffs[1] * mW \
|
|
1088
|
+
+ self.laser_power_coeffs[2] * mW * mW \
|
|
1089
|
+
+ self.laser_power_coeffs[3] * mW * mW * mW
|
|
1090
|
+
|
|
1091
|
+
return perc
|
|
1092
|
+
|
|
1093
|
+
def is_valid_serial_number(self):
|
|
1094
|
+
return re.match(r"^[-+a-z0-9_ ]{1,16}$", self.serial_number, re.IGNORECASE)
|
|
1095
|
+
|
|
1096
|
+
# ##########################################################################
|
|
1097
|
+
# #
|
|
1098
|
+
# Subformats #
|
|
1099
|
+
# #
|
|
1100
|
+
# ##########################################################################
|
|
1101
|
+
|
|
1102
|
+
# ##########################################################################
|
|
1103
|
+
# Subformat 1: Raman Intensity Calibration
|
|
1104
|
+
# ##########################################################################
|
|
1105
|
+
|
|
1106
|
+
def init_raman_intensity_calibration(self):
|
|
1107
|
+
self.raman_intensity_calibration_order = 5
|
|
1108
|
+
self.raman_intensity_coeffs = []
|
|
1109
|
+
|
|
1110
|
+
def read_raman_intensity_calibration(self):
|
|
1111
|
+
self.raman_intensity_coeffs = []
|
|
1112
|
+
order = self.RAMAN_INTENSITY_CALIBRATION_ORDER
|
|
1113
|
+
for i in range(order + 1):
|
|
1114
|
+
offset = i * 4 + 1
|
|
1115
|
+
self.raman_intensity_coeffs.append(self.unpack((6, offset, 4), "f", "raman_intensity_coeff_%d" % i))
|
|
1116
|
+
|
|
1117
|
+
def write_raman_intensity_calibration(self):
|
|
1118
|
+
self.pack((6, 0, 1), "B", self.raman_intensity_calibration_order)
|
|
1119
|
+
order = self.RAMAN_INTENSITY_CALIBRATION_ORDER
|
|
1120
|
+
for i in range(order + 1):
|
|
1121
|
+
offset = i * 4 + 1
|
|
1122
|
+
coeff = 0.0
|
|
1123
|
+
if self.raman_intensity_coeffs is not None and i < len(self.raman_intensity_coeffs):
|
|
1124
|
+
coeff = self.raman_intensity_coeffs[i]
|
|
1125
|
+
self.pack((6, offset, 4), "f", coeff)
|
|
1126
|
+
|
|
1127
|
+
def dump_raman_intensity_calibration(self):
|
|
1128
|
+
log.debug("Raman Intensity Calibration:")
|
|
1129
|
+
log.debug(" Raman Int Coeffs: %s", self.raman_intensity_coeffs)
|
|
1130
|
+
|
|
1131
|
+
# ##########################################################################
|
|
1132
|
+
# Subformat 2: Spline
|
|
1133
|
+
# ##########################################################################
|
|
1134
|
+
|
|
1135
|
+
def init_spline(self):
|
|
1136
|
+
self.spline_points = 0
|
|
1137
|
+
self.spline_min = 0
|
|
1138
|
+
self.spline_max = 0
|
|
1139
|
+
self.spline_wavelengths = []
|
|
1140
|
+
self.spline_y = []
|
|
1141
|
+
self.spline_y2 = []
|
|
1142
|
+
|
|
1143
|
+
##
|
|
1144
|
+
# @todo turn into EEPROMSpline
|
|
1145
|
+
def read_spline(self):
|
|
1146
|
+
self.spline_wavelengths = []
|
|
1147
|
+
self.spline_y = []
|
|
1148
|
+
self.splint_y2 = []
|
|
1149
|
+
|
|
1150
|
+
self.spline_points = self.unpack((6, 0, 1), "B", "spline.points")
|
|
1151
|
+
if self.spline_points <= 0:
|
|
1152
|
+
log.debug("empty spline")
|
|
1153
|
+
return
|
|
1154
|
+
|
|
1155
|
+
if self.spline_points > 14:
|
|
1156
|
+
log.error("invalid spline (%d points)", self.spline_points)
|
|
1157
|
+
return
|
|
1158
|
+
|
|
1159
|
+
for i in len(range(self.spline_points)):
|
|
1160
|
+
if i < 5:
|
|
1161
|
+
page = 6
|
|
1162
|
+
base = 4
|
|
1163
|
+
first = 0
|
|
1164
|
+
elif i < 10:
|
|
1165
|
+
page = 7
|
|
1166
|
+
base = 0
|
|
1167
|
+
first = 5
|
|
1168
|
+
else:
|
|
1169
|
+
page = 4
|
|
1170
|
+
base = 0
|
|
1171
|
+
first = 10
|
|
1172
|
+
|
|
1173
|
+
offset = base + (i - first) * 12
|
|
1174
|
+
wavelength = self.unpack((page, offset, 4), "f", "spline.wavelength[%d]" % i)
|
|
1175
|
+
y = self.unpack((page, offset + 4, 4), "f", "spline.y[%d]" % i)
|
|
1176
|
+
y2 = self.unpack((page, offset + 8, 4), "f", "spline.y2[%d]" % i)
|
|
1177
|
+
|
|
1178
|
+
self.spline_wavelengths.append(wavelength)
|
|
1179
|
+
self.spline_y.append(y)
|
|
1180
|
+
self.spline_y2.append(y2)
|
|
1181
|
+
|
|
1182
|
+
self.spline_min = self.unpack((4, 56, 4), "f", "spline_wavelength_min")
|
|
1183
|
+
self.spline_max = self.unpack((4, 60, 4), "f", "spline_wavelength_max")
|
|
1184
|
+
if self.spline_min >= self.spline_max:
|
|
1185
|
+
log.error("invalid spline (min %f, max %f)", self.spline_min, self.spline_max)
|
|
1186
|
+
return
|
|
1187
|
+
|
|
1188
|
+
def write_spline(self):
|
|
1189
|
+
log.error("EEPROM.write_spline not implemented")
|
|
1190
|
+
|
|
1191
|
+
def dump_spline(self):
|
|
1192
|
+
return # not implemented
|
|
1193
|
+
|
|
1194
|
+
# ##########################################################################
|
|
1195
|
+
# Subformat 3: Untethered
|
|
1196
|
+
# ##########################################################################
|
|
1197
|
+
|
|
1198
|
+
def init_untethered(self):
|
|
1199
|
+
self.untethered_library_type = 0
|
|
1200
|
+
self.untethered_library_id = 0
|
|
1201
|
+
self.untethered_scans_to_average = 0
|
|
1202
|
+
self.untethered_min_ramp_pixels = 0
|
|
1203
|
+
self.untethered_min_peak_height = 0
|
|
1204
|
+
self.untethered_match_threshold = 0
|
|
1205
|
+
self.untethered_library_count = 0
|
|
1206
|
+
|
|
1207
|
+
def read_untethered(self):
|
|
1208
|
+
self.untethered_library_type = self.unpack((7, 0, 1), "B", "library_type")
|
|
1209
|
+
self.untethered_library_id = self.unpack((7, 1, 2), "H", "library_id")
|
|
1210
|
+
self.untethered_scans_to_average = self.unpack((7, 3, 1), "B", "scans_to_average")
|
|
1211
|
+
self.untethered_min_ramp_pixels = self.unpack((7, 4, 1), "B", "min_ramp_pixels")
|
|
1212
|
+
self.untethered_min_peak_height = self.unpack((7, 5, 2), "H", "min_peak_height")
|
|
1213
|
+
self.untethered_match_threshold = self.unpack((7, 7, 1), "B", "match_threshold")
|
|
1214
|
+
self.untethered_library_count = self.unpack((7, 8, 1), "B", "library_count")
|
|
1215
|
+
|
|
1216
|
+
def write_untethered(self):
|
|
1217
|
+
self.pack((7, 0, 1), "B", self.untethered_library_type)
|
|
1218
|
+
self.pack((7, 1, 2), "H", self.untethered_library_id)
|
|
1219
|
+
self.pack((7, 3, 1), "B", self.untethered_scans_to_average)
|
|
1220
|
+
self.pack((7, 4, 1), "B", self.untethered_min_ramp_pixels)
|
|
1221
|
+
self.pack((7, 5, 2), "H", self.untethered_min_peak_height)
|
|
1222
|
+
self.pack((7, 7, 1), "B", self.untethered_match_threshold)
|
|
1223
|
+
self.pack((7, 8, 1), "B", self.untethered_library_count)
|
|
1224
|
+
|
|
1225
|
+
def dump_untethered(self):
|
|
1226
|
+
log.debug("Untethered:")
|
|
1227
|
+
log.debug(" Library Type: %d", self.untethered_library_type)
|
|
1228
|
+
log.debug(" Library ID: %d", self.untethered_library_id)
|
|
1229
|
+
log.debug(" Scans to Average: %d", self.untethered_scans_to_average)
|
|
1230
|
+
log.debug(" Min Ramp Pixels: %d", self.untethered_min_ramp_pixels)
|
|
1231
|
+
log.debug(" Min Peak Height: %d", self.untethered_min_peak_height)
|
|
1232
|
+
log.debug(" Match Threshold: %d", self.untethered_match_threshold)
|
|
1233
|
+
log.debug(" Library Count: %d", self.untethered_library_count)
|
|
1234
|
+
|
|
1235
|
+
class MultiWavelengthCalibration:
|
|
1236
|
+
"""
|
|
1237
|
+
Subformat 5: Multi-Wavelength
|
|
1238
|
+
|
|
1239
|
+
This class is used to manage those attributes which can vary in a single
|
|
1240
|
+
multi-wavelength spectrometer (for instance, where a compound grating
|
|
1241
|
+
supports different Raman excitations in different detector ROIs).
|
|
1242
|
+
|
|
1243
|
+
Currently the set of such attributes includes laser excitation wavelength,
|
|
1244
|
+
wavelength calibration, horizontal ROI, Raman Intensity Calibration,
|
|
1245
|
+
average resolution, and horizontal binning mode. (Vertical ROI was
|
|
1246
|
+
presciently covered in Format 1.)
|
|
1247
|
+
|
|
1248
|
+
@par Theory of Operation
|
|
1249
|
+
|
|
1250
|
+
A "calibration" is one set of attributes (self.attributes) related to
|
|
1251
|
+
a particular grating region.
|
|
1252
|
+
|
|
1253
|
+
Most of this class architecture could conceivably support many different
|
|
1254
|
+
calibrations. In practice, it is limited to 3, because it is currently
|
|
1255
|
+
using the 3-region Vertical ROI fields included in the original (Rev 1)
|
|
1256
|
+
EEPROM format.
|
|
1257
|
+
|
|
1258
|
+
It is also practically limited to 2 calibrations because currently it's
|
|
1259
|
+
using one EEPROM page per "extra" calibration, and the "standard 8" pages
|
|
1260
|
+
only has one extra: page 7. This isn't a limitation on XS, which has a much
|
|
1261
|
+
larger EEPROM than X series, so we could easily add more pages to subformat
|
|
1262
|
+
5 if desired.
|
|
1263
|
+
|
|
1264
|
+
Calibration 0 is the "original calibration," and is stored in the "standard"
|
|
1265
|
+
EEPROM pages where such things (wavecal, Raman Intensity, etc) have always
|
|
1266
|
+
been stored. When loaded to memory, they are initially read into the EEPROM
|
|
1267
|
+
instance properties (.wavecal_coeffs, .excitation_float_nm etc) where they
|
|
1268
|
+
have always been stored. HOWEVER, this is not where "modern" code should
|
|
1269
|
+
attempt to read or write them, as described below.
|
|
1270
|
+
|
|
1271
|
+
Additional calibrations (1+) are persisted on a single EEPROM page each (p7
|
|
1272
|
+
for calibration 1). When loaded from EEPROM, they are NOT stored in object
|
|
1273
|
+
properties like Calibration 0, but are stored in a dict by calibration index
|
|
1274
|
+
and their field name. For instance, the laser excitation wavelength of the
|
|
1275
|
+
second calibration (calibration 1) is in values["excitation_nm_float"][1].
|
|
1276
|
+
|
|
1277
|
+
For consistency, working copies of calibration 0 fields are populated to
|
|
1278
|
+
values[name][0]. New code is recommended to access even the old "calibration
|
|
1279
|
+
0" fields through the provided MultiWavelengthCalibration accessors (get()
|
|
1280
|
+
and set()), as this will make the new code "future-proof" and multi-
|
|
1281
|
+
calibration ready. All known Wasatch.PY and ENLIGHTEN references to 0-
|
|
1282
|
+
calibration properties have been updated to use the new accessors as
|
|
1283
|
+
reference examples.
|
|
1284
|
+
"""
|
|
1285
|
+
|
|
1286
|
+
def __init__(self, eeprom):
|
|
1287
|
+
self.eeprom = eeprom
|
|
1288
|
+
|
|
1289
|
+
self.attributes = [ 'excitation_nm_float', 'wavelength_coeffs',
|
|
1290
|
+
'roi_horizontal_start', 'roi_horizontal_end',
|
|
1291
|
+
'avg_resolution', 'raman_intensity_coeffs',
|
|
1292
|
+
'horiz_binning_mode' ]
|
|
1293
|
+
self.values = {}
|
|
1294
|
+
self.calibrations = 1
|
|
1295
|
+
self.selected_calibration = 0 # e.g., self.values['avg_resolution'][selected_calibration]
|
|
1296
|
+
|
|
1297
|
+
# this won't do anything except copy "defaults" into self.values (which
|
|
1298
|
+
# will get overwritten when EEPROM.read re-calls self.initialize after
|
|
1299
|
+
# "better" values are available to assign to self.values), but it helps
|
|
1300
|
+
# non-FID classes like AndorDevice to start with an initialized baseline
|
|
1301
|
+
self.initialize()
|
|
1302
|
+
|
|
1303
|
+
def is_multi_wavelength(self, name):
|
|
1304
|
+
return name in self.attributes
|
|
1305
|
+
|
|
1306
|
+
def initialize(self, name=None):
|
|
1307
|
+
""" initialize attributes to single-element arrays with the "standard" value at calibration 0 """
|
|
1308
|
+
if name:
|
|
1309
|
+
self.values[name] = [ getattr(self.eeprom, name) ]
|
|
1310
|
+
else:
|
|
1311
|
+
for name in self.attributes:
|
|
1312
|
+
self.values[name] = [ getattr(self.eeprom, name) ]
|
|
1313
|
+
|
|
1314
|
+
def get(self, name, calibration=None, index=None, default=None):
|
|
1315
|
+
# allow getter to be used for any EEPROM attribute
|
|
1316
|
+
if name not in self.attributes:
|
|
1317
|
+
return getattr(self.eeprom, name)
|
|
1318
|
+
|
|
1319
|
+
if name not in self.values:
|
|
1320
|
+
log.debug(f"get: name {name} in attributes, but not in values: {self.values}")
|
|
1321
|
+
return default
|
|
1322
|
+
|
|
1323
|
+
label = f"{name}[{calibration}]" if index is None else f"{name}[{calibration}][{index}]"
|
|
1324
|
+
|
|
1325
|
+
a = self.values[name]
|
|
1326
|
+
if calibration is None:
|
|
1327
|
+
calibration = self.selected_calibration
|
|
1328
|
+
if calibration >= len(a):
|
|
1329
|
+
# log.warn(f"MultiWavelengthCalibration.get: returning default {default} because calibration {calibration} not in {label} array len {len(a)}")
|
|
1330
|
+
return default
|
|
1331
|
+
|
|
1332
|
+
if index is None:
|
|
1333
|
+
value = a[calibration]
|
|
1334
|
+
else:
|
|
1335
|
+
if isinstance(a[calibration], list):
|
|
1336
|
+
if index >= len(a[calibration]):
|
|
1337
|
+
log.warn(f"MultiWavelengthCalibration.get: returning {label} 0 because index {index} not in array[calibration {calibration}] len {len(a[calibration])}")
|
|
1338
|
+
return 0
|
|
1339
|
+
else:
|
|
1340
|
+
value = a[calibration][index]
|
|
1341
|
+
else:
|
|
1342
|
+
log.warn(f"MultiWavelengthCalibration.get: returning {label} 0 because array[calibration {calibration}] is not a list")
|
|
1343
|
+
return 0
|
|
1344
|
+
|
|
1345
|
+
# log.debug(f"MultiWavelengthCalibration.get: returning {label} = {value}")
|
|
1346
|
+
return value
|
|
1347
|
+
|
|
1348
|
+
def set(self, name, value, calibration=None, index=None):
|
|
1349
|
+
# apply some quick validation based on datatype
|
|
1350
|
+
eeprom_field = self.eeprom.fields.get(name, None)
|
|
1351
|
+
if eeprom_field is None:
|
|
1352
|
+
log.debug(f"MWC.set: {name} not in eeprom.fields?")
|
|
1353
|
+
else:
|
|
1354
|
+
# enforce integers
|
|
1355
|
+
if eeprom_field.data_type.lower() in ["i", "h", "b"]:
|
|
1356
|
+
log.debug(f"MWC.set: rounding {name} to int")
|
|
1357
|
+
value = int(value)
|
|
1358
|
+
|
|
1359
|
+
# enforce unsigned
|
|
1360
|
+
if eeprom_field.data_type in ["I", "H", "B"]:
|
|
1361
|
+
log.debug(f"MWC.set: clamping {name} to zero")
|
|
1362
|
+
value = int(max(0, value))
|
|
1363
|
+
|
|
1364
|
+
if name not in self.attributes:
|
|
1365
|
+
setattr(self.eeprom, name, value)
|
|
1366
|
+
return
|
|
1367
|
+
|
|
1368
|
+
try:
|
|
1369
|
+
a = self.values[name]
|
|
1370
|
+
if calibration is None:
|
|
1371
|
+
calibration = self.selected_calibration
|
|
1372
|
+
|
|
1373
|
+
while len(a) - 1 < calibration:
|
|
1374
|
+
a.append(0)
|
|
1375
|
+
|
|
1376
|
+
if self.calibrations < calibration + 1:
|
|
1377
|
+
self.calibrations = calibration + 1
|
|
1378
|
+
|
|
1379
|
+
if index is None:
|
|
1380
|
+
label = f"{name}[{calibration}]"
|
|
1381
|
+
a[calibration] = value
|
|
1382
|
+
if calibration == 0:
|
|
1383
|
+
setattr(self.eeprom, name, value)
|
|
1384
|
+
else:
|
|
1385
|
+
label = f"{name}[{calibration}][{index}]"
|
|
1386
|
+
a[calibration][index] = value
|
|
1387
|
+
if calibration == 0:
|
|
1388
|
+
setattr(self.eeprom, name, a[calibration])
|
|
1389
|
+
|
|
1390
|
+
log.debug(f"MultiWavelengthCalibration.set: set {label} = {value}")
|
|
1391
|
+
except:
|
|
1392
|
+
log.error(f"MultiWavelengthCalibration.set: failed to set name {name}, calibration {calibration}, index {index}, value {value}", exc_info=1)
|
|
1393
|
+
|
|
1394
|
+
def read(self, calibration):
|
|
1395
|
+
if calibration < 1:
|
|
1396
|
+
# assume calibration[0] was read with standard eeprom.read
|
|
1397
|
+
return
|
|
1398
|
+
|
|
1399
|
+
if self.calibrations < calibration + 1:
|
|
1400
|
+
self.calibrations = calibration + 1
|
|
1401
|
+
|
|
1402
|
+
page = 6 + calibration
|
|
1403
|
+
log.debug(f"MultiWavelengthCalibration.read: reading calibration {calibration} from page {page}")
|
|
1404
|
+
self.values["excitation_nm_float" ].append( self.eeprom.unpack((page, 0, 4), "f"))
|
|
1405
|
+
self.values["roi_horizontal_start" ].append( self.eeprom.unpack((page, 26, 2), "H"))
|
|
1406
|
+
self.values["roi_horizontal_end" ].append( self.eeprom.unpack((page, 28, 2), "H"))
|
|
1407
|
+
self.values["avg_resolution" ].append( self.eeprom.unpack((page, 30, 4), "f"))
|
|
1408
|
+
self.values["horiz_binning_mode" ].append( self.eeprom.unpack((page, 58, 1), "B"))
|
|
1409
|
+
self.values["wavelength_coeffs" ].append([ self.eeprom.unpack((page, 4 + i * 4, 4), "f") for i in range(5) ])
|
|
1410
|
+
self.values["raman_intensity_coeffs" ].append([ self.eeprom.unpack((page, 34 + i * 4, 4), "f") for i in range(6) ])
|
|
1411
|
+
|
|
1412
|
+
def write(self):
|
|
1413
|
+
log.debug(f"MultiWavelengthCalibration.write: calibrations {self.calibrations}")
|
|
1414
|
+
|
|
1415
|
+
# assume calibration[0] output with standard eeprom.write
|
|
1416
|
+
for calibration in range(1, self.calibrations):
|
|
1417
|
+
page = 6 + calibration
|
|
1418
|
+
log.debug(f"MultiWavelengthCalibration.write: writing calibration {calibration} to page {page}")
|
|
1419
|
+
|
|
1420
|
+
self.eeprom.pack((page, 0, 4), "f", self.get("excitation_nm_float", calibration))
|
|
1421
|
+
self.eeprom.pack((page, 26, 2), "H", self.get("roi_horizontal_start", calibration))
|
|
1422
|
+
self.eeprom.pack((page, 28, 2), "H", self.get("roi_horizontal_end", calibration))
|
|
1423
|
+
self.eeprom.pack((page, 30, 4), "f", self.get("avg_resolution", calibration))
|
|
1424
|
+
self.eeprom.pack((page, 58, 1), "B", self.get("horiz_binning_mode", calibration))
|
|
1425
|
+
for i in range(5): self.eeprom.pack((page, 4 + i * 4, 4), "f", self.get("wavelength_coeffs", calibration, i))
|
|
1426
|
+
for i in range(6): self.eeprom.pack((page, 34 + i * 4, 4), "f", self.get("raman_intensity_coeffs", calibration, i))
|
|
1427
|
+
|
|
1428
|
+
def dump(self):
|
|
1429
|
+
log.debug("Multi-Wavelength:")
|
|
1430
|
+
for i in range(self.calibrations):
|
|
1431
|
+
log.debug(f" Calibration #{i}")
|
|
1432
|
+
for name in self.attributes:
|
|
1433
|
+
log.debug(f" {name} = {self.get(name, calibration=i)}")
|
|
1434
|
+
|
|
1435
|
+
def toJSON(self):
|
|
1436
|
+
return str(self.__dict__)
|
|
1437
|
+
# return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
|
|
1438
|
+
|
|
1439
|
+
class EEPROMField:
|
|
1440
|
+
def __init__(self, pos, data_type, name):
|
|
1441
|
+
self.pos = pos
|
|
1442
|
+
self.data_type = data_type
|
|
1443
|
+
self.name = name
|
|
1444
|
+
|
|
1445
|
+
self.page = pos[0]
|
|
1446
|
+
self.offset = pos[1]
|
|
1447
|
+
self.length = pos[2]
|
|
1448
|
+
|
|
1449
|
+
def toJSON(self):
|
|
1450
|
+
return str(self.__dict__)
|