wasatch 2.3.6__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.6 → wasatch-2.3.12}/.gitignore +1 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/CHANGELOG.md +23 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/PKG-INFO +1 -1
- {wasatch-2.3.6 → wasatch-2.3.12}/README_PYPI.md +6 -0
- wasatch-2.3.12/demo-ids-raman.py +297 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/pyproject.toml +1 -1
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/AndorDevice.py +39 -32
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/AreaScanImage.py +1 -1
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/EEPROM.py +83 -12
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/FeatureIdentificationDevice.py +35 -26
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/IDSCamera.py +124 -37
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/IDSDevice.py +69 -46
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/Reading.py +1 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/SpectrometerSettings.py +4 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/TCPDevice.py +24 -7
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/WasatchDevice.py +44 -33
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/WasatchDeviceWrapper.py +1 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/WrapperWorker.py +10 -6
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/__init__.py +1 -1
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/applog.py +1 -1
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/utils.py +22 -6
- {wasatch-2.3.6 → wasatch-2.3.12}/.example_pypirc +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/.flake8 +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/.pylintrc +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/Doxyfile +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/LICENSE +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/MANIFEST.in +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/Makefile +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/README.md +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/README_RPI.md +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/README_SETTINGS.md +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/README_VIRTUAL.md +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/WasatchShell/README.md +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/WasatchShell/laser-test.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/WasatchShell/load-test.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/WasatchShell/one-shot.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/WasatchShell/wasatch-shell.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/demo-auto-raman.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/demo-simple.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/demo-virtual.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/demo-workflow.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/demo.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/environments/conda-linux.yml +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/environments/conda-macos.yml +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/environments/conda-rpi.yml +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/environments/conda-win10.yml +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/screenshots/multiplatform.png +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/scripts/dark-vs-integ.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/scripts/deploy +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/testSpectrometers/SiG_785/eeprom/EEPROM-EM-9c65d19f4c.json +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/testSpectrometers/SiG_785/readings/enlighten-20210329-094722-799520-WP-00686.csv +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/testSpectrometers/SiG_785/readings/enlighten-20210329-094755-288958-WP-00686.csv +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/testSpectrometers/WP-00887/eeprom/WP-00887-mock.json +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/testSpectrometers/WP-00904/eeprom/WP-00904-mock.json +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/testSpectrometers/WP-00913/eeprom/WP-00913-mock.json +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/tests/existing_implementations.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/tests/noise-by-setpoint.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/tests/queues.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/tests/unit.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/udev/10-wasatch.rules +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/AbstractUSBDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/AutoRaman.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/AutoRamanRequest.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/BLEDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/BalanceAcquisition.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/CSVLoader.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/CommandSettings.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/ControlObject.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/DetectorROI.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/DetectorRegions.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/DeviceFinderBLE.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/DeviceFinderTCP.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/DeviceFinderUSB.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/DeviceID.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/EEPROM_multical.sav +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/FPGAOptions.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/FirmwareRequirements.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/HardwareInfo.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/IMX385.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/InterfaceDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/MockUSBDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/OceanDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/PollStatus.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/ProcessedReading.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/ROI.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/RealUSBDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/SPIDevice.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/SpectrometerRequest.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/SpectrometerResponse.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/SpectrometerState.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/StatusMessage.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/TakeOneRequest.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/WasatchBus.py +0 -0
- {wasatch-2.3.6 → wasatch-2.3.12}/wasatch/simulation_protocol.py +0 -0
|
@@ -1,5 +1,28 @@
|
|
|
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
|
|
14
|
+
- 2026-01-12 2.3.8
|
|
15
|
+
- added settings.has_detector
|
|
16
|
+
- improved support for Andor .json "virtual EEPROMs"
|
|
17
|
+
- bump internal WrapperWorker poll rate from 20Hz to 200Hz
|
|
18
|
+
- retain debug logging for 20sec after connection (vs previous 10sec)
|
|
19
|
+
- only update Andor detector temperature at 1Hz
|
|
20
|
+
- improved support for using 220250 boards as standalone laser drivers w/o
|
|
21
|
+
detectors (added Reading.keep_alive to indicate metadata but no spectra)
|
|
22
|
+
- 2025-12-10 2.3.7
|
|
23
|
+
- added EEPROM.is_oem
|
|
24
|
+
- fixed TCPDevice.set_detector_gain and set_vertical_roi
|
|
25
|
+
- disable use of eeprom.actual_pixels_horizontal during acquisition
|
|
3
26
|
- 2025-11-06 2.3.6
|
|
4
27
|
- resolve hotplug race condition when laser power set in percent
|
|
5
28
|
- cache certain device getters to avoid I2C overruns
|
|
@@ -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`
|
|
@@ -0,0 +1,297 @@
|
|
|
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
|
+
from PIL import Image, ImageStat # for image normalization
|
|
30
|
+
|
|
31
|
+
import wasatch
|
|
32
|
+
from wasatch import applog
|
|
33
|
+
from wasatch.DeviceID import DeviceID
|
|
34
|
+
from wasatch.IDSDevice import IDSDevice
|
|
35
|
+
from wasatch.WasatchBus import WasatchBus
|
|
36
|
+
from wasatch.WasatchDevice import WasatchDevice
|
|
37
|
+
|
|
38
|
+
log = logging.getLogger(__name__)
|
|
39
|
+
demo = None
|
|
40
|
+
|
|
41
|
+
class WasatchDemo:
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self.camera_device = None # a wasatch.IDSDevice wrapping a wasatch.IDSCamera (as 'camera')
|
|
45
|
+
self.laser_device = None # a wasatch.WasatchDevice wrapping a wasatch.FeatureIdentificationDevice (as 'hardware')
|
|
46
|
+
self.reading_count = 0
|
|
47
|
+
self.exiting = False
|
|
48
|
+
|
|
49
|
+
self.args = self.parse_args()
|
|
50
|
+
|
|
51
|
+
print(f"Wasatch.PY {wasatch.__version__} IDS Raman Demo")
|
|
52
|
+
|
|
53
|
+
def parse_args(self):
|
|
54
|
+
parser = argparse.ArgumentParser(description="Simple demo to acquire spectra from command-line interface")
|
|
55
|
+
parser.add_argument("--log-level", type=str, default="INFO", help="logging level", choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL","NEVER"])
|
|
56
|
+
parser.add_argument("--integration-time-ms", type=int, default=1000, help="integration time (ms, default 10)")
|
|
57
|
+
parser.add_argument("--take-dark", action="store_true", help="first measurement is used for dark subtraction")
|
|
58
|
+
parser.add_argument("--enable-laser", action="store_true", help="fire laser (after optional dark)")
|
|
59
|
+
parser.add_argument("--max", type=int, default=0, help="max spectra to acquire (default 0, unlimited)")
|
|
60
|
+
parser.add_argument("--save-spectra", action="store_true", help="save vertically-binned spectra to row-ordered CSV")
|
|
61
|
+
parser.add_argument("--save-png", action="store_true", help="save PNG of each image frame")
|
|
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")
|
|
64
|
+
parser.add_argument("--save-dir", type=str, default=".", help="directory to save files")
|
|
65
|
+
parser.add_argument("--prefix", type=str, default="ids-raman", help="filename prefix")
|
|
66
|
+
|
|
67
|
+
args = parser.parse_args()
|
|
68
|
+
applog.MainLogger(args.log_level)
|
|
69
|
+
|
|
70
|
+
return args
|
|
71
|
+
|
|
72
|
+
def connect(self):
|
|
73
|
+
|
|
74
|
+
########################################################################
|
|
75
|
+
# Look for laser driver board
|
|
76
|
+
########################################################################
|
|
77
|
+
|
|
78
|
+
bus = WasatchBus()
|
|
79
|
+
if not bus.device_ids:
|
|
80
|
+
print("No Wasatch USB spectrometers found.")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# look for laser driver on WasatchBus -- we won't see the camera here,
|
|
84
|
+
# because IDSPeak devices are not automatically considered for inclusion
|
|
85
|
+
# by WasatchBus (although they could be)
|
|
86
|
+
for device_id in bus.device_ids:
|
|
87
|
+
log.debug("connect: trying to connect to %s", device_id)
|
|
88
|
+
device = WasatchDevice(device_id)
|
|
89
|
+
ok = device.connect()
|
|
90
|
+
if not ok:
|
|
91
|
+
log.critical("connect: can't connect to %s", device_id)
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
log.debug(f"connect: connected to {device_id} ({device})")
|
|
95
|
+
if device.settings is None:
|
|
96
|
+
log.error("can't have device without SpectrometerSettings")
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
print(f"connected to {device.settings.full_model()} {device.settings.eeprom.serial_number} with {device.settings.pixels()} pixels " +
|
|
100
|
+
f"from ({device.settings.wavelengths[0]:.2f}, {device.settings.wavelengths[-1]:.2f}nm)" +
|
|
101
|
+
( f" ({device.settings.wavenumbers[0]:.2f}, {device.settings.wavenumbers[-1]:.2f}cm⁻¹)" if device.settings.has_excitation() else "") +
|
|
102
|
+
f" (Microcontroller {device.settings.microcontroller_firmware_version}," +
|
|
103
|
+
f" FPGA {device.settings.fpga_firmware_version})")
|
|
104
|
+
|
|
105
|
+
self.laser_device = device
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
if self.laser_device is None:
|
|
109
|
+
log.critical("could not find laser driver, exitting")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
########################################################################
|
|
113
|
+
# now look for an IDS camera
|
|
114
|
+
########################################################################
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
device_id = DeviceID(label="IDSPeak")
|
|
118
|
+
device = IDSDevice(device_id=device_id, scratch_dir=".", consumer_deletes_area_scan_image=True)
|
|
119
|
+
if device.connect():
|
|
120
|
+
self.camera_device = device
|
|
121
|
+
else:
|
|
122
|
+
log.error("failed connecting to IDSCamera")
|
|
123
|
+
except:
|
|
124
|
+
log.critical("exception connecting to IDS camera", exc_info=1)
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
if self.camera_device is None:
|
|
128
|
+
log.critical("could not find IDS camera, exitting")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
device = self.camera_device
|
|
132
|
+
camera = device.camera
|
|
133
|
+
settings = device.settings
|
|
134
|
+
eeprom = settings.eeprom
|
|
135
|
+
|
|
136
|
+
device.set_area_scan_enable(True)
|
|
137
|
+
|
|
138
|
+
# copy the laser excitation over to the camera "virtual EEPROM" so we can
|
|
139
|
+
# compute wavenumbers
|
|
140
|
+
eeprom.excitation_nm = self.laser_device.settings.eeprom.excitation_nm
|
|
141
|
+
settings.update_wavecal()
|
|
142
|
+
print(f"connected to IDSPeak camera {eeprom.model} {eeprom.serial_number} with {camera.width}x{camera.height} pixels " +
|
|
143
|
+
f"from ({settings.wavelengths[0]:.2f}, {settings.wavelengths[-1]:.2f}nm)" +
|
|
144
|
+
( f" ({settings.wavenumbers[0]:.2f}, {settings.wavenumbers[-1]:.2f}cm⁻¹)" if settings.has_excitation() else ""))
|
|
145
|
+
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
############################################################################
|
|
149
|
+
# #
|
|
150
|
+
# Run-Time Loop #
|
|
151
|
+
# #
|
|
152
|
+
############################################################################
|
|
153
|
+
|
|
154
|
+
def run(self):
|
|
155
|
+
if self.args.save_dir != ".":
|
|
156
|
+
os.makedirs(self.args.save_dir, exist_ok=True)
|
|
157
|
+
|
|
158
|
+
self.camera_device.set_integration_time_ms(self.args.integration_time_ms)
|
|
159
|
+
|
|
160
|
+
# initialize outfile if saving spectra
|
|
161
|
+
outfile = None
|
|
162
|
+
if self.args.save_spectra:
|
|
163
|
+
filename = os.path.join(self.args.save_dir, self.args.prefix + "-spectra.csv")
|
|
164
|
+
outfile = open(filename, "w")
|
|
165
|
+
outfile.write("pixels, 0, " + ", ".join([f"{x}" for x in range(self.camera_device.settings.pixels())]) + "\n")
|
|
166
|
+
outfile.write("wavelengths, 0, " + ", ".join([f"{x:.2f}" for x in self.camera_device.settings.wavelengths]) + "\n")
|
|
167
|
+
outfile.write("wavenumbers, 0, " + ", ".join([f"{x:.2f}" for x in self.camera_device.settings.wavenumbers]) + "\n")
|
|
168
|
+
outfile.write("time, count, spectrum\n")
|
|
169
|
+
|
|
170
|
+
# read spectra until user presses Control-Break
|
|
171
|
+
while not self.exiting and (self.args.max == 0 or self.reading_count < self.args.max):
|
|
172
|
+
|
|
173
|
+
####################################################################
|
|
174
|
+
# check to fire laser
|
|
175
|
+
####################################################################
|
|
176
|
+
|
|
177
|
+
# on the first reading (or the second, if taking dark), enable the
|
|
178
|
+
# laser if requested
|
|
179
|
+
if (self.args.enable_laser and (self.reading_count == 0 or (self.args.take_dark and self.reading_count == 1))):
|
|
180
|
+
print("enabling laser")
|
|
181
|
+
self.laser_device.hardware.set_laser_enable(True)
|
|
182
|
+
|
|
183
|
+
####################################################################
|
|
184
|
+
# read image
|
|
185
|
+
####################################################################
|
|
186
|
+
|
|
187
|
+
if self.exiting:
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
response = self.camera_device.acquire_data()
|
|
191
|
+
if response.keep_alive:
|
|
192
|
+
log.debug(f"ignoring keepalive from {device}")
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
if isinstance(response.data, bool):
|
|
196
|
+
if response.data:
|
|
197
|
+
log.debug("received poison-pill, exiting")
|
|
198
|
+
break
|
|
199
|
+
else:
|
|
200
|
+
log.debug("no reading available")
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
if response.data.failure:
|
|
204
|
+
log.debug("reading indicated failure")
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
if self.exiting:
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
self.reading_count += 1
|
|
211
|
+
|
|
212
|
+
reading = response.data
|
|
213
|
+
spectrum = reading.spectrum
|
|
214
|
+
settings = self.camera_device.settings
|
|
215
|
+
asi = reading.area_scan_image
|
|
216
|
+
ts = reading.timestamp.strftime("%Y%m%d-%H%M%S-%f")
|
|
217
|
+
|
|
218
|
+
# print some stats as we go
|
|
219
|
+
spectrum_min = np.amin(spectrum)
|
|
220
|
+
spectrum_max = np.amax(spectrum)
|
|
221
|
+
spectrum_avg = np.mean(spectrum)
|
|
222
|
+
spectrum_std = np.std (spectrum)
|
|
223
|
+
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}")
|
|
224
|
+
|
|
225
|
+
# append to row-ordered file
|
|
226
|
+
if outfile:
|
|
227
|
+
values = ",".join(format(x, ".2f") for x in spectrum)
|
|
228
|
+
outfile.write(f"{reading.timestamp}, {self.reading_count}, {values}\n")
|
|
229
|
+
|
|
230
|
+
# save PNG
|
|
231
|
+
if self.args.save_png and asi.pathname_png:
|
|
232
|
+
filename = os.path.join(self.args.save_dir, f"{self.args.prefix}-{self.reading_count:04d}.png")
|
|
233
|
+
log.debug(f"renaming {asi.pathname_png} -> {filename}")
|
|
234
|
+
os.replace(asi.pathname_png, filename)
|
|
235
|
+
print(f"\tsaved {filename}")
|
|
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
|
+
|
|
248
|
+
# save area scan data
|
|
249
|
+
if self.args.save_data and asi.data is not None:
|
|
250
|
+
filename = os.path.join(self.args.save_dir, f"{self.args.prefix}-{self.reading_count:04d}.csv")
|
|
251
|
+
np.savetxt(filename, asi.data, fmt='%d', delimiter=",")
|
|
252
|
+
print(f"\tsaved {filename}")
|
|
253
|
+
|
|
254
|
+
if self.args.take_dark and self.reading_count == 1:
|
|
255
|
+
print("\tsaving dark")
|
|
256
|
+
self.camera_device.camera.set_dark_asi(asi)
|
|
257
|
+
|
|
258
|
+
################################################################################
|
|
259
|
+
# main()
|
|
260
|
+
################################################################################
|
|
261
|
+
|
|
262
|
+
def signal_handler(signal, frame):
|
|
263
|
+
print('\rInterrupted by Ctrl-C...shutting down', end=' ')
|
|
264
|
+
shutdown()
|
|
265
|
+
|
|
266
|
+
def shutdown():
|
|
267
|
+
if demo:
|
|
268
|
+
demo.exiting = True
|
|
269
|
+
if demo.laser_device is not None:
|
|
270
|
+
demo.laser_device.hardware.set_laser_enable(False)
|
|
271
|
+
demo.laser_device.disconnect()
|
|
272
|
+
|
|
273
|
+
if demo.camera_device is not None:
|
|
274
|
+
demo.camera_device.disconnect()
|
|
275
|
+
sleep(1)
|
|
276
|
+
|
|
277
|
+
log.debug(None)
|
|
278
|
+
applog.explicit_log_close()
|
|
279
|
+
sys.exit()
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
# add a signal-handler to catch ctrl-C interrupts, so we can cleanly close
|
|
283
|
+
# down all connected devices
|
|
284
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
285
|
+
|
|
286
|
+
demo = WasatchDemo()
|
|
287
|
+
if demo.connect():
|
|
288
|
+
# Note that on Windows, Control-Break (SIGBREAK) differs from
|
|
289
|
+
# Control-C (SIGINT); see https://stackoverflow.com/a/1364199
|
|
290
|
+
log.debug("Press Control-Break to interrupt...")
|
|
291
|
+
try:
|
|
292
|
+
demo.run()
|
|
293
|
+
except:
|
|
294
|
+
# catch exception so shutdown will run and disable laser
|
|
295
|
+
log.error("Caught exception during demo", exc_info=1)
|
|
296
|
+
|
|
297
|
+
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}>"
|