wasatch 2.3.6__tar.gz → 2.3.8__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.6 → wasatch-2.3.8}/CHANGELOG.md +12 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/PKG-INFO +1 -1
- wasatch-2.3.8/demo-ids-raman.py +281 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/pyproject.toml +1 -1
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/AndorDevice.py +39 -32
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/AreaScanImage.py +1 -1
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/EEPROM.py +11 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/FeatureIdentificationDevice.py +27 -18
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/IDSCamera.py +40 -11
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/IDSDevice.py +17 -11
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/Reading.py +1 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/SpectrometerSettings.py +4 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/TCPDevice.py +24 -7
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/WasatchDevice.py +44 -33
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/WasatchDeviceWrapper.py +1 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/WrapperWorker.py +10 -6
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/__init__.py +1 -1
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/applog.py +1 -1
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/utils.py +1 -1
- {wasatch-2.3.6 → wasatch-2.3.8}/.example_pypirc +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/.flake8 +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/.gitignore +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/.pylintrc +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/Doxyfile +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/LICENSE +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/MANIFEST.in +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/Makefile +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/README.md +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/README_PYPI.md +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/README_RPI.md +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/README_SETTINGS.md +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/README_VIRTUAL.md +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/WasatchShell/README.md +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/WasatchShell/laser-test.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/WasatchShell/load-test.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/WasatchShell/one-shot.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/WasatchShell/wasatch-shell.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/demo-auto-raman.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/demo-simple.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/demo-virtual.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/demo-workflow.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/demo.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/environments/conda-linux.yml +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/environments/conda-macos.yml +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/environments/conda-rpi.yml +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/environments/conda-win10.yml +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/screenshots/multiplatform.png +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/scripts/dark-vs-integ.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/scripts/deploy +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/testSpectrometers/SiG_785/eeprom/EEPROM-EM-9c65d19f4c.json +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/testSpectrometers/SiG_785/readings/enlighten-20210329-094722-799520-WP-00686.csv +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/testSpectrometers/SiG_785/readings/enlighten-20210329-094755-288958-WP-00686.csv +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/testSpectrometers/WP-00887/eeprom/WP-00887-mock.json +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/testSpectrometers/WP-00904/eeprom/WP-00904-mock.json +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/testSpectrometers/WP-00913/eeprom/WP-00913-mock.json +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/tests/existing_implementations.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/tests/noise-by-setpoint.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/tests/queues.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/tests/unit.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/udev/10-wasatch.rules +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/AbstractUSBDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/AutoRaman.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/AutoRamanRequest.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/BLEDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/BalanceAcquisition.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/CSVLoader.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/CommandSettings.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/ControlObject.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/DetectorROI.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/DetectorRegions.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/DeviceFinderBLE.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/DeviceFinderTCP.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/DeviceFinderUSB.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/DeviceID.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/EEPROM_multical.sav +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/FPGAOptions.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/FirmwareRequirements.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/HardwareInfo.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/IMX385.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/InterfaceDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/MockUSBDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/OceanDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/PollStatus.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/ProcessedReading.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/ROI.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/RealUSBDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/SPIDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/SpectrometerRequest.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/SpectrometerResponse.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/SpectrometerState.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/StatusMessage.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/TakeOneRequest.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/WasatchBus.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.8}/wasatch/simulation_protocol.py +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
- 2026-01-12 2.3.8
|
|
4
|
+
- added settings.has_detector
|
|
5
|
+
- improved support for Andor .json "virtual EEPROMs"
|
|
6
|
+
- bump internal WrapperWorker poll rate from 20Hz to 200Hz
|
|
7
|
+
- retain debug logging for 20sec after connection (vs previous 10sec)
|
|
8
|
+
- only update Andor detector temperature at 1Hz
|
|
9
|
+
- improved support for using 220250 boards as standalone laser drivers w/o
|
|
10
|
+
detectors (added Reading.keep_alive to indicate metadata but no spectra)
|
|
11
|
+
- 2025-12-10 2.3.7
|
|
12
|
+
- added EEPROM.is_oem
|
|
13
|
+
- fixed TCPDevice.set_detector_gain and set_vertical_roi
|
|
14
|
+
- disable use of eeprom.actual_pixels_horizontal during acquisition
|
|
3
15
|
- 2025-11-06 2.3.6
|
|
4
16
|
- resolve hotplug race condition when laser power set in percent
|
|
5
17
|
- cache certain device getters to avoid I2C overruns
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
################################################################################
|
|
3
|
+
# demo-ids-raman.py #
|
|
4
|
+
################################################################################
|
|
5
|
+
# #
|
|
6
|
+
# DESCRIPTION: Simple cmd-line demo to control an XS spectrometer using IDS #
|
|
7
|
+
# detector and 220250 Rev4 laser driver. #
|
|
8
|
+
# #
|
|
9
|
+
# NOTES: This is for the fairly unusual case of having an XS bench built with #
|
|
10
|
+
# an IDS camera (providing its own USB3 port for comms and control), #
|
|
11
|
+
# plus a 220250 Rev4 PCB for use as a standalone laser driver (but w/o #
|
|
12
|
+
# an IMX385 detector connected). #
|
|
13
|
+
# #
|
|
14
|
+
################################################################################
|
|
15
|
+
|
|
16
|
+
# DEPENDENCIES: pywin32, psutil, seabreeze, pyftdi, crcmod, bleak
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
import numpy as np
|
|
23
|
+
import signal
|
|
24
|
+
import logging
|
|
25
|
+
import argparse
|
|
26
|
+
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from time import sleep
|
|
29
|
+
|
|
30
|
+
import wasatch
|
|
31
|
+
from wasatch import applog
|
|
32
|
+
from wasatch.DeviceID import DeviceID
|
|
33
|
+
from wasatch.IDSDevice import IDSDevice
|
|
34
|
+
from wasatch.WasatchBus import WasatchBus
|
|
35
|
+
from wasatch.WasatchDevice import WasatchDevice
|
|
36
|
+
|
|
37
|
+
log = logging.getLogger(__name__)
|
|
38
|
+
demo = None
|
|
39
|
+
|
|
40
|
+
class WasatchDemo:
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
self.camera_device = None # a wasatch.IDSDevice wrapping a wasatch.IDSCamera (as 'camera')
|
|
44
|
+
self.laser_device = None # a wasatch.WasatchDevice wrapping a wasatch.FeatureIdentificationDevice (as 'hardware')
|
|
45
|
+
self.reading_count = 0
|
|
46
|
+
self.exiting = False
|
|
47
|
+
|
|
48
|
+
self.args = self.parse_args()
|
|
49
|
+
|
|
50
|
+
print(f"Wasatch.PY {wasatch.__version__} IDS Raman Demo")
|
|
51
|
+
|
|
52
|
+
def parse_args(self):
|
|
53
|
+
parser = argparse.ArgumentParser(description="Simple demo to acquire spectra from command-line interface")
|
|
54
|
+
parser.add_argument("--log-level", type=str, default="INFO", help="logging level", choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL","NEVER"])
|
|
55
|
+
parser.add_argument("--integration-time-ms", type=int, default=1000, help="integration time (ms, default 10)")
|
|
56
|
+
parser.add_argument("--take-dark", action="store_true", help="first measurement is used for dark subtraction")
|
|
57
|
+
parser.add_argument("--enable-laser", action="store_true", help="fire laser (after optional dark)")
|
|
58
|
+
parser.add_argument("--max", type=int, default=0, help="max spectra to acquire (default 0, unlimited)")
|
|
59
|
+
parser.add_argument("--save-spectra", action="store_true", help="save vertically-binned spectra to row-ordered CSV")
|
|
60
|
+
parser.add_argument("--save-png", action="store_true", help="save PNG of each image frame")
|
|
61
|
+
parser.add_argument("--save-data", action="store_true", help="save 2D array of each image frame")
|
|
62
|
+
parser.add_argument("--save-dir", type=str, default=".", help="directory to save files")
|
|
63
|
+
parser.add_argument("--prefix", type=str, default="ids-raman", help="filename prefix")
|
|
64
|
+
|
|
65
|
+
args = parser.parse_args()
|
|
66
|
+
applog.MainLogger(args.log_level)
|
|
67
|
+
|
|
68
|
+
return args
|
|
69
|
+
|
|
70
|
+
def connect(self):
|
|
71
|
+
|
|
72
|
+
########################################################################
|
|
73
|
+
# Look for laser driver board
|
|
74
|
+
########################################################################
|
|
75
|
+
|
|
76
|
+
bus = WasatchBus()
|
|
77
|
+
if not bus.device_ids:
|
|
78
|
+
print("No Wasatch USB spectrometers found.")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# look for laser driver on WasatchBus -- we won't see the camera here,
|
|
82
|
+
# because IDSPeak devices are not automatically considered for inclusion
|
|
83
|
+
# by WasatchBus (although they could be)
|
|
84
|
+
for device_id in bus.device_ids:
|
|
85
|
+
log.debug("connect: trying to connect to %s", device_id)
|
|
86
|
+
device = WasatchDevice(device_id)
|
|
87
|
+
ok = device.connect()
|
|
88
|
+
if not ok:
|
|
89
|
+
log.critical("connect: can't connect to %s", device_id)
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
log.debug(f"connect: connected to {device_id} ({device})")
|
|
93
|
+
if device.settings is None:
|
|
94
|
+
log.error("can't have device without SpectrometerSettings")
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
print(f"connected to {device.settings.full_model()} {device.settings.eeprom.serial_number} with {device.settings.pixels()} pixels " +
|
|
98
|
+
f"from ({device.settings.wavelengths[0]:.2f}, {device.settings.wavelengths[-1]:.2f}nm)" +
|
|
99
|
+
( f" ({device.settings.wavenumbers[0]:.2f}, {device.settings.wavenumbers[-1]:.2f}cm⁻¹)" if device.settings.has_excitation() else "") +
|
|
100
|
+
f" (Microcontroller {device.settings.microcontroller_firmware_version}," +
|
|
101
|
+
f" FPGA {device.settings.fpga_firmware_version})")
|
|
102
|
+
|
|
103
|
+
self.laser_device = device
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
if self.laser_device is None:
|
|
107
|
+
log.critical("could not find laser driver, exitting")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
########################################################################
|
|
111
|
+
# now look for an IDS camera
|
|
112
|
+
########################################################################
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
device_id = DeviceID(label="IDSPeak")
|
|
116
|
+
device = IDSDevice(device_id=device_id, scratch_dir=".", consumer_deletes_area_scan_image=True)
|
|
117
|
+
if device.connect():
|
|
118
|
+
self.camera_device = device
|
|
119
|
+
else:
|
|
120
|
+
log.error("failed connecting to IDSCamera")
|
|
121
|
+
except:
|
|
122
|
+
log.critical("exception connecting to IDS camera", exc_info=1)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
if self.camera_device is None:
|
|
126
|
+
log.critical("could not find IDS camera, exitting")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
device = self.camera_device
|
|
130
|
+
camera = device.camera
|
|
131
|
+
settings = device.settings
|
|
132
|
+
eeprom = settings.eeprom
|
|
133
|
+
|
|
134
|
+
device.set_area_scan_enable(True)
|
|
135
|
+
|
|
136
|
+
# copy the laser excitation over to the camera "virtual EEPROM" so we can
|
|
137
|
+
# compute wavenumbers
|
|
138
|
+
eeprom.excitation_nm = self.laser_device.settings.eeprom.excitation_nm
|
|
139
|
+
settings.update_wavecal()
|
|
140
|
+
print(f"connected to IDSPeak camera {eeprom.model} {eeprom.serial_number} with {camera.width}x{camera.height} pixels " +
|
|
141
|
+
f"from ({settings.wavelengths[0]:.2f}, {settings.wavelengths[-1]:.2f}nm)" +
|
|
142
|
+
( f" ({settings.wavenumbers[0]:.2f}, {settings.wavenumbers[-1]:.2f}cm⁻¹)" if settings.has_excitation() else ""))
|
|
143
|
+
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
############################################################################
|
|
147
|
+
# #
|
|
148
|
+
# Run-Time Loop #
|
|
149
|
+
# #
|
|
150
|
+
############################################################################
|
|
151
|
+
|
|
152
|
+
def run(self):
|
|
153
|
+
self.camera_device.set_integration_time_ms(self.args.integration_time_ms)
|
|
154
|
+
|
|
155
|
+
# initialize outfile if saving spectra
|
|
156
|
+
outfile = None
|
|
157
|
+
if self.args.save_spectra:
|
|
158
|
+
filename = os.path.join(self.args.save_dir, self.args.prefix + "-spectra.csv")
|
|
159
|
+
outfile = open(filename, "w")
|
|
160
|
+
outfile.write("pixels, 0, " + ", ".join([f"{x}" for x in range(self.camera_device.settings.pixels())]) + "\n")
|
|
161
|
+
outfile.write("wavelengths, 0, " + ", ".join([f"{x:.2f}" for x in self.camera_device.settings.wavelengths]) + "\n")
|
|
162
|
+
outfile.write("wavenumbers, 0, " + ", ".join([f"{x:.2f}" for x in self.camera_device.settings.wavenumbers]) + "\n")
|
|
163
|
+
outfile.write("time, count, spectrum\n")
|
|
164
|
+
|
|
165
|
+
# read spectra until user presses Control-Break
|
|
166
|
+
while not self.exiting and (self.args.max == 0 or self.reading_count < self.args.max):
|
|
167
|
+
|
|
168
|
+
####################################################################
|
|
169
|
+
# check to fire laser
|
|
170
|
+
####################################################################
|
|
171
|
+
|
|
172
|
+
# on the first reading (or the second, if taking dark), enable the
|
|
173
|
+
# laser if requested
|
|
174
|
+
if (self.args.enable_laser and (self.reading_count == 0 or (self.args.take_dark and self.reading_count == 1))):
|
|
175
|
+
print("enabling laser")
|
|
176
|
+
self.laser_device.hardware.set_laser_enable(True)
|
|
177
|
+
|
|
178
|
+
####################################################################
|
|
179
|
+
# read image
|
|
180
|
+
####################################################################
|
|
181
|
+
|
|
182
|
+
if self.exiting:
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
response = self.camera_device.acquire_data()
|
|
186
|
+
if response.keep_alive:
|
|
187
|
+
log.debug(f"ignoring keepalive from {device}")
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
if isinstance(response.data, bool):
|
|
191
|
+
if response.data:
|
|
192
|
+
log.debug("received poison-pill, exiting")
|
|
193
|
+
break
|
|
194
|
+
else:
|
|
195
|
+
log.debug("no reading available")
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
if response.data.failure:
|
|
199
|
+
log.debug("reading indicated failure")
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
if self.exiting:
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
self.reading_count += 1
|
|
206
|
+
|
|
207
|
+
reading = response.data
|
|
208
|
+
spectrum = reading.spectrum
|
|
209
|
+
settings = self.camera_device.settings
|
|
210
|
+
asi = reading.area_scan_image
|
|
211
|
+
ts = reading.timestamp.strftime("%Y%m%d-%H%M%S-%f")
|
|
212
|
+
|
|
213
|
+
# print some stats as we go
|
|
214
|
+
spectrum_min = np.amin(spectrum)
|
|
215
|
+
spectrum_max = np.amax(spectrum)
|
|
216
|
+
spectrum_avg = np.mean(spectrum)
|
|
217
|
+
spectrum_std = np.std (spectrum)
|
|
218
|
+
print(f"{ts}: {self.reading_count:4d} : min {spectrum_min:8.2f}, max {spectrum_max:8.2f}, avg {spectrum_avg:8.2f}, stdev {spectrum_std:8.2f}")
|
|
219
|
+
|
|
220
|
+
# append to row-ordered file
|
|
221
|
+
if outfile:
|
|
222
|
+
values = ",".join(format(x, ".2f") for x in spectrum)
|
|
223
|
+
outfile.write(f"{reading.timestamp}, {self.reading_count}, {values}\n")
|
|
224
|
+
|
|
225
|
+
# save PNG
|
|
226
|
+
if self.args.save_png and asi.pathname_png:
|
|
227
|
+
filename = os.path.join(self.args.save_dir, f"{self.args.prefix}-{self.reading_count:04d}.png")
|
|
228
|
+
log.debug(f"renaming {asi.pathname_png} -> {filename}")
|
|
229
|
+
os.replace(asi.pathname_png, filename)
|
|
230
|
+
print(f"\tsaved {filename}")
|
|
231
|
+
|
|
232
|
+
# save area scan data
|
|
233
|
+
if self.args.save_data and asi.data is not None:
|
|
234
|
+
filename = os.path.join(self.args.save_dir, f"{self.args.prefix}-{self.reading_count:04d}.csv")
|
|
235
|
+
np.savetxt(filename, asi.data, fmt='%d', delimiter=",")
|
|
236
|
+
print(f"\tsaved {filename}")
|
|
237
|
+
|
|
238
|
+
if self.args.take_dark and self.reading_count == 1:
|
|
239
|
+
print("\tsaving dark")
|
|
240
|
+
self.camera_device.camera.set_dark_asi(asi)
|
|
241
|
+
|
|
242
|
+
################################################################################
|
|
243
|
+
# main()
|
|
244
|
+
################################################################################
|
|
245
|
+
|
|
246
|
+
def signal_handler(signal, frame):
|
|
247
|
+
print('\rInterrupted by Ctrl-C...shutting down', end=' ')
|
|
248
|
+
shutdown()
|
|
249
|
+
|
|
250
|
+
def shutdown():
|
|
251
|
+
if demo:
|
|
252
|
+
demo.exiting = True
|
|
253
|
+
if demo.laser_device is not None:
|
|
254
|
+
demo.laser_device.hardware.set_laser_enable(False)
|
|
255
|
+
demo.laser_device.disconnect()
|
|
256
|
+
|
|
257
|
+
if demo.camera_device is not None:
|
|
258
|
+
demo.camera_device.disconnect()
|
|
259
|
+
sleep(1)
|
|
260
|
+
|
|
261
|
+
log.debug(None)
|
|
262
|
+
applog.explicit_log_close()
|
|
263
|
+
sys.exit()
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
# add a signal-handler to catch ctrl-C interrupts, so we can cleanly close
|
|
267
|
+
# down all connected devices
|
|
268
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
269
|
+
|
|
270
|
+
demo = WasatchDemo()
|
|
271
|
+
if demo.connect():
|
|
272
|
+
# Note that on Windows, Control-Break (SIGBREAK) differs from
|
|
273
|
+
# Control-C (SIGINT); see https://stackoverflow.com/a/1364199
|
|
274
|
+
log.debug("Press Control-Break to interrupt...")
|
|
275
|
+
try:
|
|
276
|
+
demo.run()
|
|
277
|
+
except:
|
|
278
|
+
# catch exception so shutdown will run and disable laser
|
|
279
|
+
log.error("Caught exception during demo", exc_info=1)
|
|
280
|
+
|
|
281
|
+
shutdown()
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import struct
|
|
6
6
|
import logging
|
|
7
|
-
import datetime
|
|
7
|
+
from datetime import datetime
|
|
8
8
|
|
|
9
9
|
from ctypes import *
|
|
10
10
|
|
|
@@ -37,11 +37,11 @@ class AndorDevice(InterfaceDevice):
|
|
|
37
37
|
|
|
|
38
38
|
handle_requests
|
|
39
39
|
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
-----------
|
|
41
|
+
| | | | |
|
|
42
42
|
{ get_laser status, acquire, set_laser_watchdog, etc....}
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
| | | | |
|
|
44
|
+
-----------
|
|
45
45
|
|
|
|
46
46
|
{self.driver.some_andor_sdk_call}
|
|
47
47
|
@endverbatim
|
|
@@ -50,6 +50,7 @@ class AndorDevice(InterfaceDevice):
|
|
|
50
50
|
|
|
51
51
|
SUCCESS = 20002 #!< see load_error_codes()
|
|
52
52
|
SHUTTER_SPEED_MS = 50 #!< allow time for mechanical shutter to stabilize
|
|
53
|
+
TEMPERATURE_CACHE_SEC = 1.0
|
|
53
54
|
|
|
54
55
|
def __init__(self, device_id, message_queue=None, alert_queue=None):
|
|
55
56
|
# if passed a string representation of a DeviceID, deserialize it
|
|
@@ -83,13 +84,16 @@ class AndorDevice(InterfaceDevice):
|
|
|
83
84
|
self.driver = None
|
|
84
85
|
|
|
85
86
|
self.process_id = os.getpid()
|
|
86
|
-
self.last_memory_check = datetime.
|
|
87
|
+
self.last_memory_check = datetime.now()
|
|
87
88
|
self.last_battery_percentage = 0
|
|
88
89
|
self.spec_index = 0
|
|
89
90
|
self._scan_averaging = 1
|
|
90
91
|
self.dark = None
|
|
91
92
|
self.boxcar_half_width = 0
|
|
92
93
|
|
|
94
|
+
self.temperature_cache_value = None
|
|
95
|
+
self.temperature_cache_timestamp = None
|
|
96
|
+
|
|
93
97
|
# decide appropriate DLL filename for architecture
|
|
94
98
|
arch = 64 if 64 == struct.calcsize("P") * 8 else 32
|
|
95
99
|
filename = f"atmcd{arch}d.dll"
|
|
@@ -344,34 +348,37 @@ class AndorDevice(InterfaceDevice):
|
|
|
344
348
|
|
|
345
349
|
def get_detector_temperature_degC(self):
|
|
346
350
|
if self.tec_enabled:
|
|
347
|
-
log.debug("TEC enabled, so
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
351
|
+
log.debug("TEC enabled, so including temperature")
|
|
352
|
+
|
|
353
|
+
now = datetime.now()
|
|
354
|
+
if self.temperature_cache_timestamp is None or self.temperature_cache_value is None or (now - self.temperature_cache_timestamp).total_seconds() >= self.TEMPERATURE_CACHE_SEC:
|
|
355
|
+
# read new temperature
|
|
356
|
+
use_float = True # seems to work on 785XL (WP-01635 and WP-01491)
|
|
357
|
+
if use_float:
|
|
358
|
+
c_temp = c_float()
|
|
359
|
+
result = self.driver.GetTemperatureF(byref(c_temp))
|
|
360
|
+
else:
|
|
361
|
+
c_temp = c_int()
|
|
362
|
+
result = self.driver.GetTemperature(byref(c_temp))
|
|
363
|
+
|
|
364
|
+
label = self.get_error_code(result)
|
|
365
|
+
if label in [ "DRV_SUCCESS",
|
|
366
|
+
"DRV_TEMPERATURE_DRIFT",
|
|
367
|
+
"DRV_TEMPERATURE_STABILIZED",
|
|
368
|
+
"DRV_TEMPERATURE_NOT_REACHED",
|
|
369
|
+
"DRV_TEMPERATURE_NOT_STABILIZED" ]:
|
|
370
|
+
self.temperature_cache_value = c_temp.value
|
|
371
|
+
self.temperature_cache_timestamp = now
|
|
372
|
+
log.debug(f"Andor temperature {self.temperature_cache_value:.2f} ({label})")
|
|
373
|
+
else:
|
|
374
|
+
log.error(f"unable to read detector temperature, result was {label}")
|
|
375
|
+
self.temperature_cache_value = None
|
|
376
|
+
self.temperature_cache_timestamp = None
|
|
372
377
|
else:
|
|
373
|
-
|
|
378
|
+
# use cached value
|
|
379
|
+
pass
|
|
374
380
|
|
|
381
|
+
return self.temperature_cache_value
|
|
375
382
|
|
|
376
383
|
def _close_ex_shutter(self):
|
|
377
384
|
self.check_result(self.driver.SetShutterEx(1, 1, self.SHUTTER_SPEED_MS, self.SHUTTER_SPEED_MS, 2), "SetShutterEx(2)")
|
|
@@ -16,4 +16,4 @@ class AreaScanImage:
|
|
|
16
16
|
AreaScanImage.FRAME_COUNT += 1
|
|
17
17
|
|
|
18
18
|
def __repr__(self):
|
|
19
|
-
return f"AreaScanImage<frame {self.frame_count}, line_index {line_index}, width {self.width} (orig {self.width_orig}), height {self.height} (orig {self.height_orig}), format_name {self.format_name}, pathname_png {self.pathname_png}>"
|
|
19
|
+
return f"AreaScanImage<frame {self.frame_count}, line_index {self.line_index}, width {self.width} (orig {self.width_orig}), height {self.height} (orig {self.height_orig}), format_name {self.format_name}, pathname_png {self.pathname_png}>"
|
|
@@ -123,6 +123,8 @@ class EEPROM:
|
|
|
123
123
|
self.sig_laser_tec = False # better: sig_sml_laser_tec
|
|
124
124
|
self.has_interlock_feedback = False
|
|
125
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.laser_timeout_after_count = False
|
|
127
|
+
self.is_oem = False
|
|
126
128
|
self.has_shutter = False
|
|
127
129
|
self.disable_ble_power = False
|
|
128
130
|
self.disable_laser_armed_indicator = False
|
|
@@ -506,6 +508,8 @@ class EEPROM:
|
|
|
506
508
|
self.disable_ble_power = 0 != self.feature_mask & 0x0100
|
|
507
509
|
self.disable_laser_armed_indicator = 0 != self.feature_mask & 0x0200
|
|
508
510
|
self.laser_interlock_excluded = 0 != self.feature_mask & 0x0400
|
|
511
|
+
self.laser_timeout_after_count = 0 != self.feature_mask & 0x0800
|
|
512
|
+
self.is_oem = 0 != self.feature_mask & 0x1000
|
|
509
513
|
else:
|
|
510
514
|
self.invert_x_axis = False
|
|
511
515
|
self.horiz_binning_enabled = False
|
|
@@ -517,6 +521,9 @@ class EEPROM:
|
|
|
517
521
|
self.has_shutter = False
|
|
518
522
|
self.disable_ble_power = False
|
|
519
523
|
self.disable_laser_armed_indicator = False
|
|
524
|
+
self.laser_interlock_excluded = False
|
|
525
|
+
self.laser_timeout_after_count = False
|
|
526
|
+
self.is_oem = False
|
|
520
527
|
|
|
521
528
|
# ######################################################################
|
|
522
529
|
# sanity checks
|
|
@@ -562,6 +569,8 @@ class EEPROM:
|
|
|
562
569
|
mask |= 0x0100 if self.disable_ble_power else 0
|
|
563
570
|
mask |= 0x0200 if self.disable_laser_armed_indicator else 0
|
|
564
571
|
mask |= 0x0400 if self.laser_interlock_excluded else 0
|
|
572
|
+
mask |= 0x0800 if self.laser_timeout_after_count else 0
|
|
573
|
+
mask |= 0x1000 if self.is_oem else 0
|
|
565
574
|
return mask
|
|
566
575
|
|
|
567
576
|
##
|
|
@@ -860,6 +869,8 @@ class EEPROM:
|
|
|
860
869
|
else:
|
|
861
870
|
buf[start_byte + i] = 0
|
|
862
871
|
else:
|
|
872
|
+
if data_type == "f":
|
|
873
|
+
value = float(value)
|
|
863
874
|
struct.pack_into(data_type, buf, start_byte, value)
|
|
864
875
|
|
|
865
876
|
if False:
|
|
@@ -66,11 +66,11 @@ class FeatureIdentificationDevice(InterfaceDevice):
|
|
|
66
66
|
|
|
|
67
67
|
handle_requests
|
|
68
68
|
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
-----------
|
|
70
|
+
| | | | |
|
|
71
71
|
{ get_laser status, acquire, set_laser_watchdog, etc....}
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
| | | | |
|
|
73
|
+
-----------
|
|
74
74
|
|
|
|
75
75
|
_send_code
|
|
76
76
|
@endverbatim
|
|
@@ -185,14 +185,14 @@ class FeatureIdentificationDevice(InterfaceDevice):
|
|
|
185
185
|
# it doesn't know what PIDs it might be looking for. We know, so just
|
|
186
186
|
# narrow down the search to those devices.
|
|
187
187
|
|
|
188
|
-
log.
|
|
189
|
-
log.
|
|
188
|
+
log.debug(f"FID.connect: asked to connect to device_type {self.device_type}")
|
|
189
|
+
log.debug(f"FID.connect: calling device_type.find, looking for VID 0x{self.device_id.vid:04x} and PID 0x{self.device_id.pid:04x}")
|
|
190
190
|
devices = self.device_type.find(find_all=True, idVendor=self.device_id.vid, idProduct=self.device_id.pid)
|
|
191
|
-
log.
|
|
191
|
+
log.debug(f"FID.connect: found devices {devices}")
|
|
192
192
|
dev_list = list(devices) # convert from array
|
|
193
193
|
|
|
194
194
|
device = None
|
|
195
|
-
log.
|
|
195
|
+
log.debug(f"searching for specified device in dev_list {dev_list}")
|
|
196
196
|
for dev in dev_list:
|
|
197
197
|
if dev.bus != self.device_id.bus:
|
|
198
198
|
log.debug("FID.connect: rejecting device (bus %d != requested %d)", dev.bus, self.device_id.bus)
|
|
@@ -475,7 +475,7 @@ class FeatureIdentificationDevice(InterfaceDevice):
|
|
|
475
475
|
|
|
476
476
|
self.connected = False
|
|
477
477
|
|
|
478
|
-
log.
|
|
478
|
+
log.debug("fid.disconnect: releasing interface")
|
|
479
479
|
try:
|
|
480
480
|
#result = self.device_type.release_interface(self.device, 0)
|
|
481
481
|
try:
|
|
@@ -520,7 +520,7 @@ class FeatureIdentificationDevice(InterfaceDevice):
|
|
|
520
520
|
# I had to look at the full configuration string and saw that the way to get the instance id was the following
|
|
521
521
|
# using _try_get_string https://github.com/pyusb/pyusb/blob/master/usb/core.py#L1221
|
|
522
522
|
# so techincally we shouldn't do this by the _ meaning it should be private, but it's the only way I see
|
|
523
|
-
device_instance_id = f'USB
|
|
523
|
+
device_instance_id = f'USB/VID_{self.device_id.vid:04X}&PID_{self.device_id.pid:04X}/{usb.core._try_get_string(pyusb_devices[0], pyusb_devices[0].iSerialNumber)}'
|
|
524
524
|
log.debug(f"In reset and restart trying to reset instance id {device_instance_id}")
|
|
525
525
|
subprocess.run(["pnputil", r"/reboot", r"/disable-device", device_instance_id])
|
|
526
526
|
subprocess.run(["pnputil", r"/reboot", r"/enable-device", device_instance_id])
|
|
@@ -1316,6 +1316,10 @@ class FeatureIdentificationDevice(InterfaceDevice):
|
|
|
1316
1316
|
when NOT in external-triggered mode
|
|
1317
1317
|
@throws exception on timeout (unless external triggering enabled)
|
|
1318
1318
|
"""
|
|
1319
|
+
|
|
1320
|
+
if not self.settings.has_detector():
|
|
1321
|
+
return SpectrometerResponse(keep_alive=True)
|
|
1322
|
+
|
|
1319
1323
|
response = SpectrometerResponse()
|
|
1320
1324
|
|
|
1321
1325
|
if not self.is_sensor_stable():
|
|
@@ -1361,7 +1365,7 @@ class FeatureIdentificationDevice(InterfaceDevice):
|
|
|
1361
1365
|
########################################################################
|
|
1362
1366
|
|
|
1363
1367
|
# this is the number of pixels we expect to read-out over USB
|
|
1364
|
-
if self.settings.is_imx385():
|
|
1368
|
+
if False and self.settings.is_imx385():
|
|
1365
1369
|
# Currently, only the IMX385 uses horizontal binning, so for now
|
|
1366
1370
|
# limit code changes to that detector (even though they haven't been
|
|
1367
1371
|
# shown to break other hardware)
|
|
@@ -1413,13 +1417,14 @@ class FeatureIdentificationDevice(InterfaceDevice):
|
|
|
1413
1417
|
pass
|
|
1414
1418
|
else:
|
|
1415
1419
|
errors += 1
|
|
1416
|
-
log.error(f"Encountered error on read of {exc}", exc_info=1)
|
|
1420
|
+
log.error(f"Encountered error {errors} on read of {exc}", exc_info=1)
|
|
1417
1421
|
|
|
1418
1422
|
# Don't loop on errors on XS, so we can experimentally
|
|
1419
1423
|
# use an XS board as a detector-less laser/TEC driver
|
|
1420
|
-
# board.
|
|
1421
|
-
|
|
1422
|
-
|
|
1424
|
+
# board.
|
|
1425
|
+
if not self.settings.has_detector():
|
|
1426
|
+
pass
|
|
1427
|
+
elif errors < 3:
|
|
1423
1428
|
log.error(f"ignoring error number {errors}")
|
|
1424
1429
|
else:
|
|
1425
1430
|
response.error_msg = "Encountered error on read"
|
|
@@ -1621,7 +1626,7 @@ class FeatureIdentificationDevice(InterfaceDevice):
|
|
|
1621
1626
|
return response
|
|
1622
1627
|
|
|
1623
1628
|
def require_throwaway(self, flag):
|
|
1624
|
-
if flag and self.settings.is_xs() and self.remaining_throwaways < 1:
|
|
1629
|
+
if flag and self.settings.is_xs() and self.remaining_throwaways < 1 and self.settings.has_detector():
|
|
1625
1630
|
log.debug("queuing throwaways")
|
|
1626
1631
|
self.remaining_throwaways += 1
|
|
1627
1632
|
|
|
@@ -1634,6 +1639,10 @@ class FeatureIdentificationDevice(InterfaceDevice):
|
|
|
1634
1639
|
if not self.settings.is_xs():
|
|
1635
1640
|
return SpectrometerResponse(False)
|
|
1636
1641
|
|
|
1642
|
+
# MZ: Kludge, disabling while testing
|
|
1643
|
+
log.debug("disabling internal averaging")
|
|
1644
|
+
return SpectrometerResponse(False)
|
|
1645
|
+
|
|
1637
1646
|
retval = self._send_code(0xff, 0x62, n, label="SET_ONBOARD_SCANS_TO_AVERAGE")
|
|
1638
1647
|
self.settings.state.scans_to_average = n
|
|
1639
1648
|
self.settings.state.onboard_averaging = n > 1
|
|
@@ -2999,13 +3008,13 @@ class FeatureIdentificationDevice(InterfaceDevice):
|
|
|
2999
3008
|
log.debug(f"set_vertical_roi: start {start}, end {end}")
|
|
3000
3009
|
|
|
3001
3010
|
if start < 0 or end < 0:
|
|
3002
|
-
log.
|
|
3011
|
+
log.debug("set_vertical_roi: requires POSITIVE (start, stop) lines")
|
|
3003
3012
|
return SpectrometerResponse(data=False, error_msg="invalid start and stop lines")
|
|
3004
3013
|
|
|
3005
3014
|
# enforce ascending order (also, note that stop line is "last line binned + 1", so stop must be > start)
|
|
3006
3015
|
if start >= end:
|
|
3007
3016
|
# (start, end) = (end, start)
|
|
3008
|
-
log.
|
|
3017
|
+
log.debug("set_vertical_roi: requires ascending order (ignoring %d, %d)", start, end)
|
|
3009
3018
|
return SpectrometerResponse(data=False, error_msg="invalid start and stop lines")
|
|
3010
3019
|
|
|
3011
3020
|
if self.settings.is_xs():
|