wasatch 2.3.2__py3-none-any.whl

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