wasatch 2.1.64__tar.gz → 2.2.0__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.
Files changed (85) hide show
  1. {wasatch-2.1.64 → wasatch-2.2.0}/CHANGELOG.md +23 -0
  2. {wasatch-2.1.64 → wasatch-2.2.0}/PKG-INFO +8 -7
  3. {wasatch-2.1.64 → wasatch-2.2.0}/README.md +7 -6
  4. {wasatch-2.1.64 → wasatch-2.2.0}/demo.py +80 -55
  5. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/AndorDevice.py +2 -0
  6. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/EEPROM.py +35 -32
  7. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/FeatureIdentificationDevice.py +14 -5
  8. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/TakeOneRequest.py +13 -6
  9. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/WasatchDevice.py +9 -6
  10. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/WasatchDeviceWrapper.py +21 -14
  11. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/WrapperWorker.py +30 -3
  12. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/__init__.py +1 -1
  13. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/applog.py +1 -1
  14. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/utils.py +1 -1
  15. {wasatch-2.1.64 → wasatch-2.2.0}/.example_pypirc +0 -0
  16. {wasatch-2.1.64 → wasatch-2.2.0}/.flake8 +0 -0
  17. {wasatch-2.1.64 → wasatch-2.2.0}/.gitignore +0 -0
  18. {wasatch-2.1.64 → wasatch-2.2.0}/.pylintrc +0 -0
  19. {wasatch-2.1.64 → wasatch-2.2.0}/Doxyfile +0 -0
  20. {wasatch-2.1.64 → wasatch-2.2.0}/LICENSE +0 -0
  21. {wasatch-2.1.64 → wasatch-2.2.0}/MANIFEST.in +0 -0
  22. {wasatch-2.1.64 → wasatch-2.2.0}/Makefile +0 -0
  23. {wasatch-2.1.64 → wasatch-2.2.0}/README_PYPI.md +0 -0
  24. {wasatch-2.1.64 → wasatch-2.2.0}/README_RPI.md +0 -0
  25. {wasatch-2.1.64 → wasatch-2.2.0}/README_SETTINGS.md +0 -0
  26. {wasatch-2.1.64 → wasatch-2.2.0}/README_VIRTUAL.md +0 -0
  27. {wasatch-2.1.64 → wasatch-2.2.0}/WasatchShell/README.md +0 -0
  28. {wasatch-2.1.64 → wasatch-2.2.0}/WasatchShell/laser-test.py +0 -0
  29. {wasatch-2.1.64 → wasatch-2.2.0}/WasatchShell/load-test.py +0 -0
  30. {wasatch-2.1.64 → wasatch-2.2.0}/WasatchShell/one-shot.py +0 -0
  31. {wasatch-2.1.64 → wasatch-2.2.0}/WasatchShell/wasatch-shell.py +0 -0
  32. {wasatch-2.1.64 → wasatch-2.2.0}/demo-simple.py +0 -0
  33. {wasatch-2.1.64 → wasatch-2.2.0}/demo-virtual.py +0 -0
  34. {wasatch-2.1.64 → wasatch-2.2.0}/demo-workflow.py +0 -0
  35. {wasatch-2.1.64 → wasatch-2.2.0}/environments/conda-linux.yml +0 -0
  36. {wasatch-2.1.64 → wasatch-2.2.0}/environments/conda-macos.yml +0 -0
  37. {wasatch-2.1.64 → wasatch-2.2.0}/environments/conda-rpi.yml +0 -0
  38. {wasatch-2.1.64 → wasatch-2.2.0}/environments/conda-win10.yml +0 -0
  39. {wasatch-2.1.64 → wasatch-2.2.0}/pyproject.toml +0 -0
  40. {wasatch-2.1.64 → wasatch-2.2.0}/requirements.txt +0 -0
  41. {wasatch-2.1.64 → wasatch-2.2.0}/screenshots/multiplatform.png +0 -0
  42. {wasatch-2.1.64 → wasatch-2.2.0}/scripts/dark-vs-integ.py +0 -0
  43. {wasatch-2.1.64 → wasatch-2.2.0}/scripts/deploy +0 -0
  44. {wasatch-2.1.64 → wasatch-2.2.0}/setup.cfg +0 -0
  45. {wasatch-2.1.64 → wasatch-2.2.0}/testSpectrometers/SiG_785/eeprom/EEPROM-EM-9c65d19f4c.json +0 -0
  46. {wasatch-2.1.64 → wasatch-2.2.0}/testSpectrometers/SiG_785/readings/enlighten-20210329-094722-799520-WP-00686.csv +0 -0
  47. {wasatch-2.1.64 → wasatch-2.2.0}/testSpectrometers/SiG_785/readings/enlighten-20210329-094755-288958-WP-00686.csv +0 -0
  48. {wasatch-2.1.64 → wasatch-2.2.0}/testSpectrometers/WP-00887/eeprom/WP-00887-mock.json +0 -0
  49. {wasatch-2.1.64 → wasatch-2.2.0}/testSpectrometers/WP-00904/eeprom/WP-00904-mock.json +0 -0
  50. {wasatch-2.1.64 → wasatch-2.2.0}/testSpectrometers/WP-00913/eeprom/WP-00913-mock.json +0 -0
  51. {wasatch-2.1.64 → wasatch-2.2.0}/tests/existing_implementations.py +0 -0
  52. {wasatch-2.1.64 → wasatch-2.2.0}/tests/noise-by-setpoint.py +0 -0
  53. {wasatch-2.1.64 → wasatch-2.2.0}/tests/queues.py +0 -0
  54. {wasatch-2.1.64 → wasatch-2.2.0}/tests/unit.py +0 -0
  55. {wasatch-2.1.64 → wasatch-2.2.0}/udev/10-wasatch.rules +0 -0
  56. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/AbstractUSBDevice.py +0 -0
  57. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/BLEDevice.py +0 -0
  58. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/BalanceAcquisition.py +0 -0
  59. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/CSVLoader.py +0 -0
  60. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/CommandSettings.py +0 -0
  61. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/ControlObject.py +0 -0
  62. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/DetectorROI.py +0 -0
  63. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/DetectorRegions.py +0 -0
  64. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/DeviceFinderUSB.py +0 -0
  65. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/DeviceID.py +0 -0
  66. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/EEPROM_multical.sav +0 -0
  67. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/FPGAOptions.py +0 -0
  68. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/FirmwareRequirements.py +0 -0
  69. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/HardwareInfo.py +0 -0
  70. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/InterfaceDevice.py +0 -0
  71. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/MockUSBDevice.py +0 -0
  72. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/OceanDevice.py +0 -0
  73. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/PollStatus.py +0 -0
  74. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/ProcessedReading.py +0 -0
  75. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/ROI.py +0 -0
  76. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/Reading.py +0 -0
  77. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/RealUSBDevice.py +0 -0
  78. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/SPIDevice.py +0 -0
  79. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/SpectrometerRequest.py +0 -0
  80. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/SpectrometerResponse.py +0 -0
  81. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/SpectrometerSettings.py +0 -0
  82. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/SpectrometerState.py +0 -0
  83. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/StatusMessage.py +0 -0
  84. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/WasatchBus.py +0 -0
  85. {wasatch-2.1.64 → wasatch-2.2.0}/wasatch/simulation_protocol.py +0 -0
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ - 2024-08-22 2.2.0
4
+ - cutting release for ENLIGHTEN 4.1.0
5
+ - 2024-06-27 2.1.73
6
+ - update demo.py to support multiple spectrometers
7
+ - add optional `callback` parameter to WrapperWorker for Inversion of Control (IoC)
8
+ - 2024-06-27 2.1.72
9
+ - temporarily disable GET_LASER_ENABLED on XS
10
+ - 2024-06-19 2.1.71
11
+ - disable applog to stdout if stdout missing
12
+ - 2024-06-12 2.1.70
13
+ - force laser to disable on perc/PWM zero
14
+ - 2024-05-30 2.1.69
15
+ - added avg_resolution to edittable EEPROM fields
16
+ - 2024-05-24 2.1.68
17
+ - added horizontal ROI to XL virtual EEPROM
18
+ - Raspberry Pi testing (no change)
19
+ - 2024-05-22 2.1.67
20
+ - relax thread logging 10sec after successful connection
21
+ - display connected spectrometer properties in demo.py
22
+ - 2024-05-07 2.1.66
23
+ - gave TakeOneRequest a copy-ctor
24
+ - 2024-05-02 2.1.65
25
+ - updated README links to work on pypi servers
3
26
  - 2024-05-02 2.1.64
4
27
  - added wasatch.FirmwareRequirements
5
28
  - added wasatch.PollStatus
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wasatch
3
- Version: 2.1.64
3
+ Version: 2.2.0
4
4
  Summary: This package is a driver for Wasatch Photonics spectrometers
5
5
  Author-email: Wasatch Photonics <edort@wasatchphotonics.com>
6
6
  Description-Content-Type: text/markdown
@@ -49,7 +49,7 @@ Rendered API documentation for classes and methods is available here:
49
49
  A significant portion of Wasatch.PY's command interface is exposed through key-value
50
50
  settings, originally provided to simplify multi-process applications and summarized here:
51
51
 
52
- - [Key-Value Settings](README_SETTINGS.md)
52
+ - [Key-Value Settings](https://github.com/WasatchPhotonics/Wasatch.PY/blob/master/README_SETTINGS.md)
53
53
 
54
54
  # PyPi
55
55
 
@@ -63,13 +63,14 @@ Package info:
63
63
 
64
64
  - https://pypi.org/project/wasatch/
65
65
 
66
- For more information about PyPi package management, see [README_PYPI](README_PYPI.md).
66
+ For more information about PyPi package management, see [README_PYPI](https://github.com/WasatchPhotonics/Wasatch.PY/blob/master/README_PYPI.md).
67
67
 
68
68
  # Dependencies
69
69
 
70
- Wasatch.PY normally uses the Python 3.x build of [Miniconda](https://conda.io/miniconda.html)
71
- for dependencies and package management. It should work under either 32-bit or 64-bit
72
- Python.
70
+ Wasatch.PY used to use the Python 3.x build of [Miniconda](https://conda.io/miniconda.html)
71
+ for dependencies and package management. Increasingly however, we just use pip:
72
+
73
+ $ pip install -r requirements.txt
73
74
 
74
75
  # Running the Demo
75
76
 
@@ -197,7 +198,7 @@ and [Miniconda](https://conda.io/miniconda.html) (Python 3.x):
197
198
 
198
199
  A standalone Raspberry Pi "Quick Start" tutorial is available here:
199
200
 
200
- - [README RPI](README_RPI.md)
201
+ - [README RPI](https://github.com/WasatchPhotonics/Wasatch.PY/blob/master/README_RPI.md)
201
202
 
202
203
  ## Linux
203
204
 
@@ -40,7 +40,7 @@ Rendered API documentation for classes and methods is available here:
40
40
  A significant portion of Wasatch.PY's command interface is exposed through key-value
41
41
  settings, originally provided to simplify multi-process applications and summarized here:
42
42
 
43
- - [Key-Value Settings](README_SETTINGS.md)
43
+ - [Key-Value Settings](https://github.com/WasatchPhotonics/Wasatch.PY/blob/master/README_SETTINGS.md)
44
44
 
45
45
  # PyPi
46
46
 
@@ -54,13 +54,14 @@ Package info:
54
54
 
55
55
  - https://pypi.org/project/wasatch/
56
56
 
57
- For more information about PyPi package management, see [README_PYPI](README_PYPI.md).
57
+ For more information about PyPi package management, see [README_PYPI](https://github.com/WasatchPhotonics/Wasatch.PY/blob/master/README_PYPI.md).
58
58
 
59
59
  # Dependencies
60
60
 
61
- Wasatch.PY normally uses the Python 3.x build of [Miniconda](https://conda.io/miniconda.html)
62
- for dependencies and package management. It should work under either 32-bit or 64-bit
63
- Python.
61
+ Wasatch.PY used to use the Python 3.x build of [Miniconda](https://conda.io/miniconda.html)
62
+ for dependencies and package management. Increasingly however, we just use pip:
63
+
64
+ $ pip install -r requirements.txt
64
65
 
65
66
  # Running the Demo
66
67
 
@@ -188,7 +189,7 @@ and [Miniconda](https://conda.io/miniconda.html) (Python 3.x):
188
189
 
189
190
  A standalone Raspberry Pi "Quick Start" tutorial is available here:
190
191
 
191
- - [README RPI](README_RPI.md)
192
+ - [README RPI](https://github.com/WasatchPhotonics/Wasatch.PY/blob/master/README_RPI.md)
192
193
 
193
194
  ## Linux
194
195
 
@@ -31,7 +31,6 @@ import wasatch
31
31
  from wasatch import utils
32
32
  from wasatch import applog
33
33
  from wasatch.WasatchBus import WasatchBus
34
- from wasatch.OceanDevice import OceanDevice
35
34
  from wasatch.WasatchDevice import WasatchDevice
36
35
  from wasatch.WasatchDeviceWrapper import WasatchDeviceWrapper
37
36
  from wasatch.RealUSBDevice import RealUSBDevice
@@ -48,14 +47,16 @@ class WasatchDemo:
48
47
 
49
48
  def __init__(self, argv=None):
50
49
  self.bus = None
51
- self.device = None
50
+ self.devices = {}
52
51
  self.logger = None
53
52
  self.outfile = None
54
53
  self.exiting = False
54
+ self.reading_count = 0
55
55
 
56
56
  self.args = self.parse_args(argv)
57
57
 
58
- self.logger = applog.MainLogger(self.args.log_level)
58
+ if self.args.log_level != "NEVER":
59
+ self.logger = applog.MainLogger(self.args.log_level)
59
60
  log.info("Wasatch.PY version %s", wasatch.__version__)
60
61
 
61
62
  ############################################################################
@@ -66,7 +67,7 @@ class WasatchDemo:
66
67
 
67
68
  def parse_args(self, argv):
68
69
  parser = argparse.ArgumentParser(description="Simple demo to acquire spectra from command-line interface")
69
- parser.add_argument("--log-level", type=str, default="INFO", help="logging level [DEBUG,INFO,WARNING,ERROR,CRITICAL]")
70
+ parser.add_argument("--log-level", type=str, default="INFO", help="logging level", choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL","NEVER"])
70
71
  parser.add_argument("--integration-time-ms", type=int, default=10, help="integration time (ms, default 10)")
71
72
  parser.add_argument("--scans-to-average", type=int, default=1, help="scans to average (default 1)")
72
73
  parser.add_argument("--boxcar-half-width", type=int, default=0, help="boxcar half-width (default 0)")
@@ -83,11 +84,7 @@ class WasatchDemo:
83
84
  print("Wasatch.PY %s" % wasatch.__version__)
84
85
  sys.exit(0)
85
86
 
86
- # normalize log level
87
87
  args.log_level = args.log_level.upper()
88
- if not re.match("^(DEBUG|INFO|ERROR|WARNING|CRITICAL)$", args.log_level):
89
- print("Invalid log level: %s (defaulting to INFO)" % args.log_level)
90
- args.log_level = "INFO"
91
88
 
92
89
  return args
93
90
 
@@ -98,12 +95,7 @@ class WasatchDemo:
98
95
  ############################################################################
99
96
 
100
97
  def connect(self):
101
- """ If the current device is disconnected, and there is a new device,
102
- attempt to connect to it. """
103
-
104
- # if we're already connected, nevermind
105
- if self.device is not None:
106
- return
98
+ """ Connect to all discoverable Wasatch spectrometers. """
107
99
 
108
100
  # lazy-load a USB bus
109
101
  if self.bus is None:
@@ -114,35 +106,43 @@ class WasatchDemo:
114
106
  print("No Wasatch USB spectrometers found.")
115
107
  return
116
108
 
117
- device_id = self.bus.device_ids[0]
118
- log.debug("connect: trying to connect to %s", device_id)
119
- device_id.device_type = RealUSBDevice(device_id)
120
-
121
- if self.args.non_blocking:
122
- # this is still buggy on MacOS
123
- log.debug("instantiating WasatchDeviceWrapper (non-blocking)")
124
- device = WasatchDeviceWrapper(
125
- device_id = device_id,
126
- log_queue = self.logger.log_queue,
127
- log_level = self.args.log_level)
128
- else:
129
- log.debug("instantiating WasatchDevice (blocking)")
130
- if device_id.vid == 0x24aa:
131
- device = WasatchDevice(device_id)
109
+ for device_id in self.bus.device_ids:
110
+ log.debug("connect: trying to connect to %s", device_id)
111
+
112
+ if self.args.non_blocking:
113
+ # Non-blocking means we instantiate a WasatchDeviceWrapper (WDW)
114
+ # The WDW incapsulates its own threading.Thread (WrapperWorker)
115
+ # which continuously acquires spectra free-running mode.
116
+ log.debug("instantiating WasatchDeviceWrapper (non-blocking)")
117
+ device = WasatchDeviceWrapper(
118
+ device_id = device_id,
119
+ log_level = self.args.log_level,
120
+ callback = self.WrapperWorker_callback)
132
121
  else:
133
- device = OceanDevice(device_id)
122
+ # We're going to place blocking calls to spectrometers, so we can
123
+ # instantiate a plain WasatchDevice (no need for WDW or WrapperWorker)
124
+ log.debug("instantiating WasatchDevice (blocking)")
125
+ device = WasatchDevice(device_id)
134
126
 
135
- ok = device.connect()
136
- if not ok:
137
- log.critical("connect: can't connect to %s", device_id)
138
- return
127
+ ok = device.connect()
128
+ if not ok:
129
+ log.critical("connect: can't connect to %s", device_id)
130
+ continue
139
131
 
140
- log.debug("connect: device connected")
132
+ log.debug("connect: device connected")
133
+ if device.settings is None:
134
+ log.error("can't have device without SpectrometerSettings")
135
+ continue
141
136
 
142
- self.device = device
143
- self.reading_count = 0
137
+ print(f"connected to {device.settings.full_model()} {device.settings.eeprom.serial_number} with {device.settings.pixels()} pixels " +
138
+ f"from ({device.settings.wavelengths[0]:.2f}, {device.settings.wavelengths[-1]:.2f}nm)" +
139
+ ( f" ({device.settings.wavenumbers[0]:.2f}, {device.settings.wavenumbers[-1]:.2f}cm⁻¹)" if device.settings.has_excitation() else "") +
140
+ f" (Microcontroller {device.settings.microcontroller_firmware_version}," +
141
+ f" FPGA {device.settings.fpga_firmware_version})")
144
142
 
145
- return device
143
+ self.devices[str(device.device_id)] = device
144
+
145
+ return len(self.devices) > 0
146
146
 
147
147
  ############################################################################
148
148
  # #
@@ -154,15 +154,15 @@ class WasatchDemo:
154
154
  log.info("Wasatch.PY %s Demo", wasatch.__version__)
155
155
 
156
156
  # apply initial settings
157
- self.device.change_setting("integration_time_ms", self.args.integration_time_ms)
158
- self.device.change_setting("scans_to_average", self.args.scans_to_average)
159
- self.device.change_setting("detector_tec_enable", True)
157
+ for device_id, device in self.devices.items():
158
+ device.change_setting("integration_time_ms", self.args.integration_time_ms)
159
+ device.change_setting("scans_to_average", self.args.scans_to_average)
160
160
 
161
161
  # initialize outfile if one was specified
162
162
  if self.args.outfile:
163
163
  try:
164
164
  self.outfile = open(self.args.outfile, "w")
165
- self.outfile.write("time,temp,%s\n" % ",".join(format(x, ".2f") for x in self.device.settings.wavelengths))
165
+ self.outfile.write("time,temp,serial,spectrum\n")
166
166
  except:
167
167
  log.error("Error initializing %s", self.args.outfile)
168
168
  self.outfile = None
@@ -170,7 +170,9 @@ class WasatchDemo:
170
170
  # read spectra until user presses Control-Break
171
171
  while not self.exiting:
172
172
  start_time = datetime.datetime.now()
173
- self.attempt_reading()
173
+ if not self.args.non_blocking:
174
+ for device_id, device in self.devices.items():
175
+ self.attempt_reading(device)
174
176
  end_time = datetime.datetime.now()
175
177
 
176
178
  if self.args.max > 0 and self.reading_count >= self.args.max:
@@ -185,14 +187,13 @@ class WasatchDemo:
185
187
  try:
186
188
  time.sleep(float(sleep_ms) / 1000)
187
189
  except:
188
- # log.critical("WasatchDemo.run sleep() caught an exception", exc_info=1)
189
190
  self.exiting = True
190
191
 
191
192
  log.debug("WasatchDemo.run exiting")
192
193
 
193
- def attempt_reading(self):
194
+ def attempt_reading(self, device):
194
195
  try:
195
- reading_response = self.acquire_reading()
196
+ reading_response = self.acquire_reading(device)
196
197
  except:
197
198
  log.critical("attempt_reading caught exception", exc_info=1)
198
199
  self.exiting = True
@@ -213,23 +214,42 @@ class WasatchDemo:
213
214
 
214
215
  self.process_reading(reading_response.data)
215
216
 
216
- def acquire_reading(self):
217
+ def acquire_reading(self, device):
217
218
  # We want the demo to effectively block on new scans, so keep
218
219
  # polling the subprocess until a reading is ready. In other apps,
219
220
  # we could do other things (like respond to GUI events) if
220
221
  # device.acquire_data() was None (meaning the next spectrum wasn't
221
222
  # yet ready).
222
223
  while True:
223
- reading = self.device.acquire_data()
224
+ reading = device.acquire_data()
224
225
  if reading is None:
225
226
  log.debug("waiting on next reading")
226
227
  else:
227
228
  return reading
228
229
 
230
+ def WrapperWorker_callback(self, response):
231
+ if response is None:
232
+ log.error("WrapperWorker_callback received null SpectrometerResponse")
233
+ return
234
+
235
+ reading = response.data
236
+ if reading is None:
237
+ log.error("WrapperWorker_callback received null Reading")
238
+ return
239
+
240
+ self.process_reading(reading)
241
+
229
242
  def process_reading(self, reading):
230
- if self.args.scans_to_average > 1 and not reading.averaged:
243
+ if (self.exiting or
244
+ (self.args.scans_to_average > 1 and not reading.averaged) or
245
+ (self.args.max > 0 and self.reading_count >= self.args.max)):
231
246
  return
232
247
 
248
+ device_id = reading.device_id
249
+ log.debug(f"received reading from device_id {device_id}")
250
+ device = self.devices[str(reading.device_id)]
251
+ settings = device.settings
252
+
233
253
  self.reading_count += 1
234
254
 
235
255
  if self.args.boxcar_half_width > 0:
@@ -238,7 +258,7 @@ class WasatchDemo:
238
258
  spectrum = reading.spectrum
239
259
 
240
260
  if self.args.ascii_art:
241
- print("\n".join(wasatch.utils.ascii_spectrum(spectrum, rows=20, cols=80, x_axis=self.device.settings.wavelengths, x_unit="nm")))
261
+ print("\n".join(wasatch.utils.ascii_spectrum(spectrum, rows=20, cols=80, x_axis=settings.wavelengths, x_unit="nm")))
242
262
  else:
243
263
  spectrum_min = numpy.amin(spectrum)
244
264
  spectrum_max = numpy.amax(spectrum)
@@ -246,7 +266,9 @@ class WasatchDemo:
246
266
  spectrum_std = numpy.std (spectrum)
247
267
  size_in_bytes = psutil.Process(os.getpid()).memory_info().rss
248
268
 
249
- print("Reading: %4d Detector: %5.2f degC Min: %8.2f Max: %8.2f Avg: %8.2f StdDev: %8.2f Memory: %11d" % (
269
+ print("%s: %s %4d Detector: %5.2f degC Min: %8.2f Max: %8.2f Avg: %8.2f StdDev: %8.2f Memory: %11d" % (
270
+ reading.timestamp,
271
+ settings.eeprom.serial_number,
250
272
  self.reading_count,
251
273
  reading.detector_temperature_degC,
252
274
  spectrum_min,
@@ -257,8 +279,9 @@ class WasatchDemo:
257
279
  log.debug("%s", str(reading))
258
280
 
259
281
  if self.outfile:
260
- self.outfile.write("%s,%.2f,%s\n" % (datetime.datetime.now(),
282
+ self.outfile.write("%s,%.2f,%s,%s\n" % (datetime.datetime.now(),
261
283
  reading.detector_temperature_degC,
284
+ settings.eeprom.serial_number,
262
285
  ",".join(format(x, ".2f") for x in spectrum)))
263
286
 
264
287
  ################################################################################
@@ -272,9 +295,11 @@ def signal_handler(signal, frame):
272
295
  def clean_shutdown():
273
296
  log.debug("Exiting")
274
297
  if demo:
275
- if demo.args and demo.args.non_blocking and demo.device:
276
- log.debug("closing background thread")
277
- demo.device.disconnect()
298
+ if demo.args.non_blocking and len(demo.devices):
299
+ for device_id, device in demo.devices.items():
300
+ log.debug(f"closing {device_id}")
301
+ device.disconnect()
302
+ time.sleep(1)
278
303
 
279
304
  if demo.logger:
280
305
  log.debug("closing logger")
@@ -472,6 +472,8 @@ class AndorDevice(InterfaceDevice):
472
472
  'invert_x_axis',
473
473
  'wavelength_coeffs',
474
474
  'excitation_nm_float',
475
+ 'roi_horizontal_end',
476
+ 'roi_horizontal_start',
475
477
  'raman_intensity_coeffs',
476
478
  'raman_intensity_calibration_order',
477
479
  'startup_temp_degC',
@@ -113,37 +113,40 @@ class EEPROM:
113
113
  self.write_buffers = []
114
114
  self.digest = None
115
115
 
116
- self.editable = [ "excitation_nm",
117
- "excitation_nm_float",
118
- "detector_gain",
119
- "detector_offset",
120
- "detector_gain_odd",
121
- "detector_offset_odd",
122
- "calibrated_by",
123
- "calibration_date",
124
- "user_text",
125
- "wavelength_coeffs",
126
- "linearity_coeffs",
127
- "max_laser_power_mW",
128
- "min_laser_power_mW",
129
- "laser_power_coeffs",
130
- "bad_pixels",
131
- "bin_2x2",
132
- "gen15",
133
- "cutoff_filter_installed",
134
- "has_shutter",
135
- "laser_warmup_sec",
136
- "laser_watchdog_sec",
137
- "roi_horizontal_end",
138
- "roi_horizontal_start",
139
- "roi_vertical_region_1_end",
140
- "roi_vertical_region_1_start",
141
- "roi_vertical_region_2_end",
142
- "roi_vertical_region_2_start",
143
- "roi_vertical_region_3_end",
144
- "roi_vertical_region_3_start",
145
- "raman_intensity_calibration_order",
146
- "raman_intensity_coeffs" ]
116
+ self.editable = [
117
+ "avg_resolution",
118
+ "bad_pixels",
119
+ "bin_2x2",
120
+ "calibrated_by",
121
+ "calibration_date",
122
+ "cutoff_filter_installed",
123
+ "detector_gain",
124
+ "detector_gain_odd",
125
+ "detector_offset",
126
+ "detector_offset_odd",
127
+ "excitation_nm",
128
+ "excitation_nm_float",
129
+ "gen15",
130
+ "has_shutter",
131
+ "laser_power_coeffs",
132
+ "laser_warmup_sec",
133
+ "laser_watchdog_sec",
134
+ "linearity_coeffs",
135
+ "max_laser_power_mW",
136
+ "min_laser_power_mW",
137
+ "raman_intensity_calibration_order",
138
+ "raman_intensity_coeffs",
139
+ "roi_horizontal_end",
140
+ "roi_horizontal_start",
141
+ "roi_vertical_region_1_end",
142
+ "roi_vertical_region_1_start",
143
+ "roi_vertical_region_2_end",
144
+ "roi_vertical_region_2_start",
145
+ "roi_vertical_region_3_end",
146
+ "roi_vertical_region_3_start",
147
+ "user_text",
148
+ "wavelength_coeffs",
149
+ ]
147
150
 
148
151
  self.init_raman_intensity_calibration()
149
152
  self.init_spline()
@@ -738,7 +741,7 @@ class EEPROM:
738
741
  else:
739
742
  struct.pack_into(data_type, buf, start_byte, value)
740
743
 
741
- if True:
744
+ if False:
742
745
  extra = "" if label is None else (" (%s)" % label)
743
746
  log.debug("Packed (%d, %2d, %2d) '%s' value %s -> %s%s",
744
747
  page, start_byte, length, data_type, value, buf[start_byte:end_byte], extra)
@@ -123,7 +123,6 @@ class FeatureIdentificationDevice(InterfaceDevice):
123
123
  self.shutdown_requested = False
124
124
 
125
125
  self.last_spectrum = None
126
- self.spectrum_count = 0
127
126
  self.prev_pixels = None
128
127
 
129
128
  # in case of I2C collisions within the spectrometer, e.g. due to battery-LED status
@@ -201,6 +200,8 @@ class FeatureIdentificationDevice(InterfaceDevice):
201
200
  log.debug("on Windows, so NOT setting configuration and claiming interface")
202
201
  elif "macOS" in platform.platform():
203
202
  log.debug("on MacOS, so NOT setting configuration and claiming interface")
203
+ # elif "raspberrypi" in str(os.uname()):
204
+ # log.debug("on Raspberry Pi, so NOT setting configuration and claiming interface")
204
205
  else:
205
206
  log.debug("on posix, so setting configuration and claiming interface")
206
207
 
@@ -1961,13 +1962,13 @@ class FeatureIdentificationDevice(InterfaceDevice):
1961
1962
  return SpectrometerResponse(data=self.settings.state.selected_laser)
1962
1963
 
1963
1964
  def get_laser_enabled(self):
1964
- res = self._get_code(0xe2, label="GET_LASER_ENABLED", msb_len=1)
1965
- flag = 0 != res.data
1966
-
1967
- if self.device_id.pid == 0x4000:
1965
+ if self.settings.is_xs():
1968
1966
  # MZ: get_laser_enabled not currently robust on SiG :-(
1969
1967
  return SpectrometerResponse(data=self.settings.state.laser_enabled)
1970
1968
 
1969
+ res = self._get_code(0xe2, label="GET_LASER_ENABLED", msb_len=1)
1970
+ flag = 0 != res.data
1971
+
1971
1972
  log.debug(f"get_laser_enabled: {flag} (storing to {self.settings.state})")
1972
1973
  self.settings.state.laser_enabled = flag
1973
1974
  return SpectrometerResponse(data=flag)
@@ -2198,6 +2199,8 @@ class FeatureIdentificationDevice(InterfaceDevice):
2198
2199
 
2199
2200
  # don't want anything weird when passing over USB
2200
2201
  value = float(max(0, min(100, value)))
2202
+ if value <= 0:
2203
+ return set_laser_enable(False)
2201
2204
 
2202
2205
  # If full power (and allowed), disable modulation and exit
2203
2206
  if value >= 100:
@@ -2212,6 +2215,9 @@ class FeatureIdentificationDevice(InterfaceDevice):
2212
2215
  period_us = 1000 if self.settings.state.laser_power_high_resolution else 100
2213
2216
  width_us = int(round(1.0 * value * period_us / 100.0, 0)) # note that value is in range (0, 100) not (0, 1)
2214
2217
 
2218
+ if width_us <= 0:
2219
+ return set_laser_enable(False)
2220
+
2215
2221
  # pulse width can't be longer than period, or shorter than 1us
2216
2222
  width_us = max(1, min(width_us, period_us))
2217
2223
 
@@ -2812,6 +2818,9 @@ class FeatureIdentificationDevice(InterfaceDevice):
2812
2818
  log.debug(msg)
2813
2819
  return
2814
2820
 
2821
+ # MZ: just disable for now
2822
+ # return
2823
+
2815
2824
  if self.settings.microcontroller_firmware_version == "1.0.2.9":
2816
2825
  msg = "ambient temperature ARM requires newer firmware"
2817
2826
  log.debug(msg)
@@ -5,12 +5,19 @@ import threading
5
5
 
6
6
  class TakeOneRequest:
7
7
 
8
- def __init__(self, take_dark=False, enable_laser_before=False, disable_laser_after=False, laser_warmup_ms=0, scans_to_average=1):
9
- self.take_dark = take_dark
10
- self.enable_laser_before = enable_laser_before
11
- self.disable_laser_after = disable_laser_after
12
- self.laser_warmup_ms = laser_warmup_ms
13
- self.scans_to_average = scans_to_average
8
+ def __init__(self, take_dark=False, enable_laser_before=False, disable_laser_after=False, laser_warmup_ms=0, scans_to_average=1, template=None):
9
+ if template is not None:
10
+ self.take_dark = template.take_dark
11
+ self.enable_laser_before = template.enable_laser_before
12
+ self.disable_laser_after = template.disable_laser_after
13
+ self.laser_warmup_ms = template.laser_warmup_ms
14
+ self.scans_to_average = template.scans_to_average
15
+ else:
16
+ self.take_dark = take_dark
17
+ self.enable_laser_before = enable_laser_before
18
+ self.disable_laser_after = disable_laser_after
19
+ self.laser_warmup_ms = laser_warmup_ms
20
+ self.scans_to_average = scans_to_average
14
21
 
15
22
  self.request_id = f"{os.getpid():05d}-{threading.get_native_id():05d}-{time.time_ns()}-{random.randrange(65536):05d}"
16
23
 
@@ -293,7 +293,7 @@ class WasatchDevice(InterfaceDevice):
293
293
  # (given sleep()'s precision) for each acquisition. For true precision
294
294
  # this should all go into the firmware anyway.
295
295
 
296
- auto_enable_laser = tor and tor.enable_laser_before
296
+ auto_enable_laser = tor is not None and tor.enable_laser_before
297
297
  log.debug("acquire_spectrum: auto_enable_laser = %s", auto_enable_laser)
298
298
 
299
299
  dark_reading = SpectrometerResponse()
@@ -841,7 +841,7 @@ class WasatchDevice(InterfaceDevice):
841
841
  self.session_reading_count += 1
842
842
  reading.session_count = self.session_reading_count
843
843
  reading.sum_count = self.sum_count
844
- log.debug(f"take_one_averaged_reading: reading.sum_count now {reading.sum_count}")
844
+ log.debug(f"take_one_averaged_reading: reading.sum_count now {reading.sum_count}, session_count {reading.session_count}")
845
845
 
846
846
  # have we completed the averaged reading?
847
847
  if sum_locally:
@@ -878,17 +878,17 @@ class WasatchDevice(InterfaceDevice):
878
878
  # to the device.
879
879
  #
880
880
  # Essentially this iterates through all the (setting, value) pairs we've
881
- # received through change_setting() which have not yet been processed, and
881
+ # received through change_setting() which have not yet been processed, and <-- MZ: incomplete sentence
882
882
  #
883
883
  #
884
- # Note that WasatchDeviceWrapper.continuous_poll "de-dupes" commands on
884
+ # Note that WrapperWorker.run "de-dupes" commands on
885
885
  # receipt from ENLIGHTEN, so the command stream arising from that source
886
886
  # should already be optimized and minimal. Commands injected manually by
887
887
  # calling WasatchDevice.change_setting() do not receive this treatment.
888
888
  #
889
- # In the normal multi-process (ENLIGHTEN) workflow, this function is called
889
+ # In the normal multithreaded (ENLIGHTEN) workflow, this function is called
890
890
  # at the beginning of acquire_data, itself ticked regularly by
891
- # WasatchDeviceWrapper.continuous_poll.
891
+ # WrapperWorker.run.
892
892
  def process_commands(self):
893
893
  control_object = "throwaway"
894
894
  retval = False
@@ -900,6 +900,9 @@ class WasatchDevice(InterfaceDevice):
900
900
  # is this a command used by WasatchDevice itself, and not
901
901
  # passed down to FeatureIdentificationDevice?
902
902
  if control_object.setting.lower() == "acquire":
903
+ # MZ: does this ever happen? It looks like the current
904
+ # request sent down from WrapperWorker is "acquire_data"
905
+ # not "acquire"...
903
906
  log.debug("process_commands: acquire found")
904
907
  retval = True
905
908
  else:
@@ -17,6 +17,9 @@ log = logging.getLogger(__name__)
17
17
  # Readings and StatusMessages) for multiprocessing-safe device communications
18
18
  # and acquisition under Windows and Qt.
19
19
  #
20
+ # @todo This document is full of references to continuous_poll(). That method
21
+ # no longer exists, and has been replaced by WrapperWorker.run()
22
+ #
20
23
  # @par Lifecycle
21
24
  #
22
25
  # From ENLIGHTEN's standpoint (the original Wasatch.PY caller), here's what's going on:
@@ -134,9 +137,10 @@ class WasatchDeviceWrapper:
134
137
  # could include "FILE:/path/to/dir", etc. However, device_id is just
135
138
  # a string scalar to this class, and actually parsing / using it should be
136
139
  # entirely encapsulated within WasatchDevice and lower using DeviceID.
137
- def __init__(self, device_id, log_level):
140
+ def __init__(self, device_id, log_level, callback=None):
138
141
  self.device_id = device_id
139
142
  self.log_level = log_level
143
+ self.callback = callback
140
144
 
141
145
  self.settings_queue = False
142
146
  self.response_queue = False
@@ -223,7 +227,9 @@ class WasatchDeviceWrapper:
223
227
  is_ocean = self.is_ocean,
224
228
  is_andor = self.is_andor,
225
229
  is_spi = self.is_spi,
226
- is_ble = self.is_ble)
230
+ is_ble = self.is_ble,
231
+ log_level = self.log_level,
232
+ callback = self.callback)
227
233
  log.debug("device wrapper: Instance created for worker")
228
234
 
229
235
  self.wrapper_worker.daemon = True
@@ -236,14 +242,25 @@ class WasatchDeviceWrapper:
236
242
  self.settings = None
237
243
  log.debug("connect: setup connection, returning to controller for settings polling")
238
244
 
245
+ if self.callback:
246
+ self.wait_for_settings()
247
+
239
248
  return True
240
249
 
241
- def poll_settings(self): # -> SpectrometerResponse
250
+ def wait_for_settings(self):
251
+ while True:
252
+ if self.poll_settings():
253
+ break
254
+ if (datetime.datetime.now() - self.connect_start_time).total_seconds() > 2:
255
+ raise Exception("Failed to read SpectrometerSettings")
256
+ time.sleep(0.1)
257
+
258
+ def poll_settings(self):
242
259
  """ @returns SpectrometerResponse(True) on success, (False) otherwise """
243
260
  log.debug("polling device settings")
244
261
  if not self.settings_queue.empty():
245
262
  result = self.settings_queue.get_nowait()
246
- if result is None: # shouldn't happen
263
+ if result is None:
247
264
  log.critical("poll_settings: failed to retrieve device settings (got None, shouldn't happen)")
248
265
  return SpectrometerResponse(False, error_msg="Failed to retrieve device settings")
249
266
 
@@ -280,12 +297,6 @@ class WasatchDeviceWrapper:
280
297
 
281
298
  self.connected = False
282
299
 
283
- # MZ: why do we recreate these?
284
- self.settings_queue = Queue()
285
- self.response_queue = Queue()
286
- self.message_queue = Queue()
287
- self.command_queue = Queue()
288
-
289
300
  return True
290
301
 
291
302
  ##
@@ -413,10 +424,6 @@ class WasatchDeviceWrapper:
413
424
  # none of those. Yet apparently we did read some normal readings.
414
425
  # Return the latest of those.
415
426
 
416
- # MZ: this would be a possible place to revert logging from DEBUG to
417
- # whatever was passed (self.log_level), especially if this is the
418
- # first reading returned by this thread.
419
-
420
427
  if WasatchDeviceWrapper.DISABLE_RESPONSE_QUEUE:
421
428
  self.previous_reading = last_reading
422
429
  return last_reading
@@ -2,6 +2,7 @@ import threading
2
2
  import logging
3
3
  import time
4
4
  from queue import Queue
5
+ from datetime import datetime
5
6
 
6
7
  from .SpectrometerResponse import SpectrometerResponse
7
8
  from .SpectrometerRequest import SpectrometerRequest
@@ -34,6 +35,8 @@ class WrapperWorker(threading.Thread):
34
35
  # TODO: Create ABC of hardware device that keeps common functions like handle_requests
35
36
  POLLER_WAIT_SEC = 0.05 # .05sec = 50ms = update from hardware device at 20Hz
36
37
 
38
+ DEBUG_SEC = 10 # enforce debug logging for the 1st 10sec after connecting a new spectrometer
39
+
37
40
  def __init__(
38
41
  self,
39
42
  device_id,
@@ -41,11 +44,12 @@ class WrapperWorker(threading.Thread):
41
44
  response_queue,
42
45
  settings_queue,
43
46
  message_queue,
44
- is_ocean,
47
+ is_ocean, # MZ: all these bools should have just been a string "type"
45
48
  is_andor,
46
49
  is_spi,
47
50
  is_ble,
48
- parent=None):
51
+ log_level,
52
+ callback=None):
49
53
 
50
54
  threading.Thread.__init__(self)
51
55
 
@@ -58,8 +62,18 @@ class WrapperWorker(threading.Thread):
58
62
  self.response_queue = response_queue
59
63
  self.settings_queue = settings_queue
60
64
  self.message_queue = message_queue
65
+ self.log_level = log_level
66
+ self.callback = callback
67
+
61
68
  self.connected_device = None
62
69
 
70
+ self.thread_start = datetime.now()
71
+ self.initial_connection_logging = True
72
+
73
+ # enforce debug logging around ENLIGHTEN connections
74
+ if not self.callback:
75
+ logging.getLogger().setLevel("DEBUG")
76
+
63
77
  ##
64
78
  # This is essentially the main() loop in a thread.
65
79
  # All communications with the parent thread are routed through
@@ -176,7 +190,11 @@ class WrapperWorker(threading.Thread):
176
190
  continue
177
191
  log.debug(f"response {reading_response} data is {reading_response.data}")
178
192
 
179
- if reading_response.keep_alive:
193
+ if self.callback:
194
+ log.debug("worker returning response via callback")
195
+ self.callback(reading_response)
196
+
197
+ elif reading_response.keep_alive:
180
198
  log.debug("worker is flowing up keep_alive")
181
199
  self.response_queue.put(reading_response)
182
200
 
@@ -209,6 +227,15 @@ class WrapperWorker(threading.Thread):
209
227
  except:
210
228
  log.error("unable to push Reading %d to GUI", reading_response.data.session_count, exc_info=1)
211
229
 
230
+ # We just successfully sent a reading back to the main thread.
231
+ # After sending 10sec of such readings, we can disable the default
232
+ # DEBUG logging and revert to whatever the caller requested.
233
+ if (self.initial_connection_logging and
234
+ (datetime.now() - self.thread_start).total_seconds() > self.DEBUG_SEC):
235
+ log.info(f"relaxing log_level to {self.log_level} after {self.DEBUG_SEC}sec")
236
+ logging.getLogger().setLevel(self.log_level)
237
+ self.initial_connection_logging = False
238
+
212
239
  else:
213
240
  log.error("received non-failure Reading without spectrum...ignoring?")
214
241
 
@@ -1,4 +1,4 @@
1
1
  """This package is a driver for Wasatch Photonics spectrometers"""
2
2
 
3
- __version__ = "2.1.64" # This is used by flit and other pypi things
3
+ __version__ = "2.2.0" # This is used by flit and other pypi things
4
4
  version = __version__ # This is to avoid breaking other files that originally used .version instead of __version__
@@ -159,7 +159,7 @@ class MainLogger:
159
159
  fh.setFormatter(formatter)
160
160
  root_logger.addHandler(fh)
161
161
 
162
- if self.enable_stdout:
162
+ if self.enable_stdout and sys.stdout is not None:
163
163
  sys.stdout.reconfigure(encoding='utf-8')
164
164
  stream_handler = logging.StreamHandler(sys.stdout)
165
165
  stream_handler.setFormatter(formatter)
@@ -468,7 +468,7 @@ def coeffs_look_valid(coeffs, count=None):
468
468
 
469
469
  # check for constants (all coefficients the same value)
470
470
  all_const = True
471
- log.info(coeffs)
471
+ # log.debug(coeffs)
472
472
  for i in range(1, len(coeffs)):
473
473
  if coeffs[0] != coeffs[i]:
474
474
  all_const = False
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