wasatch 2.3.24__tar.gz → 2.4.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 (103) hide show
  1. {wasatch-2.3.24 → wasatch-2.4.0}/CHANGELOG.md +81 -0
  2. {wasatch-2.3.24 → wasatch-2.4.0}/PKG-INFO +1 -1
  3. {wasatch-2.3.24 → wasatch-2.4.0}/WasatchShell/wasatch-shell.py +4 -4
  4. {wasatch-2.3.24 → wasatch-2.4.0}/pyproject.toml +1 -1
  5. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/AbstractUSBDevice.py +4 -2
  6. wasatch-2.4.0/wasatch/AccessoryConnector.py +75 -0
  7. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/AndorDevice.py +84 -80
  8. wasatch-2.4.0/wasatch/AssemblyRevision.py +56 -0
  9. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/AutoRaman.py +213 -105
  10. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/BLEDevice.py +52 -35
  11. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/CSVLoader.py +3 -4
  12. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/DeviceFinderUSB.py +4 -0
  13. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/EEPROM.py +247 -108
  14. wasatch-2.4.0/wasatch/EtalonCorrection.py +73 -0
  15. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/FeatureIdentificationDevice.py +303 -273
  16. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/FirmwareRequirements.py +5 -4
  17. wasatch-2.4.0/wasatch/HardwareInfo.py +15 -0
  18. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/IDSCamera.py +5 -0
  19. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/IDSDevice.py +238 -52
  20. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/IMX385.py +42 -0
  21. wasatch-2.4.0/wasatch/InGaAsCorrection.py +79 -0
  22. wasatch-2.4.0/wasatch/InterfaceDevice.py +255 -0
  23. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/MockUSBDevice.py +3 -0
  24. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/OceanDevice.py +2 -41
  25. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/ProcessedReading.py +29 -1
  26. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/Reading.py +3 -0
  27. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/RealUSBDevice.py +5 -0
  28. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/SPIDevice.py +1 -39
  29. wasatch-2.4.0/wasatch/SpectrometerRequest.py +41 -0
  30. wasatch-2.4.0/wasatch/SpectrometerResponse.py +41 -0
  31. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/SpectrometerSettings.py +75 -22
  32. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/SpectrometerState.py +5 -3
  33. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/TCPDevice.py +1 -2
  34. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/USBCPowerConnectionState.py +34 -9
  35. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/WasatchBus.py +10 -12
  36. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/WasatchDevice.py +126 -234
  37. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/WasatchDeviceWrapper.py +34 -6
  38. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/WrapperWorker.py +17 -35
  39. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/__init__.py +1 -1
  40. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/utils.py +12 -0
  41. wasatch-2.3.24/wasatch/CommandSettings.py +0 -95
  42. wasatch-2.3.24/wasatch/HardwareInfo.py +0 -36
  43. wasatch-2.3.24/wasatch/InterfaceDevice.py +0 -48
  44. wasatch-2.3.24/wasatch/PollStatus.py +0 -15
  45. wasatch-2.3.24/wasatch/SpectrometerRequest.py +0 -15
  46. wasatch-2.3.24/wasatch/SpectrometerResponse.py +0 -38
  47. {wasatch-2.3.24 → wasatch-2.4.0}/.example_pypirc +0 -0
  48. {wasatch-2.3.24 → wasatch-2.4.0}/.flake8 +0 -0
  49. {wasatch-2.3.24 → wasatch-2.4.0}/.gitignore +0 -0
  50. {wasatch-2.3.24 → wasatch-2.4.0}/.pylintrc +0 -0
  51. {wasatch-2.3.24 → wasatch-2.4.0}/Doxyfile +0 -0
  52. {wasatch-2.3.24 → wasatch-2.4.0}/LICENSE +0 -0
  53. {wasatch-2.3.24 → wasatch-2.4.0}/MANIFEST.in +0 -0
  54. {wasatch-2.3.24 → wasatch-2.4.0}/Makefile +0 -0
  55. {wasatch-2.3.24 → wasatch-2.4.0}/README.md +0 -0
  56. {wasatch-2.3.24 → wasatch-2.4.0}/README_PYPI.md +0 -0
  57. {wasatch-2.3.24 → wasatch-2.4.0}/README_RPI.md +0 -0
  58. {wasatch-2.3.24 → wasatch-2.4.0}/README_SETTINGS.md +0 -0
  59. {wasatch-2.3.24 → wasatch-2.4.0}/README_VIRTUAL.md +0 -0
  60. {wasatch-2.3.24 → wasatch-2.4.0}/WasatchShell/README.md +0 -0
  61. {wasatch-2.3.24 → wasatch-2.4.0}/WasatchShell/laser-test.py +0 -0
  62. {wasatch-2.3.24 → wasatch-2.4.0}/WasatchShell/load-test.py +0 -0
  63. {wasatch-2.3.24 → wasatch-2.4.0}/WasatchShell/one-shot.py +0 -0
  64. {wasatch-2.3.24 → wasatch-2.4.0}/demo-auto-raman.py +0 -0
  65. {wasatch-2.3.24 → wasatch-2.4.0}/demo-ids-raman.py +0 -0
  66. {wasatch-2.3.24 → wasatch-2.4.0}/demo-simple.py +0 -0
  67. {wasatch-2.3.24 → wasatch-2.4.0}/demo-virtual.py +0 -0
  68. {wasatch-2.3.24 → wasatch-2.4.0}/demo-workflow.py +0 -0
  69. {wasatch-2.3.24 → wasatch-2.4.0}/demo.py +0 -0
  70. {wasatch-2.3.24 → wasatch-2.4.0}/environments/conda-linux.yml +0 -0
  71. {wasatch-2.3.24 → wasatch-2.4.0}/environments/conda-macos.yml +0 -0
  72. {wasatch-2.3.24 → wasatch-2.4.0}/environments/conda-rpi.yml +0 -0
  73. {wasatch-2.3.24 → wasatch-2.4.0}/environments/conda-win10.yml +0 -0
  74. {wasatch-2.3.24 → wasatch-2.4.0}/screenshots/multiplatform.png +0 -0
  75. {wasatch-2.3.24 → wasatch-2.4.0}/scripts/dark-vs-integ.py +0 -0
  76. {wasatch-2.3.24 → wasatch-2.4.0}/scripts/deploy +0 -0
  77. {wasatch-2.3.24 → wasatch-2.4.0}/testSpectrometers/SiG_785/eeprom/EEPROM-EM-9c65d19f4c.json +0 -0
  78. {wasatch-2.3.24 → wasatch-2.4.0}/testSpectrometers/SiG_785/readings/enlighten-20210329-094722-799520-WP-00686.csv +0 -0
  79. {wasatch-2.3.24 → wasatch-2.4.0}/testSpectrometers/SiG_785/readings/enlighten-20210329-094755-288958-WP-00686.csv +0 -0
  80. {wasatch-2.3.24 → wasatch-2.4.0}/testSpectrometers/WP-00887/eeprom/WP-00887-mock.json +0 -0
  81. {wasatch-2.3.24 → wasatch-2.4.0}/testSpectrometers/WP-00904/eeprom/WP-00904-mock.json +0 -0
  82. {wasatch-2.3.24 → wasatch-2.4.0}/testSpectrometers/WP-00913/eeprom/WP-00913-mock.json +0 -0
  83. {wasatch-2.3.24 → wasatch-2.4.0}/tests/existing_implementations.py +0 -0
  84. {wasatch-2.3.24 → wasatch-2.4.0}/tests/noise-by-setpoint.py +0 -0
  85. {wasatch-2.3.24 → wasatch-2.4.0}/tests/queues.py +0 -0
  86. {wasatch-2.3.24 → wasatch-2.4.0}/tests/unit.py +0 -0
  87. {wasatch-2.3.24 → wasatch-2.4.0}/udev/10-wasatch.rules +0 -0
  88. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/AreaScanImage.py +0 -0
  89. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/AutoRamanRequest.py +0 -0
  90. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/BalanceAcquisition.py +0 -0
  91. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/ControlObject.py +0 -0
  92. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/DetectorROI.py +0 -0
  93. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/DetectorRegions.py +0 -0
  94. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/DeviceFinderBLE.py +0 -0
  95. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/DeviceFinderTCP.py +0 -0
  96. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/DeviceID.py +0 -0
  97. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/EEPROM_multical.sav +0 -0
  98. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/FPGAOptions.py +0 -0
  99. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/ROI.py +0 -0
  100. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/StatusMessage.py +0 -0
  101. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/TakeOneRequest.py +0 -0
  102. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/applog.py +0 -0
  103. {wasatch-2.3.24 → wasatch-2.4.0}/wasatch/simulation_protocol.py +0 -0
@@ -1,5 +1,86 @@
1
1
  # Changelog
2
2
 
3
+ _Note: in the following, FID = wasatch.FeatureInterfaceDevice_
4
+
5
+ - 2026-05-27 2.4.0 (enlighten-4.2.2)
6
+ - InterfaceDevice
7
+ - This was a major change. The InterfaceDevice base class has existed for
8
+ years, but didn't contribute anything beyond handle_requests. I've
9
+ moved message_queue and alert_queue up from WasatchDevice/FID so all
10
+ InterfaceDevice subclasses can use that functionality directly.
11
+ - Also, the handle_requests([ SpectrometerRequest(cmd, args=[value]) ])[0]
12
+ syntax was neurose-inducing, so I've added convenience shortcuts for
13
+ handle_request(SpectrometerRequest) (singular), and handle_cmd(name[, value]).
14
+ - WasatchDevice
15
+ - deprecated command_queue (pending_commands)
16
+ - handle_requests now directly peeks into FID to see which commands can
17
+ be handled by "the hardware layer," and processes those commands in
18
+ real-time, accumulating the list of responses. This allows callers to
19
+ use gettor methods like GET_LASER_WARNING_TIME_SEC and actually receive
20
+ the response value, which was previously impossible when such commands
21
+ were shoved onto the "pending command queue" (which offered no method
22
+ for response delivery).
23
+ - FeatureInterfaceDevice
24
+ - deprecated SpectrumAndRow (legacy Area Scan design)
25
+ - removed handle_requests overload (not sure why this was there)
26
+ - added get_power_connection_state
27
+ - added periodic check for late-arriving attributes like ble_firmware_version
28
+ - enabled get_microcontroller_serial_number
29
+ - tweak laser password validation logic
30
+ - add default laser warning delay sec for old FW
31
+ - IDSDevice
32
+ - paired laser_device
33
+ - A key goal for this release was to allow IDSDevice "sensor
34
+ control" objects to "pair" with WasatchDevice "laser control"
35
+ objects, for the new case where an IDS sensor was being combined
36
+ with a Wasatch laser driver to create a 2-device Raman spectrometer.
37
+ - Note that once "paired," they essentially merge EEPROMs (starting
38
+ with whatever WasatchDevice loaded from USB, then folding-in
39
+ whatever IDSDevice found in JSON).
40
+ - In this first implementation, I chose to connect the two (make one
41
+ device "discoverable" by the other) through WasatchDeviceWrapper.
42
+ That created a weird import loop and maybe was higher than it
43
+ needed to be. I may refactor this later so they're discoverable /
44
+ joined at the InterfaceDevice level itself, which resolves the loop
45
+ and makes the pair usable without WasatchDeviceWrapper (a class
46
+ essentially designed by and for ENLIGHTEN).
47
+ - Part of the point of the above was to support a subtype of Auto-Raman,
48
+ tentatively called Auto-Collection, which skips the integ/gain
49
+ optimization and goes straight for averaging dark-collected Raman
50
+ within a measurement window.
51
+ - EEPROM
52
+ - A third key goal of this release was to support all the new EEPROM fields
53
+ added from Rev18 to Rev19 of ENG-0034, including:
54
+ - startup_laser_tec_setpoint
55
+ - light_source_type
56
+ - assembly_revision (and AssemblyRevision)
57
+ - feature_mask_xs
58
+ - max_battery_temp_deg_c
59
+ - max_laser_temp_deg_c
60
+ - acc_state
61
+ - acc_state_gpio1
62
+ - acc_state_gpio2
63
+ - acc_cont_strobe_count
64
+ - acc_cont_strobe_delay_us
65
+ - acc_cont_strobe_period_us
66
+ - acc_cont_strobe_width_us
67
+ - pixel_correction_type
68
+ - deprecated baud_rate, linearity_coeffs
69
+ - made EEPROM subclasses JSON serializable (hopefully)
70
+ - fixed EEPROM.pack for binary types ("*") for our growing set of "binary"
71
+ (marshalled) fields
72
+ - moved further into using the new un/pack_field functions, automatically
73
+ pulling offset/size/type from eeprom_fields table
74
+ - revisited user_text / user_data relationship
75
+ - consolidating backup location when writing EEPROM (can probably just
76
+ get rid of this)
77
+ - BLEDevice
78
+ - added laser PWM
79
+ - AndorDevice
80
+ - renamed "shutter_enable" (whatever that meant) to "shutter_open"
81
+ - SpectrometerResponse
82
+ - deprecated progress and incomplete
83
+ - starting to consider Safe Mode
3
84
  - 2026-04-18 2.3.24
4
85
  - add avg_resolution to IDSDevice EEPROM
5
86
  - change XS attenuator default from 127 to 40
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wasatch
3
- Version: 2.3.24
3
+ Version: 2.4.0
4
4
  Summary: Application-level driver for Wasatch Photonics spectrometers
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -105,7 +105,7 @@ class WasatchShell:
105
105
  "get_selected_adc", # SR
106
106
  "get_selected_laser", # SR
107
107
  "get_sensor_line_length", # SR
108
- "get_shutter_enabled", # SR
108
+ "get_shutter_open", # SR
109
109
  "get_strobe_enabled", # SR
110
110
  "get_tec_enabled", # SR
111
111
  "get_trigger_delay", # SR
@@ -146,7 +146,7 @@ class WasatchShell:
146
146
 
147
147
  set_fan_enable - takes bool argument
148
148
  set_lamp_enable - takes bool argument
149
- set_shutter_enable - takes bool argument
149
+ set_shutter_open - takes bool argument
150
150
  set_strobe_enable - takes bool argument
151
151
  set_mod_enable - takes bool argument
152
152
  set_mod_period_us - takes int argument
@@ -394,8 +394,8 @@ class WasatchShell:
394
394
  self.device.change_setting("lamp_enable", self.read_bool())
395
395
  self.display(1)
396
396
 
397
- elif command == "set_shutter_enable":
398
- self.device.change_setting("shutter_enable", self.read_bool())
397
+ elif command == "set_shutter_open":
398
+ self.device.change_setting("shutter_open", self.read_bool())
399
399
  self.display(1)
400
400
 
401
401
  elif command == "set_strobe_enable":
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wasatch"
3
- version = "2.3.24"
3
+ version = "2.4.0"
4
4
  description= "Application-level driver for Wasatch Photonics spectrometers"
5
5
  requires-python = ">=3.9"
6
6
  readme = "README.md"
@@ -1,5 +1,7 @@
1
-
2
1
  class AbstractUSBDevice:
2
+ """
3
+ This is NOT an InterfaceDevice. This is used by MockUSBDevice and RealUSBDevice.
4
+ """
3
5
 
4
6
  def __init__(self):
5
7
  pass
@@ -26,4 +28,4 @@ class AbstractUSBDevice:
26
28
  pass
27
29
 
28
30
  def send_code(self):
29
- pass
31
+ pass
@@ -0,0 +1,75 @@
1
+
2
+
3
+ class GPIOState:
4
+ MODE_FUNCTION = 0
5
+ MODE_MANUAL = 1
6
+
7
+ DIR_INPUT = 0
8
+ DIR_OUTPUT = 1
9
+
10
+ GPIO_FUNCTION_DISABLED = 0
11
+
12
+ GPIO1_FUNCTION_EXT_TRIGGER = 1
13
+ GPIO1_FUNCTION_LASER_OVERRIDE = 2
14
+
15
+ GPIO2_FUNCTION_CONT_STROBE = 1
16
+ GPIO2_FUNCTION_DATA_READY = 2
17
+ GPIO2_FUNCTION_LASER_MIRROR = 3
18
+ GPIO2_FUNCTION_LASER_ERROR = 4
19
+
20
+ def __init__(self, num):
21
+ self.num = num
22
+ self.mode = GPIOState.MODE_FUNCTION
23
+ self.dir = GPIOState.DIR_INPUT
24
+ self.function = GPIOState.GPIO_FUNCTION_DISABLED
25
+ self.value = 0
26
+
27
+ def serialize(self):
28
+ mask = 0x00
29
+ mask |= 0x01 if self.mode == self.MODE_MANUAL else 0
30
+ mask |= 0x02 if self.dir == self.DIR_OUTPUT else 0
31
+ if self.dir == self.DIR_OUTPUT:
32
+ mask |= 0x04 if self.value else 0
33
+ # bit 3 reserved
34
+ mask |= (self.function << 4)
35
+
36
+ def deserialize(self, mask):
37
+ self.mode = mask & 0x01
38
+ self.dir = mask & 0x02
39
+ if self.dir == self.DIR_INPUT:
40
+ self.value = mask & 0x04
41
+ self.function = (mask >> 4) & 0xf
42
+
43
+ def __repr__(self):
44
+ return f"GPIO{self.num}< mode {self.mode}, dir {self.dir}, func {self.function}, value {self.value} >"
45
+
46
+ class AccessoryConnector:
47
+
48
+ def __init__(self):
49
+ self.acc_gpio_enabled = False
50
+ self.acc_5v_enabled = False
51
+ self.acc_5v_good = False # read-only
52
+
53
+ self.state_gpio1 = GPIOState(1)
54
+ self.state_gpio2 = GPIOState(2)
55
+
56
+ self.strobe_period_us = None
57
+ self.strobe_width_us = None
58
+ self.strobe_delay_us = None
59
+ self.strobe_count = None
60
+
61
+ def serialize_acc_state(self):
62
+ mask = 0x0000
63
+ mask |= 0x0001 if self.acc_gpio_enabled else 0
64
+ mask |= 0x0002 if self.acc_5v_enabled else 0
65
+ # acc_5v_good read-only
66
+ # bits 3-15 reserved
67
+ return mask
68
+
69
+ def deserialize_acc_state(self, mask):
70
+ self.acc_gpio_enabled = 0 != mask & 0x01
71
+ self.acc_5v_enabled = 0 != mask & 0x02
72
+ self.acc_5v_good = 0 != mask & 0x04 # read-only
73
+
74
+ def __repr__(self):
75
+ return f"AccessoryConnector < gpio_enabled {self.acc_gpio_enabled}, 5v_enabled {self.acc_5v_enabled}, 5v_good {self.acc_5v_good}"
@@ -24,28 +24,6 @@ class AndorDevice(InterfaceDevice):
24
24
 
25
25
  @todo have check_result return a SpectrometerResponse
26
26
  @todo try to auto-detect whether x-axis needs inverted via DLL.GetImageFlip()
27
-
28
- ##########################################################################
29
- This class adopts the external device interface structure.
30
- This involves receiving a request through the handle_request function.
31
- A request is processed based on the key in the request.
32
- The processing function passes the commands to the requested device.
33
- Once it receives a response from the connected device it then passes that
34
- back up the chain.
35
- @verbatim
36
- Enlighten Request
37
- |
38
- handle_requests
39
- |
40
- -----------
41
- | | | | |
42
- { get_laser status, acquire, set_laser_watchdog, etc....}
43
- | | | | |
44
- -----------
45
- |
46
- {self.driver.some_andor_sdk_call}
47
- @endverbatim
48
- ############################################################################
49
27
  """
50
28
 
51
29
  SUCCESS = 20002 #!< see load_error_codes()
@@ -55,29 +33,15 @@ class AndorDevice(InterfaceDevice):
55
33
  DRIVER_NOT_INSTALLED = False
56
34
 
57
35
  def __init__(self, device_id, message_queue=None, alert_queue=None):
58
- # if passed a string representation of a DeviceID, deserialize it
59
- super().__init__()
36
+ super().__init__(device_id=device_id, message_queue=message_queue, alert_queue=alert_queue)
60
37
 
61
38
  if self.DRIVER_NOT_INSTALLED:
62
39
  raise InterfaceDeviceClassUnavailable("Andor driver not installed")
63
40
 
64
- if type(device_id) is str:
65
- device_id = DeviceID(label=device_id)
66
-
67
- self.device_id = device_id
68
- self.message_queue = message_queue
69
- self.alert_queue = alert_queue
70
-
71
41
  self.load_error_codes()
72
42
 
73
43
  self.connected = False
74
44
 
75
- # Receives ENLIGHTEN's 'change settings' commands in the spectrometer
76
- # process.
77
- self.command_queue = []
78
-
79
- self.immediate_mode = False
80
-
81
45
  # An AndorDevice has-a SpectrometerSettings, which has-a EEPROM, which
82
46
  # has-a MultiWavelengthCalibration
83
47
  self.settings = SpectrometerSettings(self.device_id)
@@ -179,40 +143,39 @@ class AndorDevice(InterfaceDevice):
179
143
  except:
180
144
  log.error(f"failed to enqueue StatusMessage {msg}", exc_info=1)
181
145
 
182
- def not_implemented(self):
146
+ def not_implemented(self, arg):
183
147
  pass
184
148
 
185
149
  def _init_process_funcs(self):
186
150
  process_f = {}
187
151
 
188
- process_f["connect"] = self.connect
189
- process_f["acquire_data"] = self.acquire_data
190
- process_f["set_shutter_enable"] = self.set_shutter_enable
191
- process_f["set_integration_time_ms"] = self.set_integration_time_ms
192
- process_f["get_serial_number"] = self.get_serial_number
193
- process_f["init_tec_setpoint"] = self.init_tec_setpoint
194
- process_f["set_tec_setpoint"] = self.set_tec_setpoint
195
- process_f["init_detector_area"] = self.init_detector_area
196
- process_f["scans_to_average"] = self.set_scans_to_average
197
- process_f["high_gain_mode_enable"] = self.high_gain_mode_enable
198
- process_f["save_config"] = self.save_config
152
+ # setting and function have same name
153
+ for fn_name in [
154
+ "connect",
155
+ "acquire_data",
156
+ "set_shutter_open",
157
+ "set_integration_time_ms",
158
+ "get_serial_number",
159
+ "init_tec_setpoint",
160
+ "set_tec_setpoint",
161
+ "init_detector_area",
162
+ "scans_to_average",
163
+ "high_gain_mode_enable",
164
+ "save_config",
165
+ "set_vertical_binning",
166
+ "set_ingaas_correction_enable",
167
+ "set_etalon_correction_enable" ]:
168
+ process_f[fn_name] = getattr(self, fn_name)
169
+
170
+ # setting and function have different names
171
+ process_f["integration_time_ms"] = self.set_integration_time_ms
172
+ process_f["fan_enable"] = self.set_fan_enable
173
+ process_f["shutter_open"] = self.set_shutter_open
174
+ process_f["detector_tec_enable"] = self.set_detector_tec_enable
175
+ process_f["detector_tec_setpoint_degC"] = self.set_tec_setpoint
199
176
  process_f["vertical_binning"] = self.set_vertical_binning
200
177
  process_f["take_one_request"] = self.set_take_one_request
201
178
 
202
- process_f["reset_scan_averaging"] = self.not_implemented
203
- process_f["heartbeat"] = lambda x: None
204
-
205
- ##################################################################
206
- # What follows is the old init-lambdas that are squashed into process_f
207
- # Long term, the upstream requests should be changed to match the new format
208
- # This is an easy fix for the time being to make things behave
209
- ##################################################################
210
- process_f["integration_time_ms"] = lambda x: self.set_integration_time_ms(x)
211
- process_f["fan_enable"] = lambda x: self.set_fan_enable(bool(x))
212
- process_f["shutter_enable"] = lambda x: self.set_shutter_enable(bool(x))
213
- process_f["detector_tec_enable"] = lambda x: self.toggle_tec(bool(x))
214
- process_f["detector_tec_setpoint_degC"] = lambda x: self.set_tec_setpoint(int(round(x)))
215
-
216
179
  return process_f
217
180
 
218
181
  def high_gain_mode_enable(self, enabled):
@@ -227,9 +190,13 @@ class AndorDevice(InterfaceDevice):
227
190
  log.debug(f"for {enabled} setting gain to {self.gain_options[0]}")
228
191
  return
229
192
 
230
- def set_fan_enable(self, x):
231
- self.check_result(self.driver.SetFanMode(int(x)), f"Andor Fan On {x}")
232
- return SpectrometerResponse()
193
+ def set_fan_enable(self, flag):
194
+ flag = True if flag else False
195
+ try:
196
+ self.check_result(self.driver.SetFanMode(int(x)), f"Andor Fan On {x}")
197
+ except:
198
+ return SpectrometerResponse(False)
199
+ return SpectrometerResponse(True)
233
200
 
234
201
  def _get_default_data_dir(self):
235
202
  if os.name == "nt":
@@ -271,6 +238,12 @@ class AndorDevice(InterfaceDevice):
271
238
  if not spectrum.any():
272
239
  self._queue_message("marquee_error", "Andor camera is saturated")
273
240
 
241
+ if self.settings.ingaas_correction:
242
+ spectrum = self.settings.ingaas_correction.apply(spectrum)
243
+
244
+ if self.settings.etalon_correction:
245
+ spectrum = self.settings.etalon_correction.apply(spectrum)
246
+
274
247
  # log.debug(f"_get_spectrum_raw: returning spectrum {spectrum}")
275
248
  return spectrum
276
249
 
@@ -389,13 +362,29 @@ class AndorDevice(InterfaceDevice):
389
362
  return self.temperature_cache_value
390
363
 
391
364
  def _close_ex_shutter(self):
365
+ # SetShutterEx(typ, internalMode, closingTimeMS, openingTimeMS, externalMode)
366
+ # where:
367
+ # - typ:
368
+ # 0 = output TTL LOW to open shutter
369
+ # 1 = output TTL HIGH to open shutter
370
+ #
371
+ # - mode:
372
+ # 0 = Fully Auto
373
+ # 1 = Permanently Open
374
+ # 2 = Permanently Closed
375
+ # 3 = UNDEFINED
376
+ # 4 = Open for FVB series
377
+ # 5 = Open for any series
378
+
379
+ # set TTL HIGH="open", internalMode PermanentlyOpen, externalMode PermanentlyClosed
392
380
  self.check_result(self.driver.SetShutterEx(1, 1, self.SHUTTER_SPEED_MS, self.SHUTTER_SPEED_MS, 2), "SetShutterEx(2)")
393
- self.settings.state.shutter_enabled = False
381
+ self.settings.state.shutter_open = False
394
382
  return SpectrometerResponse(True)
395
383
 
396
384
  def _open_ex_shutter(self):
385
+ # set TTL HIGH="open", internalMode PermanentlyOpen, externalMode PermanentlyOpen
397
386
  self.check_result(self.driver.SetShutterEx(1, 1, self.SHUTTER_SPEED_MS, self.SHUTTER_SPEED_MS, 1), "SetShutterEx(1)")
398
- self.settings.state.shutter_enabled = True
387
+ self.settings.state.shutter_open = True
399
388
  return SpectrometerResponse(True)
400
389
 
401
390
  ###############################################################
@@ -461,8 +450,9 @@ class AndorDevice(InterfaceDevice):
461
450
  self.init_detector_speed() # step 12+13
462
451
 
463
452
  # step 14
453
+ # set internal shutter to PermanentlyOpen, external shutter to Automatic
464
454
  self.check_result(self.driver.SetShutterEx(1, 1, self.SHUTTER_SPEED_MS, self.SHUTTER_SPEED_MS, 0), "SetShutterEx(fully automatic external with internal always open)")
465
- self.settings.state.shutter_enabled = True
455
+ self.settings.state.shutter_open = True
466
456
 
467
457
  # step 15
468
458
  self.set_integration_time_ms(self.settings.eeprom.startup_integration_time_ms)
@@ -560,8 +550,9 @@ class AndorDevice(InterfaceDevice):
560
550
  """
561
551
  Loads configuration from file `self.config_file` and populates `self.settings.eeprom` with members.
562
552
  """
563
- f = open(self.config_file,)
564
- self.config_values = dict(json.load(f))
553
+ with open(self.config_file) as f:
554
+ self.config_values = json.load(f)
555
+
565
556
  log.debug(f"loaded {self.config_file}: {self.config_values}")
566
557
 
567
558
  # handle wp_ prefixes
@@ -570,6 +561,7 @@ class AndorDevice(InterfaceDevice):
570
561
  self.settings.eeprom.multi_wavelength_calibration.set(v, self.config_values[k])
571
562
 
572
563
  # same spelling
564
+ # consider just using self.settings.eeprom.fields.keys()
573
565
  for k in [ 'model',
574
566
  'stubbed',
575
567
  'detector',
@@ -597,17 +589,20 @@ class AndorDevice(InterfaceDevice):
597
589
  if 'startup_temp_degC' in self.config_values:
598
590
  self.set_tec_setpoint(self.settings.eeprom.startup_temp_degC)
599
591
 
592
+ # pick up Pixel Corrections like EtalonCorrection, InGaAsCorrection etc
593
+ self.settings.augment_from_json_data(data)
594
+
600
595
  def acquire_data(self):
601
596
  # handle TakeOneRequest.take_dark
602
597
  dark_reading = None
603
598
  tor = self.take_one_request
604
599
  log.debug(f"acquire_data: tor {tor}")
605
600
  if tor and tor.take_dark:
606
- self.set_shutter_enable(True)
601
+ self.set_shutter_open(False)
607
602
  dark_reading = self._take_one_averaged_reading()
608
- if dark is None:
603
+ if dark_reading is None:
609
604
  return SpectrometerResponse(False, error_msg="failed to collect dark")
610
- self.set_shutter_enable(False)
605
+ self.set_shutter_open(True)
611
606
 
612
607
  # get spectrum (potentially averaged)
613
608
  reading = self._take_one_averaged_reading()
@@ -647,8 +642,8 @@ class AndorDevice(InterfaceDevice):
647
642
  log.debug(f"acquire_data: reading {reading}")
648
643
  return SpectrometerResponse(data=reading)
649
644
 
650
- def set_shutter_enable(self, enable):
651
- if enable:
645
+ def set_shutter_open(self, flag):
646
+ if flag:
652
647
  return self._open_ex_shutter()
653
648
  else:
654
649
  return self._close_ex_shutter()
@@ -696,16 +691,17 @@ class AndorDevice(InterfaceDevice):
696
691
 
697
692
  return SpectrometerResponse(True)
698
693
 
699
- def toggle_tec(self, toggle_state):
700
- c_toggle = c_int(toggle_state)
701
- self.tec_enabled = c_toggle.value
702
- if self.tec_enabled:
694
+ def set_detector_tec_enable(self, flag):
695
+ flag = True if flag else False
696
+ self.tec_enabled = flag
697
+ if flag:
703
698
  self.check_result(self.driver.CoolerON(), "CoolerON")
704
699
  else:
705
700
  self.check_result(self.driver.CoolerOFF(), "CoolerOFF")
706
701
  return SpectrometerResponse(True)
707
702
 
708
703
  def set_tec_setpoint(self, set_temp):
704
+ set_temp = int(round(set_temp, 0))
709
705
  if set_temp < self.settings.eeprom.min_temp_degC or set_temp > self.settings.eeprom.max_temp_degC:
710
706
  log.error(f"requested temp of {set_temp}, but it is outside range ({self.settings.eeprom.min_temp_degC}C, {self.settings.eeprom.max_temp_degC}C)")
711
707
  return
@@ -776,6 +772,14 @@ class AndorDevice(InterfaceDevice):
776
772
  log.debug(f"set AD channel {ADnumber} with horizontal speed {HSnumber} ({STemp})")
777
773
  return SpectrometerResponse(True)
778
774
 
775
+ def set_ingaas_correction_enable(self, flag):
776
+ if self.settings.ingaas_correction:
777
+ self.settings.ingaas_correction.enable = flag
778
+
779
+ def set_etalon_correction_enable(self, flag):
780
+ if self.settings.etalon_correction:
781
+ self.settings.etalon_correction.enable = flag
782
+
779
783
  def set_scans_to_average(self, value):
780
784
  value = int(value)
781
785
 
@@ -0,0 +1,56 @@
1
+ import json
2
+ from . import utils
3
+
4
+ class AssemblyRevision(dict):
5
+ """
6
+ Making this a standalone class because might end up using from both FID (USB)
7
+ and BLEDevice.
8
+ """
9
+ def __init__(self, data=None):
10
+ dict.__init__(self) # https://stackoverflow.com/a/31207881
11
+ self.partnumber = None
12
+ self.variant = None
13
+ self.revision = None
14
+
15
+ if isinstance(data, str):
16
+ data = utils.hex_string_to_data(data)
17
+
18
+ self.parse_data(data)
19
+
20
+ def parse_data(self, data):
21
+ """
22
+ This is based on the data structure defined in ENG-0120 Rev 10.
23
+ """
24
+ if data is None or len(data) != 6:
25
+ log.error(f"parse_data: invalid length {data}")
26
+ return
27
+
28
+ self.partnumber = (data[0] << 8) + data[1] + 140_000
29
+ self.variant = (data[2] << 8) + data[3]
30
+ self.revision = (data[4] << 8) + data[5]
31
+
32
+ def serialize(self):
33
+ """ probably a simpler way to do this with struct """
34
+ buf = [ 0 ] * 6
35
+ if self.partnumber:
36
+ n = self.partnumber - 140_000
37
+ buf[0] = (n >> 8) & 0xff
38
+ buf[1] = n & 0xff
39
+ if self.variant:
40
+ n = self.variant
41
+ buf[2] = (n >> 8) & 0xff
42
+ buf[3] = n & 0xff
43
+ if self.revision:
44
+ n = self.revision
45
+ buf[4] = (n >> 8) & 0xff
46
+ buf[5] = n & 0xff
47
+
48
+ def __repr__(self):
49
+ s = ""
50
+ if self.partnumber and self.partnumber != 140_000:
51
+ s += f"{self.partnumber}"
52
+ if self.variant:
53
+ s += f"-{self.variant}"
54
+ if self.revision:
55
+ s += f" Rev{self.revision}"
56
+ return s