wasatch 2.3.8__tar.gz → 2.3.12__tar.gz
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-2.3.8 → wasatch-2.3.12}/.gitignore +1 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/CHANGELOG.md +11 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/PKG-INFO +1 -1
- {wasatch-2.3.8 → wasatch-2.3.12}/README_PYPI.md +6 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/demo-ids-raman.py +16 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/pyproject.toml +1 -1
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/EEPROM.py +72 -12
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/FeatureIdentificationDevice.py +8 -8
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/IDSCamera.py +85 -27
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/IDSDevice.py +54 -37
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/__init__.py +1 -1
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/utils.py +21 -5
- {wasatch-2.3.8 → wasatch-2.3.12}/.example_pypirc +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/.flake8 +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/.pylintrc +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/Doxyfile +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/LICENSE +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/MANIFEST.in +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/Makefile +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/README.md +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/README_RPI.md +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/README_SETTINGS.md +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/README_VIRTUAL.md +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/WasatchShell/README.md +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/WasatchShell/laser-test.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/WasatchShell/load-test.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/WasatchShell/one-shot.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/WasatchShell/wasatch-shell.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/demo-auto-raman.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/demo-simple.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/demo-virtual.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/demo-workflow.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/demo.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/environments/conda-linux.yml +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/environments/conda-macos.yml +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/environments/conda-rpi.yml +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/environments/conda-win10.yml +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/screenshots/multiplatform.png +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/scripts/dark-vs-integ.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/scripts/deploy +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/testSpectrometers/SiG_785/eeprom/EEPROM-EM-9c65d19f4c.json +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/testSpectrometers/SiG_785/readings/enlighten-20210329-094722-799520-WP-00686.csv +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/testSpectrometers/SiG_785/readings/enlighten-20210329-094755-288958-WP-00686.csv +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/testSpectrometers/WP-00887/eeprom/WP-00887-mock.json +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/testSpectrometers/WP-00904/eeprom/WP-00904-mock.json +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/testSpectrometers/WP-00913/eeprom/WP-00913-mock.json +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/tests/existing_implementations.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/tests/noise-by-setpoint.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/tests/queues.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/tests/unit.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/udev/10-wasatch.rules +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/AbstractUSBDevice.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/AndorDevice.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/AreaScanImage.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/AutoRaman.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/AutoRamanRequest.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/BLEDevice.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/BalanceAcquisition.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/CSVLoader.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/CommandSettings.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/ControlObject.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/DetectorROI.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/DetectorRegions.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/DeviceFinderBLE.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/DeviceFinderTCP.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/DeviceFinderUSB.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/DeviceID.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/EEPROM_multical.sav +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/FPGAOptions.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/FirmwareRequirements.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/HardwareInfo.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/IMX385.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/InterfaceDevice.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/MockUSBDevice.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/OceanDevice.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/PollStatus.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/ProcessedReading.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/ROI.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/Reading.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/RealUSBDevice.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/SPIDevice.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/SpectrometerRequest.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/SpectrometerResponse.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/SpectrometerSettings.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/SpectrometerState.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/StatusMessage.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/TCPDevice.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/TakeOneRequest.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/WasatchBus.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/WasatchDevice.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/WasatchDeviceWrapper.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/WrapperWorker.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/applog.py +0 -0
- {wasatch-2.3.8 → wasatch-2.3.12}/wasatch/simulation_protocol.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
- 2026-02-17 2.3.12
|
|
4
|
+
- added laser_password, startup_scans_to_average, laser_attenuator
|
|
5
|
+
- added [un]pack_field
|
|
6
|
+
- un-rotated Hamamatsu area scan 180 deg :-(
|
|
7
|
+
- allow IDS config files to be located by serial number prefix
|
|
8
|
+
- 2026-02-09 2.3.11
|
|
9
|
+
- rotate Hamamatsu area scan 180 deg
|
|
10
|
+
- 2026-01-16 2.3.10
|
|
11
|
+
- support 2min integrations on STARVIS
|
|
12
|
+
- 2026-01-15 2.3.9
|
|
13
|
+
- added --normalize to demo-ids-raman.py
|
|
3
14
|
- 2026-01-12 2.3.8
|
|
4
15
|
- added settings.has_detector
|
|
5
16
|
- improved support for Andor .json "virtual EEPROMs"
|
|
@@ -50,6 +50,12 @@ before uploading to the "public, global, default" pypi repository.
|
|
|
50
50
|
When you are ready to publish run `flit publish` or `flit publish --repository pypi`
|
|
51
51
|
(or just "make publish").
|
|
52
52
|
|
|
53
|
+
## Common Errors
|
|
54
|
+
|
|
55
|
+
### requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://test.pypi.org/legacy/
|
|
56
|
+
|
|
57
|
+
Ensure wasatch/__init__.py version matches pyproject.toml.
|
|
58
|
+
|
|
53
59
|
# Installing 'wasatch' package from source
|
|
54
60
|
|
|
55
61
|
If you've cloned the Wasatch.PY repo and want to just install the `wasatch`
|
|
@@ -26,6 +26,7 @@ import argparse
|
|
|
26
26
|
|
|
27
27
|
from datetime import datetime
|
|
28
28
|
from time import sleep
|
|
29
|
+
from PIL import Image, ImageStat # for image normalization
|
|
29
30
|
|
|
30
31
|
import wasatch
|
|
31
32
|
from wasatch import applog
|
|
@@ -59,6 +60,7 @@ class WasatchDemo:
|
|
|
59
60
|
parser.add_argument("--save-spectra", action="store_true", help="save vertically-binned spectra to row-ordered CSV")
|
|
60
61
|
parser.add_argument("--save-png", action="store_true", help="save PNG of each image frame")
|
|
61
62
|
parser.add_argument("--save-data", action="store_true", help="save 2D array of each image frame")
|
|
63
|
+
parser.add_argument("--normalize", action="store_true", help="normalize PNG image")
|
|
62
64
|
parser.add_argument("--save-dir", type=str, default=".", help="directory to save files")
|
|
63
65
|
parser.add_argument("--prefix", type=str, default="ids-raman", help="filename prefix")
|
|
64
66
|
|
|
@@ -150,6 +152,9 @@ class WasatchDemo:
|
|
|
150
152
|
############################################################################
|
|
151
153
|
|
|
152
154
|
def run(self):
|
|
155
|
+
if self.args.save_dir != ".":
|
|
156
|
+
os.makedirs(self.args.save_dir, exist_ok=True)
|
|
157
|
+
|
|
153
158
|
self.camera_device.set_integration_time_ms(self.args.integration_time_ms)
|
|
154
159
|
|
|
155
160
|
# initialize outfile if saving spectra
|
|
@@ -229,6 +234,17 @@ class WasatchDemo:
|
|
|
229
234
|
os.replace(asi.pathname_png, filename)
|
|
230
235
|
print(f"\tsaved {filename}")
|
|
231
236
|
|
|
237
|
+
# normalize PNG (just the image file, not the raw 2D data)
|
|
238
|
+
if self.args.normalize:
|
|
239
|
+
img = Image.open(filename).convert('L')
|
|
240
|
+
stat = ImageStat.Stat(img)
|
|
241
|
+
mean_brightness = stat.mean[0]
|
|
242
|
+
img_array = np.array(img)
|
|
243
|
+
normalized_img_array = img_array / mean_brightness * 100
|
|
244
|
+
normalized_img = Image.fromarray(np.uint8(normalized_img_array))
|
|
245
|
+
normalized_img.save(filename)
|
|
246
|
+
print(f"\tnormalized {filename}")
|
|
247
|
+
|
|
232
248
|
# save area scan data
|
|
233
249
|
if self.args.save_data and asi.data is not None:
|
|
234
250
|
filename = os.path.join(self.args.save_dir, f"{self.args.prefix}-{self.reading_count:04d}.csv")
|
|
@@ -25,9 +25,9 @@ log = logging.getLogger(__name__)
|
|
|
25
25
|
# @see 24LC128 for FX2 (16KB, https://www.microchip.com/en-us/product/24LC128)
|
|
26
26
|
class EEPROM:
|
|
27
27
|
|
|
28
|
-
LATEST_REV =
|
|
28
|
+
LATEST_REV = 18
|
|
29
29
|
|
|
30
|
-
MAX_PAGES =
|
|
30
|
+
MAX_PAGES = 9
|
|
31
31
|
PAGE_LENGTH = 64
|
|
32
32
|
SUBPAGE_COUNT = 4 # used for BLE
|
|
33
33
|
RAMAN_INTENSITY_CALIBRATION_ORDER = 5
|
|
@@ -68,6 +68,7 @@ class EEPROM:
|
|
|
68
68
|
((1, 46, 2), "h", "tec_beta"),
|
|
69
69
|
((1, 48, 12), "s", "calibration_date"),
|
|
70
70
|
((1, 60, 3), "s", "calibrated_by"),
|
|
71
|
+
|
|
71
72
|
((2, 0, 16), "s", "detector"),
|
|
72
73
|
((2, 16, 2), "H", "active_pixels_horizontal"),
|
|
73
74
|
((2, 18, 1), "B", "laser_warmup_sec"),
|
|
@@ -87,6 +88,7 @@ class EEPROM:
|
|
|
87
88
|
((2, 51, 4), "f", "linearity_c2"),
|
|
88
89
|
((2, 55, 4), "f", "linearity_c3"),
|
|
89
90
|
((2, 59, 4), "f", "linearity_c4"),
|
|
91
|
+
|
|
90
92
|
((3, 12, 4), "f", "laser_power_c0"),
|
|
91
93
|
((3, 16, 4), "f", "laser_power_c1"),
|
|
92
94
|
((3, 20, 4), "f", "laser_power_c2"),
|
|
@@ -101,9 +103,15 @@ class EEPROM:
|
|
|
101
103
|
((3, 55, 2), "H", "power_timeout_sec"),
|
|
102
104
|
((3, 57, 2), "H", "detector_timeout_sec"),
|
|
103
105
|
((3, 59, 1), "B", "horiz_binning_mode"),
|
|
106
|
+
((3, 60, 1), "B", "startup_scans_to_average"),
|
|
107
|
+
((3, 61, 1), "B", "laser_attenuator"),
|
|
108
|
+
|
|
104
109
|
((4, 0, 64), "s", "user_data"),
|
|
110
|
+
|
|
105
111
|
((5, 30, 16), "s", "product_configuration"),
|
|
106
|
-
((5, 63, 1), "B", "subformat")
|
|
112
|
+
((5, 63, 1), "B", "subformat"),
|
|
113
|
+
|
|
114
|
+
((8, 0, 15), "s", "laser_password"),
|
|
107
115
|
]
|
|
108
116
|
|
|
109
117
|
def __init__(self):
|
|
@@ -145,6 +153,8 @@ class EEPROM:
|
|
|
145
153
|
self.power_timeout_sec = 0
|
|
146
154
|
self.detector_timeout_sec = 0
|
|
147
155
|
self.horiz_binning_mode = 0
|
|
156
|
+
self.startup_scans_to_average = 1
|
|
157
|
+
self.laser_attenuator = 127
|
|
148
158
|
|
|
149
159
|
self.wavelength_coeffs = []
|
|
150
160
|
self.degC_to_dac_coeffs = []
|
|
@@ -185,6 +195,8 @@ class EEPROM:
|
|
|
185
195
|
self.bad_pixels = [] # should be set, not list (but this works with EEPROMEditor)
|
|
186
196
|
self.product_configuration = None
|
|
187
197
|
|
|
198
|
+
self.laser_password = None
|
|
199
|
+
|
|
188
200
|
self.format = EEPROM.LATEST_REV
|
|
189
201
|
self.subformat = 0 # determines format of pages 6-7
|
|
190
202
|
|
|
@@ -227,6 +239,7 @@ class EEPROM:
|
|
|
227
239
|
"roi_vertical_region_3_start",
|
|
228
240
|
"user_text",
|
|
229
241
|
"wavelength_coeffs",
|
|
242
|
+
"laser_password",
|
|
230
243
|
]
|
|
231
244
|
|
|
232
245
|
self.init_raman_intensity_calibration()
|
|
@@ -276,7 +289,7 @@ class EEPROM:
|
|
|
276
289
|
# ##########################################################################
|
|
277
290
|
|
|
278
291
|
##
|
|
279
|
-
# given a set of the 8 buffers read from a spectrometer via USB,
|
|
292
|
+
# given a set of the 8+ buffers read from a spectrometer via USB,
|
|
280
293
|
# parse those into the approrpriate fields and datatypes
|
|
281
294
|
def parse(self, buffers):
|
|
282
295
|
if len(buffers) < EEPROM.MAX_PAGES:
|
|
@@ -448,6 +461,10 @@ class EEPROM:
|
|
|
448
461
|
self.detector_timeout_sec = self.unpack((3, 57, 2), "H", "detector_timeout_sec")
|
|
449
462
|
self.horiz_binning_mode = self.unpack((3, 59, 1), "B", "horiz_binning_mode")
|
|
450
463
|
|
|
464
|
+
if self.format >= 18:
|
|
465
|
+
self.unpack_field("startup_scans_to_average")
|
|
466
|
+
self.unpack_field("laser_attenuator")
|
|
467
|
+
|
|
451
468
|
# ######################################################################
|
|
452
469
|
# Page 4
|
|
453
470
|
# ######################################################################
|
|
@@ -525,6 +542,17 @@ class EEPROM:
|
|
|
525
542
|
self.laser_timeout_after_count = False
|
|
526
543
|
self.is_oem = False
|
|
527
544
|
|
|
545
|
+
# ######################################################################
|
|
546
|
+
# Page 8
|
|
547
|
+
# ######################################################################
|
|
548
|
+
|
|
549
|
+
if "XS" in self.model.upper(): # and self.format >= 18:
|
|
550
|
+
|
|
551
|
+
s = self.unpack_field("laser_password", quiet=True)
|
|
552
|
+
if len(s) < 4 or any([c in [0x00, 0xff] for c in s]):
|
|
553
|
+
log.debug("no laser password found")
|
|
554
|
+
self.laser_password = None
|
|
555
|
+
|
|
528
556
|
# ######################################################################
|
|
529
557
|
# sanity checks
|
|
530
558
|
# ######################################################################
|
|
@@ -695,6 +723,8 @@ class EEPROM:
|
|
|
695
723
|
self.pack((3, 55, 2), "H", self.power_timeout_sec)
|
|
696
724
|
self.pack((3, 57, 2), "H", self.detector_timeout_sec)
|
|
697
725
|
self.pack((3, 59, 1), "B", self.multi_wavelength_calibration.get("horiz_binning_mode"))
|
|
726
|
+
self.pack_field("startup_scans_to_average")
|
|
727
|
+
self.pack_field("laser_attenuator")
|
|
698
728
|
|
|
699
729
|
# ######################################################################
|
|
700
730
|
# Page 4
|
|
@@ -742,6 +772,14 @@ class EEPROM:
|
|
|
742
772
|
else:
|
|
743
773
|
log.error(f"Unwriteable EEPROM subformat {self.subformat}")
|
|
744
774
|
|
|
775
|
+
# ######################################################################
|
|
776
|
+
# Page 8
|
|
777
|
+
# ######################################################################
|
|
778
|
+
|
|
779
|
+
if "XS" in self.model.upper():
|
|
780
|
+
# self.pack((8, 0, 16), "s", self.laser_password)
|
|
781
|
+
self.pack_field("laser_password", quiet=True)
|
|
782
|
+
|
|
745
783
|
# ##########################################################################
|
|
746
784
|
# #
|
|
747
785
|
# Utility methods #
|
|
@@ -789,19 +827,29 @@ class EEPROM:
|
|
|
789
827
|
log.debug("float_to_uint16: %f -> 0x%04x", gain, raw)
|
|
790
828
|
return raw
|
|
791
829
|
|
|
830
|
+
def unpack_field(self, name, quiet=False):
|
|
831
|
+
if name not in self.fields:
|
|
832
|
+
log.error(f"unable to unpack unknown field {name}")
|
|
833
|
+
return
|
|
834
|
+
|
|
835
|
+
field = self.fields[name]
|
|
836
|
+
value = self.unpack(field.pos, field.data_type, label=name, quiet=quiet)
|
|
837
|
+
setattr(self, name, value)
|
|
838
|
+
return value
|
|
839
|
+
|
|
792
840
|
##
|
|
793
841
|
# Unpack a single field at a given buffer offset of the given datatype.
|
|
794
842
|
#
|
|
795
843
|
# @param address a tuple of the form (buf, offset, len)
|
|
796
844
|
# @param data_type see https://docs.python.org/2/library/struct.html#format-characters
|
|
797
845
|
# @param label if provided, is included in debug log output
|
|
798
|
-
def unpack(self, address, data_type, label=None):
|
|
846
|
+
def unpack(self, address, data_type, label=None, quiet=False):
|
|
799
847
|
page = address[0]
|
|
800
848
|
start_byte = address[1]
|
|
801
849
|
length = address[2]
|
|
802
850
|
end_byte = start_byte + length
|
|
803
851
|
|
|
804
|
-
if page > len(self.buffers):
|
|
852
|
+
if page + 1 > len(self.buffers):
|
|
805
853
|
log.error("error unpacking EEPROM page %d, offset %d, len %d as %s: invalid page (label %s)",
|
|
806
854
|
page, start_byte, length, data_type, label, exc_info=1)
|
|
807
855
|
return
|
|
@@ -827,19 +875,29 @@ class EEPROM:
|
|
|
827
875
|
except:
|
|
828
876
|
log.error("error unpacking EEPROM page %d, offset %d, len %d as %s", page, start_byte, length, data_type, exc_info=1)
|
|
829
877
|
|
|
830
|
-
if
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
878
|
+
if not quiet:
|
|
879
|
+
if label is None:
|
|
880
|
+
log.debug("Unpacked [%s]: %s", data_type, unpack_result)
|
|
881
|
+
else:
|
|
882
|
+
log.debug("Unpacked [%s]: %s (%s)", data_type, unpack_result, label)
|
|
834
883
|
return unpack_result
|
|
835
884
|
|
|
885
|
+
def pack_field(self, name, quiet=False):
|
|
886
|
+
if name not in self.fields:
|
|
887
|
+
log.error(f"unable to pack unknown field {name}")
|
|
888
|
+
return
|
|
889
|
+
|
|
890
|
+
field = self.fields[name]
|
|
891
|
+
value = getattr(self, name)
|
|
892
|
+
self.pack(field.pos, field.data_type, value, label=name, quiet=quiet)
|
|
893
|
+
|
|
836
894
|
##
|
|
837
895
|
# Marshall or serialize a single field at a given buffer offset of the given datatype.
|
|
838
896
|
#
|
|
839
897
|
# @param address a tuple of the form (buf, offset, len)
|
|
840
898
|
# @param data_type see https://docs.python.org/2/library/struct.html#format-characters
|
|
841
899
|
# @param value value to serialize
|
|
842
|
-
def pack(self, address, data_type, value, label=None):
|
|
900
|
+
def pack(self, address, data_type, value, label=None, quiet=False):
|
|
843
901
|
page = address[0]
|
|
844
902
|
start_byte = address[1]
|
|
845
903
|
length = address[2]
|
|
@@ -873,7 +931,7 @@ class EEPROM:
|
|
|
873
931
|
value = float(value)
|
|
874
932
|
struct.pack_into(data_type, buf, start_byte, value)
|
|
875
933
|
|
|
876
|
-
if False:
|
|
934
|
+
if False and not quiet:
|
|
877
935
|
extra = "" if label is None else (" (%s)" % label)
|
|
878
936
|
log.debug("Packed (%d, %2d, %2d) '%s' value %s -> %s%s",
|
|
879
937
|
page, start_byte, length, data_type, value, buf[start_byte:end_byte], extra)
|
|
@@ -960,6 +1018,8 @@ class EEPROM:
|
|
|
960
1018
|
log.debug(" Power Timeout: %d", self.power_timeout_sec)
|
|
961
1019
|
log.debug(" Detector Timeout: %d", self.detector_timeout_sec)
|
|
962
1020
|
log.debug(" Horiz Bin Mode: %d", self.multi_wavelength_calibration.get("horiz_binning_mode", default=0))
|
|
1021
|
+
log.debug(" Startup Scan Avg: %d", self.startup_scans_to_average)
|
|
1022
|
+
log.debug(" Laser Attenuator: %d", self.laser_attenuator)
|
|
963
1023
|
log.debug(" Slit size: %s um", self.slit_size_um)
|
|
964
1024
|
log.debug(" Start Integ Time: %d ms", self.startup_integration_time_ms)
|
|
965
1025
|
log.debug(" Start Temp: %.2f degC", self.startup_temp_degC)
|
|
@@ -942,14 +942,14 @@ class FeatureIdentificationDevice(InterfaceDevice):
|
|
|
942
942
|
def get_battery_state_raw(self):
|
|
943
943
|
"""Retrieves the raw battery reading and then caches it for 1 sec"""
|
|
944
944
|
now = datetime.datetime.now()
|
|
945
|
-
if self.settings.state.battery_timestamp is
|
|
946
|
-
|
|
945
|
+
if self.settings.state.battery_timestamp is None or \
|
|
946
|
+
self.settings.state.battery_raw is None or \
|
|
947
|
+
(now - self.settings.state.battery_timestamp).total_seconds() > 1:
|
|
948
|
+
self.settings.state.battery_timestamp = now
|
|
949
|
+
response = self.get_upper_code(0x13, label="GET_BATTERY_STATE", msb_len=3)
|
|
950
|
+
self.settings.state.battery_raw = response.data
|
|
951
|
+
log.debug(f"battery_state_raw: 0x{self.settings.state.battery_raw:06x}")
|
|
947
952
|
|
|
948
|
-
self.settings.state.battery_timestamp = now
|
|
949
|
-
response = self.get_upper_code(0x13, label="GET_BATTERY_STATE", msb_len=3)
|
|
950
|
-
self.settings.state.battery_raw = response.data
|
|
951
|
-
|
|
952
|
-
log.debug(f"battery_state_raw: 0x{self.settings.state.battery_raw:04x}")
|
|
953
953
|
return SpectrometerResponse(data=self.settings.state.battery_raw)
|
|
954
954
|
|
|
955
955
|
def get_battery_percentage(self):
|
|
@@ -958,7 +958,7 @@ class FeatureIdentificationDevice(InterfaceDevice):
|
|
|
958
958
|
lsb = (word >> 16) & 0xff
|
|
959
959
|
msb = (word >> 8) & 0xff
|
|
960
960
|
perc = msb + (1.0 * lsb / 256.0)
|
|
961
|
-
log.debug("battery_perc:
|
|
961
|
+
log.debug(f"battery_perc: msb 0x{msb:02x} + lsb 0x{lsb:02x} = {perc:0.2f}%%")
|
|
962
962
|
return SpectrometerResponse(data=perc)
|
|
963
963
|
|
|
964
964
|
def get_battery_charging(self):
|
|
@@ -259,10 +259,11 @@ class IDSCamera:
|
|
|
259
259
|
self.node_map = None
|
|
260
260
|
self.long_name = None
|
|
261
261
|
self.datastream = None
|
|
262
|
+
self.dumped = False
|
|
262
263
|
|
|
263
264
|
self.area_scan_enabled = False
|
|
264
265
|
self.take_one_request = None
|
|
265
|
-
self.
|
|
266
|
+
self.started = False
|
|
266
267
|
self.shutdown_in_progress = False
|
|
267
268
|
self.last_asi_timestamp = None
|
|
268
269
|
self.last_area_scan_image = None
|
|
@@ -380,19 +381,50 @@ class IDSCamera:
|
|
|
380
381
|
self.start_line = 0
|
|
381
382
|
self.stop_line = self.height - 1
|
|
382
383
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
self.node_map.FindNode("
|
|
384
|
+
self.set_user_set("Default")
|
|
385
|
+
|
|
386
|
+
# default to 15ms
|
|
387
|
+
self.node_map.FindNode("ExposureTime").SetValue(15_000) # value in µs (note this is valid in Default, but not LongExposure)
|
|
388
|
+
|
|
389
|
+
log.debug(f"connect: successfully connected to {self.long_name}")
|
|
390
|
+
return True
|
|
391
|
+
|
|
392
|
+
def doing_default(self):
|
|
393
|
+
selector = self.node_map.FindNode("UserSetSelector")
|
|
394
|
+
return "Default" == selector.CurrentEntry().SymbolicValue()
|
|
395
|
+
|
|
396
|
+
def doing_long_exposure(self):
|
|
397
|
+
selector = self.node_map.FindNode("UserSetSelector")
|
|
398
|
+
return "LongExposure" == selector.CurrentEntry().SymbolicValue()
|
|
399
|
+
|
|
400
|
+
def set_user_set(self, entry):
|
|
401
|
+
if entry not in ["Default", "LongExposure"]:
|
|
402
|
+
log.error(f"ignoring unsupported UserSetSelector {entry}")
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
selector = self.node_map.FindNode("UserSetSelector")
|
|
406
|
+
current_entry = selector.CurrentEntry().SymbolicValue()
|
|
407
|
+
if entry == current_entry:
|
|
408
|
+
log.debug(f"set_user_set: UserSet already {entry}")
|
|
409
|
+
return
|
|
410
|
+
log.debug(f"set_user_set: changing {current_entry} -> {entry}")
|
|
411
|
+
|
|
412
|
+
was_started = self.started
|
|
413
|
+
if was_started:
|
|
414
|
+
log.debug("set_user_set: was_started, so stopping")
|
|
415
|
+
self.stop()
|
|
416
|
+
|
|
417
|
+
selector.SetCurrentEntry(entry)
|
|
418
|
+
loader = self.node_map.FindNode("UserSetLoad")
|
|
419
|
+
loader.Execute()
|
|
420
|
+
loader.WaitUntilDone()
|
|
421
|
+
|
|
422
|
+
node = self.node_map.FindNode("ExposureTime")
|
|
423
|
+
log.debug(f"set_user_set: applied UserSetSelector {entry}, ExposureTime range now ({node.Minimum()}, {node.Maximum()})µs")
|
|
424
|
+
|
|
425
|
+
# assuming we may need to do this after changing UserSet, but haven't verified
|
|
426
|
+
self.init_software_trigger()
|
|
387
427
|
|
|
388
|
-
# initialize software trigger
|
|
389
|
-
# @see https://www.ids-imaging.us/manuals/ids-peak/ids-peak-user-manual/2.15.0/en/acquisition-control.html
|
|
390
|
-
log.debug("TriggerSelector set to ExposureStart")
|
|
391
|
-
self.node_map.FindNode("TriggerSelector").SetCurrentEntry("ReadOutStart") # "ExposureStart" not supported?
|
|
392
|
-
self.node_map.FindNode("TriggerMode").SetCurrentEntry("On")
|
|
393
|
-
self.node_map.FindNode("TriggerSource").SetCurrentEntry("Software")
|
|
394
|
-
self.node_map.FindNode("ExposureTime").SetValue(15_000) # value in µs
|
|
395
|
-
|
|
396
428
|
# nodeMapRemoteDevice.FindNode("SequencerMode") .SetCurrentEntry("Off")
|
|
397
429
|
# nodeMapRemoteDevice.FindNode("AcquisitionMode") .SetCurrentEntry("SingleFrame")
|
|
398
430
|
# nodeMapRemoteDevice.FindNode("ExposureMode") .SetCurrentEntry("Timed")
|
|
@@ -402,10 +434,21 @@ class IDSCamera:
|
|
|
402
434
|
# nodeMapRemoteDevice.FindNode("AcquisitionStart").Execute()
|
|
403
435
|
# nodeMapRemoteDevice.FindNode("AcquisitionStart").WaitUntilDone()
|
|
404
436
|
|
|
405
|
-
|
|
437
|
+
# MZ: this can take 247ms, so only do once
|
|
438
|
+
if not self.dumped:
|
|
439
|
+
self.dump_node(name="Root")
|
|
440
|
+
self.dumped = True
|
|
406
441
|
|
|
407
|
-
|
|
408
|
-
|
|
442
|
+
if was_started:
|
|
443
|
+
log.debug("set_user_set: was_started, so re-starting")
|
|
444
|
+
self.start()
|
|
445
|
+
|
|
446
|
+
def init_software_trigger(self):
|
|
447
|
+
# @see https://www.ids-imaging.us/manuals/ids-peak/ids-peak-user-manual/2.15.0/en/acquisition-control.html
|
|
448
|
+
log.debug("TriggerSelector set to ExposureStart")
|
|
449
|
+
self.node_map.FindNode("TriggerSelector").SetCurrentEntry("ReadOutStart") # "ExposureStart" not supported?
|
|
450
|
+
self.node_map.FindNode("TriggerMode").SetCurrentEntry("On")
|
|
451
|
+
self.node_map.FindNode("TriggerSource").SetCurrentEntry("Software")
|
|
409
452
|
|
|
410
453
|
def set_dark_asi(self, asi):
|
|
411
454
|
self.dark = asi
|
|
@@ -430,12 +473,20 @@ class IDSCamera:
|
|
|
430
473
|
self.stop_line = n
|
|
431
474
|
|
|
432
475
|
def set_integration_time_ms(self, ms):
|
|
433
|
-
#
|
|
476
|
+
# manually clamp to apparent "multi-UserSet" range of (15ms, 120sec)
|
|
477
|
+
# deliberately round to ms resolution to simplify barrier comparisons
|
|
478
|
+
ms = int(round(min(120_000, max(ms, 15)), 0))
|
|
479
|
+
|
|
434
480
|
us = ms * 1000.0
|
|
481
|
+
log.debug(f"set_integration_time_ms: ms {ms}, us {us}")
|
|
435
482
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
us
|
|
483
|
+
if us <= 1_001_000:
|
|
484
|
+
self.set_user_set("Default")
|
|
485
|
+
elif us >= 2_000_000:
|
|
486
|
+
self.set_user_set("LongExposure")
|
|
487
|
+
else:
|
|
488
|
+
# either mode can apparently support (1001, 1999ms)
|
|
489
|
+
pass
|
|
439
490
|
|
|
440
491
|
log.debug(f"set_integration_time_ms: integration time {us} µs")
|
|
441
492
|
self.node_map.FindNode("ExposureTime").SetValue(us)
|
|
@@ -457,11 +508,11 @@ class IDSCamera:
|
|
|
457
508
|
log.debug("start: no device")
|
|
458
509
|
return
|
|
459
510
|
|
|
460
|
-
if self.
|
|
511
|
+
if self.started:
|
|
461
512
|
log.debug("start: already running")
|
|
462
513
|
return
|
|
463
514
|
|
|
464
|
-
|
|
515
|
+
self.datastream = None # kludge
|
|
465
516
|
if self.datastream is None:
|
|
466
517
|
log.debug("start: initializing datastream")
|
|
467
518
|
self.datastream = self.device.DataStreams()[0].OpenDataStream()
|
|
@@ -512,7 +563,7 @@ class IDSCamera:
|
|
|
512
563
|
self.datastream.StartAcquisition()
|
|
513
564
|
self.node_map.FindNode("AcquisitionStart").Execute()
|
|
514
565
|
self.node_map.FindNode("AcquisitionStart").WaitUntilDone()
|
|
515
|
-
self.
|
|
566
|
+
self.started = True
|
|
516
567
|
|
|
517
568
|
log.debug("start: started")
|
|
518
569
|
except:
|
|
@@ -549,11 +600,11 @@ class IDSCamera:
|
|
|
549
600
|
pass
|
|
550
601
|
|
|
551
602
|
def stop(self):
|
|
552
|
-
""" Called
|
|
603
|
+
""" Called when changing UserSet, and at shutdown. """
|
|
553
604
|
if self.device is None:
|
|
554
605
|
return
|
|
555
606
|
|
|
556
|
-
if not self.
|
|
607
|
+
if not self.started:
|
|
557
608
|
log.debug("stop: not running")
|
|
558
609
|
return
|
|
559
610
|
|
|
@@ -565,7 +616,7 @@ class IDSCamera:
|
|
|
565
616
|
# Discard all buffers from the acquisition engine
|
|
566
617
|
# They remain in the announced buffer pool
|
|
567
618
|
self.datastream.Flush(IDSPeak.DataStreamFlushMode_DiscardAll)
|
|
568
|
-
self.
|
|
619
|
+
self.started = False
|
|
569
620
|
|
|
570
621
|
# Unlock parameters
|
|
571
622
|
self.node_map.FindNode("TLParamsLocked").SetValue(0)
|
|
@@ -618,27 +669,33 @@ class IDSCamera:
|
|
|
618
669
|
self.take_one_request = None
|
|
619
670
|
|
|
620
671
|
def send_trigger(self):
|
|
672
|
+
# log.debug("send_trigger: start...")
|
|
621
673
|
self.node_map.FindNode("TriggerSoftware").Execute()
|
|
622
674
|
self.node_map.FindNode("TriggerSoftware").WaitUntilDone()
|
|
675
|
+
# log.debug("send_trigger: ...done")
|
|
623
676
|
|
|
624
677
|
def get_spectrum(self):
|
|
678
|
+
# log.debug("get_spectrum: start")
|
|
625
679
|
if self.datastream is None:
|
|
626
680
|
log.error("get_spectrum: no datastream?!")
|
|
627
681
|
return
|
|
628
682
|
|
|
629
683
|
timeout_ms = int(round(1000 + 2 * max(self.integration_time_ms, self.last_integration_time_ms)))
|
|
630
684
|
try:
|
|
685
|
+
# log.debug(f"get_spectrum: waiting on finished buffer (timeout {timeout_ms}ms)")
|
|
631
686
|
buffer = self.datastream.WaitForFinishedBuffer(timeout_ms) # takes ms
|
|
632
687
|
except:
|
|
633
|
-
|
|
688
|
+
log.error(f"failed on datastream.WaitForFinishedBuffer(timeout {timeout_ms}ms)", exc_info=1)
|
|
634
689
|
return
|
|
635
690
|
|
|
636
691
|
# Get image from buffer (shallow copy)
|
|
692
|
+
# log.debug(f"get_spectrum: converting to image")
|
|
637
693
|
image = EXT.BufferToImage(buffer)
|
|
638
694
|
|
|
639
695
|
if True:
|
|
640
696
|
# normal case, just vertically bin using the configured format
|
|
641
697
|
# (pass the buffer so it can be released after conversion)
|
|
698
|
+
# log.debug(f"get_spectrum: vertically binning")
|
|
642
699
|
spectrum = self.vertically_bin_image(image, buffer=buffer)
|
|
643
700
|
else:
|
|
644
701
|
# debug code to experimentally convert the same frame into every
|
|
@@ -655,6 +712,7 @@ class IDSCamera:
|
|
|
655
712
|
log.error(f"exception during conversion to {format_name}", exc_info=1)
|
|
656
713
|
|
|
657
714
|
self.last_integration_time_ms = self.integration_time_ms
|
|
715
|
+
# log.debug("get_spectrum: done")
|
|
658
716
|
return spectrum
|
|
659
717
|
|
|
660
718
|
def vertically_bin_image(self, image, format_name=None, buffer=None, save_csv=False, save_png=False):
|
|
@@ -58,6 +58,8 @@ class IDSDevice(InterfaceDevice):
|
|
|
58
58
|
self.settings = SpectrometerSettings(self.device_id)
|
|
59
59
|
self.settings.eeprom.excitation_nm_float = 785
|
|
60
60
|
self.settings.eeprom.wavecal_coeffs = [0, 1, 0, 0, 0]
|
|
61
|
+
self.settings.eeprom.min_integration_time_ms = 15 # Default UserSet
|
|
62
|
+
self.settings.eeprom.max_integration_time_ms = 120_000 # LongExposure UserSet
|
|
61
63
|
|
|
62
64
|
# stomp from camera
|
|
63
65
|
self.settings.eeprom.model = self.camera.model_name
|
|
@@ -113,39 +115,44 @@ class IDSDevice(InterfaceDevice):
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
"""
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
log.debug(f"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
118
|
+
prefix = f"IDS-{self.camera.serial_number}"
|
|
119
|
+
suffix = ".json"
|
|
120
|
+
json_path = utils.find_first_file(path=self.config_dir, prefix=prefix, suffix=suffix)
|
|
121
|
+
if not json_path:
|
|
122
|
+
log.debug(f"could not find file with prefix {prefix}, suffix {suffix} in path {self.config_dir}")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
with open(json_path) as config_file:
|
|
126
|
+
data = json.load(config_file)
|
|
127
|
+
log.debug(f"loaded from {json_path}: {data}")
|
|
128
|
+
|
|
129
|
+
def stomp(k, attr=None):
|
|
130
|
+
if attr is None:
|
|
131
|
+
attr = k
|
|
132
|
+
log.debug(f"attempting to stomp {k}")
|
|
133
|
+
else:
|
|
134
|
+
log.debug(f"attempting to stomp {k} --> {attr}")
|
|
135
|
+
|
|
136
|
+
if k in data:
|
|
137
|
+
value = data[k]
|
|
138
|
+
log.debug(f"stomping eeprom.{attr} = {value}")
|
|
139
|
+
setattr(self.settings.eeprom, attr, value)
|
|
140
|
+
|
|
141
|
+
for k in [ "detector_gain",
|
|
142
|
+
"excitation_nm_float",
|
|
143
|
+
"invert_x_axis",
|
|
144
|
+
"horiz_binning_enabled",
|
|
145
|
+
"horiz_binning_mode",
|
|
146
|
+
"startup_integration_time_ms",
|
|
147
|
+
"wavelength_coeffs",
|
|
148
|
+
"roi_horizontal_end",
|
|
149
|
+
"roi_horizontal_start",
|
|
150
|
+
"roi_vertical_region_1_start",
|
|
151
|
+
"roi_vertical_region_1_end" ]:
|
|
152
|
+
stomp(k)
|
|
153
|
+
for k, attr in [ [ "wp_model", "model" ],
|
|
154
|
+
[ "wp_serial_number", "serial_number" ] ]:
|
|
155
|
+
stomp(k, attr)
|
|
149
156
|
|
|
150
157
|
def set_integration_time_ms(self, ms):
|
|
151
158
|
"""
|
|
@@ -213,17 +220,22 @@ class IDSDevice(InterfaceDevice):
|
|
|
213
220
|
|
|
214
221
|
def get_spectrum(self):
|
|
215
222
|
try:
|
|
223
|
+
# log.debug("get_spectrum: calling send_trigger")
|
|
216
224
|
self.camera.send_trigger()
|
|
225
|
+
# log.debug("get_spectrum: back from send_trigger, calling camera.get_spectrum")
|
|
217
226
|
spectrum = self.camera.get_spectrum()
|
|
227
|
+
# log.debug("get_spectrum: back from camera.get_spectrum")
|
|
218
228
|
except:
|
|
219
229
|
log.error("error getting spectrum from IDSCamera", exc_info=1)
|
|
220
230
|
return SpectrometerResponse(False)
|
|
221
231
|
|
|
222
232
|
if spectrum is None:
|
|
223
|
-
|
|
233
|
+
log.debug("get_spectrum: received None")
|
|
224
234
|
return SpectrometerResponse(False)
|
|
225
235
|
|
|
236
|
+
# log.debug("get_spectrum: applying horizontal binning")
|
|
226
237
|
spectrum = self.apply_horizontal_binning(spectrum)
|
|
238
|
+
# log.debug("get_spectrum: done")
|
|
227
239
|
return spectrum
|
|
228
240
|
|
|
229
241
|
def apply_horizontal_binning(self, spectrum):
|
|
@@ -262,14 +274,15 @@ class IDSDevice(InterfaceDevice):
|
|
|
262
274
|
|
|
263
275
|
def acquire_data(self):
|
|
264
276
|
# pre-process scan averaging
|
|
265
|
-
log.debug(f"acquire_data: scans_to_average {self.settings.state.scans_to_average}")
|
|
277
|
+
# log.debug(f"acquire_data: start (scans_to_average {self.settings.state.scans_to_average})")
|
|
266
278
|
if self.settings.state.scans_to_average > 1:
|
|
267
279
|
if self.sum_count > self.settings.state.scans_to_average:
|
|
268
280
|
self.reset_averaging()
|
|
269
281
|
|
|
270
282
|
reading = Reading(self.device_id)
|
|
271
|
-
|
|
283
|
+
# log.debug("acquire_data: calling get_spectrum")
|
|
272
284
|
reading.spectrum = self.get_spectrum()
|
|
285
|
+
# log.debug("acquire_data: back from get_spectrum")
|
|
273
286
|
|
|
274
287
|
if not self.camera:
|
|
275
288
|
return SpectrometerResponse(False)
|
|
@@ -301,9 +314,12 @@ class IDSDevice(InterfaceDevice):
|
|
|
301
314
|
else:
|
|
302
315
|
log.debug(f"acquire_data: averaged {self.sum_count}/{self.settings.state.scans_to_average}")
|
|
303
316
|
|
|
304
|
-
log.debug(f"acquire_data: returning {reading}")
|
|
317
|
+
# log.debug(f"acquire_data: returning {reading}")
|
|
305
318
|
return SpectrometerResponse(data=reading)
|
|
306
319
|
|
|
320
|
+
def heartbeat(self, arg):
|
|
321
|
+
return SpectrometerResponse(data=True)
|
|
322
|
+
|
|
307
323
|
############################################################################
|
|
308
324
|
# utility
|
|
309
325
|
############################################################################
|
|
@@ -319,6 +335,7 @@ class IDSDevice(InterfaceDevice):
|
|
|
319
335
|
process_f["close"] = self.disconnect
|
|
320
336
|
|
|
321
337
|
process_f["acquire_data"] = self.acquire_data
|
|
338
|
+
process_f["heartbeat"] = self.heartbeat
|
|
322
339
|
|
|
323
340
|
# deliberately don't support gain_factor or detector_gain
|
|
324
341
|
process_f["gain_db"] = lambda x: self.set_gain_db(float(x))
|
|
@@ -218,10 +218,25 @@ def get_default_data_dir():
|
|
|
218
218
|
return os.path.join(os.path.expanduser("~"), "Documents", "EnlightenSpectra")
|
|
219
219
|
return os.path.join(os.environ["HOME"], "EnlightenSpectra")
|
|
220
220
|
|
|
221
|
-
|
|
221
|
+
def find_first_file(path, prefix=None, suffix=None, recursive=False):
|
|
222
|
+
pat = f"^{prefix}" if prefix else ""
|
|
223
|
+
if suffix:
|
|
224
|
+
suffix = suffix.replace(".", "\\.")
|
|
225
|
+
pat += f".*{suffix}"
|
|
226
|
+
# log.debug(f"find_first_file: pat [{pat}]")
|
|
227
|
+
files = get_pathnames_from_directory(rootdir=path, pattern=pat, recursive=recursive)
|
|
228
|
+
if len(files) < 1:
|
|
229
|
+
return None
|
|
230
|
+
else:
|
|
231
|
+
return files[0]
|
|
232
|
+
|
|
222
233
|
def get_pathnames_from_directory(rootdir, pattern=None, recursive=False):
|
|
234
|
+
"""
|
|
235
|
+
Iterate down a directory, returning pathnames that match the given pattern in
|
|
236
|
+
deterministic sorted order.
|
|
237
|
+
"""
|
|
223
238
|
pathnames = []
|
|
224
|
-
# log.debug("searching %s matching %s with recursive %s", rootdir, pattern, recursive)
|
|
239
|
+
# log.debug("get_pathnames_from_directory: searching %s matching %s with recursive %s", rootdir, pattern, recursive)
|
|
225
240
|
if recursive:
|
|
226
241
|
for (directory, _, filenames) in os.walk(rootdir):
|
|
227
242
|
for filename in filenames:
|
|
@@ -230,7 +245,7 @@ def get_pathnames_from_directory(rootdir, pattern=None, recursive=False):
|
|
|
230
245
|
if re.search(pattern, filename):
|
|
231
246
|
pathnames.append(pathname)
|
|
232
247
|
else:
|
|
233
|
-
# log.debug("%s does not match %s", pathname, pattern)
|
|
248
|
+
# log.debug("get_pathnames_from_directory: %s does not match %s", pathname, pattern)
|
|
234
249
|
pass
|
|
235
250
|
else:
|
|
236
251
|
pathnames.append(pathname)
|
|
@@ -242,11 +257,12 @@ def get_pathnames_from_directory(rootdir, pattern=None, recursive=False):
|
|
|
242
257
|
if re.search(pattern, filename):
|
|
243
258
|
pathnames.append(pathname)
|
|
244
259
|
else:
|
|
245
|
-
# log.debug("%s does not match %s", pathname, pattern)
|
|
260
|
+
# log.debug("get_pathnames_from_directory: %s does not match %s", pathname, pattern)
|
|
246
261
|
pass
|
|
247
262
|
else:
|
|
248
263
|
pathnames.append(pathname)
|
|
249
|
-
|
|
264
|
+
pathnames.sort()
|
|
265
|
+
# log.debug("get_pathnames_from_directory: returning %s", pathnames)
|
|
250
266
|
return pathnames
|
|
251
267
|
|
|
252
268
|
##
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|