|
ENLIGHTEN
Cross-platform desktop GUI for Wasatch Photonics spectrometers
|
This is the top-level interface for controlling and communicating with Wasatch Photonics USB 2.0 spectrometers using the FeatureInterfaceDevice (FID) protocol as defined in ENG-0001. More...
Public Member Functions | |
| __init__ (self, device_id, message_queue=None, alert_queue=None) | |
| acquire_area_scan (self) | |
| FeatureIdentificationDevice.get_area_scan returns a Reading because it really needs to return both the AreaScanImage (whatever form that may take) and the vertically-binned spectrum (for live display). | |
| acquire_data (self) | |
| Process all enqueued settings, then read actual data (spectrum and temperatures) from the device. | |
| acquire_spectrum (self) | |
| Generate one Reading from the spectrometer, including one optionally-averaged spectrum, device temperatures and other hardware status. | |
| acquire_spectrum_auto_raman (self) | |
| acquire_spectrum_standard (self) | |
| balance_acquisition (self, device=None, mode=None, intensity=45000, threshold=2500, pixel=None, max_integration_time_ms=5000, max_tries=20) | |
| change_setting (self, str setting, Any value, bool allow_immediate=True) | |
| Processes an incoming (setting, value) pair. | |
| connect (self) | |
| Attempt low level connection to the specified DeviceID. | |
| connect_feature_identification (self) | |
| Given a specified universal identifier, attempt to connect to the device using FID protocol. | |
| disconnect (self) | |
| handle_requests (self, list[SpectrometerRequest] requests) | |
| initialize_settings (self) | |
| monitor_memory (self) | |
| perform_optional_throwaways (self) | |
| It's unclear how many throwaways are really needed for a stable Raman spectrum, and whether they're really based on number of integrations (sensor stabilization) or time (laser warmup); I suspect both. | |
| process_commands (self) | |
| Process every entry on the incoming command (settings) queue, writing each to the device. | |
| take_one_averaged_reading (self, label=None) | |
| Okay, let's talk about averaging. | |
Public Member Functions inherited from wasatch.InterfaceDevice.InterfaceDevice | |
| __init__ (self) | |
| Any class that communicates to a spectrometer should inherit this class. | |
Public Attributes | |
| alert_queue = alert_queue | |
| auto_raman = AutoRaman(self) | |
| list | command_queue = [] |
| bool | connected = False |
| device_id = device_id | |
| hardware = None | |
| bool | immediate_mode = False |
| int | last_battery_percentage = 0 |
| last_complete_acquisition = None | |
| last_memory_check = datetime.datetime.now() | |
| lock = threading.Lock() | |
| message_queue = message_queue | |
| process_id = os.getpid() | |
| int | session_reading_count = 0 |
| settings = SpectrometerSettings() | |
| int | sum_count = 0 |
| Aggregate scan averaging. | |
| list | summed_spectra = None |
| Aggregate scan averaging. | |
| take_one_request = None | |
Public Attributes inherited from wasatch.InterfaceDevice.InterfaceDevice | |
| dict | process_f = {} |
| int | remaining_throwaways = 0 |
Protected Member Functions | |
| _init_process_funcs (self) | |
This is the top-level interface for controlling and communicating with Wasatch Photonics USB 2.0 spectrometers using the FeatureInterfaceDevice (FID) protocol as defined in ENG-0001.
This class is essentially a clunky wrapper over FeatureInterfaceDevice, providing the higher-level InterfaceDevice interface making WasatchDevice a peer to AndorDevice, OceanDevice, TCPDevice, IDSDevice etc.
Several points will stand out:
ENLIGHTEN does not instantiate WasatchDevices directly, but instead uses a WasatchDeviceWrapper to access a single WasatchDevice in a dedicated child thread. Other users of Wasatch.PY may of course instantiate a WasatchDevice directly, or go straight to a FeatureInterfaceDevice.
Main things this "wrapper" provides:
This class exists because it used to wrap TWO types of spectrometers:
I don't know if we have an ENG document specifying the protocol that the old Stroker electronics used. It was already deprecated when I started, and was rapidly removed from Wasatch.PY. Which kind of made this class superfluous.
This class (and all who inherit InterfaceDevice) use a somewhat clunky data-passing and error-handling mechanism via SpectrometerRequest and SpectrometerResponse objects. That could probably be streamlined.
Part of the legacy "ugly-isms" in this class date back to when ENLIGHTEN was multi-process rather than multi-threaded, and all data flows between Wasatch.PY and ENLIGHTEN were via pickled queues :-(
| wasatch.WasatchDevice.WasatchDevice.__init__ | ( | self, | |
| device_id, | |||
| message_queue = None, | |||
| alert_queue = None ) |
| device_id | a DeviceID instance OR string label thereof |
| message_queue | if provided, used to send status back to caller |
| alert_queue | if provided, used to receive hints and realtime interrupts from caller |
|
protected |
| wasatch.WasatchDevice.WasatchDevice.acquire_area_scan | ( | self | ) |
FeatureIdentificationDevice.get_area_scan returns a Reading because it really needs to return both the AreaScanImage (whatever form that may take) and the vertically-binned spectrum (for live display).
| wasatch.WasatchDevice.WasatchDevice.acquire_data | ( | self | ) |
Process all enqueued settings, then read actual data (spectrum and temperatures) from the device.
ENLIGHTEN calls this function via WrapperWorker.run().
| wasatch.WasatchDevice.WasatchDevice.acquire_spectrum | ( | self | ) |
Generate one Reading from the spectrometer, including one optionally-averaged spectrum, device temperatures and other hardware status.
This is normally called by self.acquire_data when that function decides it is time to perform an acquisition.
IF the driver is in free-running mode, AND performing scan averaging, THEN scan averaging is NOT encapsulated within a single call to this function. Instead, we let ths spectrometer run in free-running mode, collecting individual spectra as per normal, and returning each "partial" readings while "building up" to the final averaged measurement.
That is to say, if scan averaging is set to 10, then this function will get called 10 times, as ticked by the regular free-running timers, before the fully averaged spectrum is returned. A total of 10 (not 11) spectra will be generated and sent upstream: the first 9 "partial" (unaveraged) reads, and the final 10th spectrum which will contain the average of all 10 measurements.
This gives the user-facing GUI an opportunity to update the "collected X-of-Y" readout on-screen, and potentially even graph the traces of in-process partial readings.
HOWEVER, this doesn't make as much sense if we're not in free-running mode, i.e. the subprocess has been slaved to explicit control by the Controller (likely a feature object like BatchCollection), and is collecting exactly those measurements we're being commanded, as they're commanded.
THEREFORE, if the driver IS NOT in free-running mode, then we ONLY return the final averaged spectrum as one atomic operation.
In this case, if scan averaging is set to 10, then A SINGLE CALL to this function will "block" while the full 10 measurements are made, and then a single, fully-averaged spectrum will be returned upstream.
| wasatch.WasatchDevice.WasatchDevice.acquire_spectrum_auto_raman | ( | self | ) |
| wasatch.WasatchDevice.WasatchDevice.acquire_spectrum_standard | ( | self | ) |
| wasatch.WasatchDevice.WasatchDevice.balance_acquisition | ( | self, | |
| device = None, | |||
| mode = None, | |||
| intensity = 45000, | |||
| threshold = 2500, | |||
| pixel = None, | |||
| max_integration_time_ms = 5000, | |||
| max_tries = 20 ) |
| wasatch.WasatchDevice.WasatchDevice.change_setting | ( | self, | |
| str | setting, | ||
| Any | value, | ||
| bool | allow_immediate = True ) |
Processes an incoming (setting, value) pair.
Some settings are processed internally within this function, if the functionality they are controlling is implemented by WasatchDevice. This includes scan averaging, and anything related to scan averaging (such as "take one" behavior).
Most tuples are queued to be sent downstream to the connected hardware (usually FeatureIdentificationDevice) at the start of the next acquisition.
Some hardware settings (those involving triggering or the laser) are sent downstream immediately, rather than waiting for the next "scheduled" settings update.
ENLIGHTEN commands to WasatchDeviceWrapper are sent here by WasatchDeviceWrapper.continuous_poll.
| setting | (Input) which setting to change |
| value | (Input) the new value of the setting (required, but can be None or "anything" for commands like "acquire" which don't use the argument). |
| allow_immediate |
| wasatch.WasatchDevice.WasatchDevice.connect | ( | self | ) |
Attempt low level connection to the specified DeviceID.
| wasatch.WasatchDevice.WasatchDevice.connect_feature_identification | ( | self | ) |
Given a specified universal identifier, attempt to connect to the device using FID protocol.
| wasatch.WasatchDevice.WasatchDevice.disconnect | ( | self | ) |
| wasatch.WasatchDevice.WasatchDevice.handle_requests | ( | self, | |
| list[SpectrometerRequest] | requests ) |
Reimplemented from wasatch.InterfaceDevice.InterfaceDevice.
| wasatch.WasatchDevice.WasatchDevice.initialize_settings | ( | self | ) |
| wasatch.WasatchDevice.WasatchDevice.monitor_memory | ( | self | ) |
| wasatch.WasatchDevice.WasatchDevice.perform_optional_throwaways | ( | self | ) |
It's unclear how many throwaways are really needed for a stable Raman spectrum, and whether they're really based on number of integrations (sensor stabilization) or time (laser warmup); I suspect both.
Also note the potential need for sensor warm-up, but I think that's handled inside FW.
Optimal would probably be something like "As many integrations as it takes to span 2sec, but not fewer than two."
These are NOT the same throwaways added to smooth spectra over changes to integration time and gain. These are separate throwaways potentially required when waking the sensor from sleep. However, I'm using the same mechanism for tracking (self.remaining_throwaways) for commonality.
| wasatch.WasatchDevice.WasatchDevice.process_commands | ( | self | ) |
Process every entry on the incoming command (settings) queue, writing each to the device.
Essentially this iterates through all the (setting, value) pairs we've received through change_setting() which have not yet been processed, and <– MZ: incomplete sentence
Note that WrapperWorker.run "de-dupes" commands on receipt from ENLIGHTEN, so the command stream arising from that source should already be optimized and minimal. Commands injected manually by calling WasatchDevice.change_setting() do not receive this treatment.
In the normal multithreaded (ENLIGHTEN) workflow, this function is called at the beginning of acquire_data, itself ticked regularly by WrapperWorker.run.
| wasatch.WasatchDevice.WasatchDevice.take_one_averaged_reading | ( | self, | |
| label = None ) |
Okay, let's talk about averaging.
We only perform averaging as a blocking call in Wasatch.PY when given a TakeOneRequest with non-zero scans_to_average, typically from ENLIGHTEN's TakeOneFeature (perhaps via VCRControls.step, possibly with AutoRaman, or BatchCollection).
Otherwise, normally this thread (the background thread owned by a WasatchDeviceWrapper object and running WrapperWorker.run) is in "free- running" mode, taking spectra just as fast as it can in an endless loop, feeding them back to the consumer (ENLIGHTEN) over a queue. To keep that pipeline "moving," generally we don't do heavy blocking operations down here in the background thread.
The new AutoRaman feature makes increased using of scan averaging, and again kind of wants to be encapsulated down here. And by implication, all TakeOneRequests should really support encapsulated, atomic averaging. So the current design is that ATOMIC scans_to_average comes from TakeOneRequest, ENLIGHTEN-based averaging in SpectrometerState.
| wasatch.WasatchDevice.WasatchDevice.alert_queue = alert_queue |
| wasatch.WasatchDevice.WasatchDevice.auto_raman = AutoRaman(self) |
| wasatch.WasatchDevice.WasatchDevice.command_queue = [] |
| bool wasatch.WasatchDevice.WasatchDevice.connected = False |
| wasatch.WasatchDevice.WasatchDevice.device_id = device_id |
| wasatch.WasatchDevice.WasatchDevice.hardware = None |
| wasatch.WasatchDevice.WasatchDevice.immediate_mode = False |
| int wasatch.WasatchDevice.WasatchDevice.last_battery_percentage = 0 |
| wasatch.WasatchDevice.WasatchDevice.last_complete_acquisition = None |
| wasatch.WasatchDevice.WasatchDevice.last_memory_check = datetime.datetime.now() |
| wasatch.WasatchDevice.WasatchDevice.lock = threading.Lock() |
| wasatch.WasatchDevice.WasatchDevice.message_queue = message_queue |
| wasatch.WasatchDevice.WasatchDevice.process_id = os.getpid() |
| int wasatch.WasatchDevice.WasatchDevice.session_reading_count = 0 |
| wasatch.WasatchDevice.WasatchDevice.settings = SpectrometerSettings() |
| int wasatch.WasatchDevice.WasatchDevice.sum_count = 0 |
Aggregate scan averaging.
| wasatch.WasatchDevice.WasatchDevice.summed_spectra = None |
Aggregate scan averaging.
| wasatch.WasatchDevice.WasatchDevice.take_one_request = None |