Wasatch.PY
Python application driver for Wasatch Photonics spectrometers
Loading...
Searching...
No Matches
wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper Class Reference

Wrap WasatchDevice in a non-blocking interface run in a separate thread, using multiprocess.pipes to exchange data (SpectrometerSettings, Readings and StatusMessages) for multiprocessing-safe device communications and acquisition under Windows and Qt. More...

Public Member Functions

 __init__ (self, device_id, log_level, callback=None)
 Instantiated by Controller.connect_new(), if-and-only-if a WasatchBus reports a DeviceID which has not already connected to the GUI.
 
 acquire_data (self, mode=None)
 This method is called by the Controller.
 
 acquire_status_message (self)
 Similar to acquire_data, this method is called by the Controller in MainProcess to dequeue a StatusMessage from the spectrometer child thread, if one is available.
 
 change_setting (self, setting, value=None)
 Add the specified setting and value to the local control queue.
 
 connect (self)
 Create a low level device object with the specified identifier, kick off the child thread to attempt to read from it.
 
 disconnect (self)
 
 get_final_item (self, keep_averaged=False)
 Read from the response queue until empty (or we find an averaged item)
 
 poll_settings (self)
 
 reset (self)
 
 wait_for_settings (self)
 

Public Attributes

 callback
 
 closing
 
 command_queue
 
 connect_start_time
 
 connected
 
 device_id
 
 is_andor
 
 is_ble
 
 is_ocean
 
 is_spi
 
 log_level
 
 message_queue
 
 mock
 
 poller
 
 previous_reading
 
 reset_tries
 
 response_queue
 
 settings
 
 settings_queue
 
 wrapper_worker
 

Static Public Attributes

int ACQUISITION_MODE_KEEP_ALL = 0
 
int ACQUISITION_MODE_KEEP_COMPLETE = 2
 
int ACQUISITION_MODE_LATEST = 1
 
bool DISABLE_RESPONSE_QUEUE = False
 

Detailed Description

Wrap WasatchDevice in a non-blocking interface run in a separate thread, using multiprocess.pipes to exchange data (SpectrometerSettings, Readings and StatusMessages) for multiprocessing-safe device communications and acquisition under Windows and Qt.

Todo
This document is full of references to continuous_poll(). That method no longer exists, and has been replaced by WrapperWorker.run()
Lifecycle

From ENLIGHTEN's standpoint (the original Wasatch.PY caller), here's what's going on:

  • MainProcess (enlighten.Controller.setup_bus_listener) instantiates a wasatch.WasatchBus (bus) which will be persistent through the application lifetime. setup_bus_listener also creates a QTimer (bus_timer) which will check the USB bus for newly-connected ("hotplug") devices every second or so.
  • Controller.tick_bus_listener
    • does nothing (silently reschedules itself) if any new spectrometers are actively in the process of connecting, because things get hairy if we're trying to enumerate and configure several spectrometers at once
    • calls bus.update(), which will internally instantiate and use a wasatch.DeviceFinderUSB to scan the current USB bus and update its internal list of all Wasatch spectrometers (whether already connected or otherwise)
    • then calls Controller.connect_new() to process the updated device list (including determining whether any new devices are visible, and if so what to do about them)
  • Controller.connect_new()
    • if there is at least one new spectrometer on the device list, pull off that ONE device for connection (don't iterate over multiple new devices...we'll get them on a subsequent bus tick).
    • instantiates a WasatchDeviceWrapper and then calls connect() on it
    • WasatchDeviceWrapper.connect()
      • spawns a thread running the continuous_poll() method of the same WasatchDeviceWrapper instance
        • the WDW in the MainProcess then waits (blocks) while waiting for a single SpectrometerSettings object to be returned via a pipe from the child thread. This doesn't block the GUI, because this whole sequence is occuring in a background tick event on the bus_timer QTimer.
        • the WDW in the child threadis running continuous_poll()

Controller.connect_new() then completes the initialization of the spectrometer in the GUI by calling Controller.initialize_new_device(), which adds the spectrometer to Multispec, updates the EEPROMEditor, defaults the TEC controls and so on.

  • In the background, WasatchDeviceWrapper.continuous_poll() continues running, acting as a "free-running mode driver" of the spectrometer, passing down new commands from the enlighten.Controller, and feeding back Readings or StatusMessages when they appear.
Shutdown

The whole thing can be shutdown in various ways, using the concept of "poison pill" messages which can be flowed either upstream or downstream:

  • if a hardware error occurs in the spectrometer thread, it sends a poison pill upstream (a False value where a Reading is expected), then self-destructs (Controller is expected to drop the spectrometer from the GUI)
  • if the GUI is closing, poison-pills (None values where ControlObjects are expected) are sent downstream to each spectrometer thread, telling them to terminate themselves
Throughput Considerations

It is important to recognize that continuous_poll() updates the command/response pipes at a relatively leisurely interval (currently 20Hz, set in POLLER_WAIT_SEC). No matter how short the integration time is (1ms), you're not going to get spectra faster than 20 per second through this (as ENLIGHTEN was designed as a real-time visualization tool, not a high-speed data collection tool).

Now, if you set ACQUISITION_MODE_KEEP_ALL, then you should still get every spectrum (whatever the spectrometer achieved through its scan-rate, potentially 220/sec or so) – but you'll get them in chunks (e.g., scan rate of 220/sec polled at 20Hz = 20 chunks of 11 ea).

Responsiveness

With regard to the "immediacy" of commands like laser_enable, note that spectrometer threads are internally single-threaded: the continuous_poll() function BLOCKS on WasatchDevice.acquire_data(), meaning that even if new laser_enable commands get pushed into the downstream command pipe, continuous_poll won't check for them until the end of the current acquisition.

What we'd really like is two threads running in the child, one handling acquisitions and most commands, and another handling high-priority events like laser_enable. I'm not sure the pipes are designed for multiple readers, but a SEPARATE pipe (or queue) could be setup for high-priority commands, and EXCLUSIVELY read by the secondary thread. That just leaves open the question of synchronization on WasatchDevice's USBDevice.

Constructor & Destructor Documentation

◆ __init__()

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.__init__ ( self,
device_id,
log_level,
callback = None )

Instantiated by Controller.connect_new(), if-and-only-if a WasatchBus reports a DeviceID which has not already connected to the GUI.

The DeviceID is "unique and relevant" to the bus which reported it, but neither the bus class nor instance is passed in to this object. If the DeviceID looks like "USB:VID:PID:bus:addr", then it is presumably USB. Future DeviceID formats could include "FILE:/path/to/dir", etc. However, device_id is just a string scalar to this class, and actually parsing / using it should be entirely encapsulated within WasatchDevice and lower using DeviceID.

Member Function Documentation

◆ acquire_data()

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.acquire_data ( self,
mode = None )

This method is called by the Controller.

It checks the response_queue it shares with the child thread to see if any Reading objects have been queued from the spectrometer to the GUI.

It is the upstream interface's job to decide how to process the potentially voluminous amount of data returned from the device. get_last by default will make sure the queue is cleared, then return the most recent reading from the device.

Note
it is not clear that measurement modes other than ACQUISITION_MODE_KEEP_COMPLETE have been well-tested, especially in the context of multiple spectrometers, BatchCollection etc.

◆ acquire_status_message()

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.acquire_status_message ( self)

Similar to acquire_data, this method is called by the Controller in MainProcess to dequeue a StatusMessage from the spectrometer child thread, if one is available.

◆ change_setting()

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.change_setting ( self,
setting,
value = None )

Add the specified setting and value to the local control queue.

In ENLIGHTEN, this is called by MainProcess.Controller.

For OEM customers controlling the spectrometer via the non-blocking WasatchDeviceWrapper interface, this is the method you would call to change the various spectrometer settings.

See also
README_SETTINGS.md for a list of valid settings you can pass, as well as any parameters expected by each

◆ connect()

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.connect ( self)

Create a low level device object with the specified identifier, kick off the child thread to attempt to read from it.

Called by Controller.connect_new() immediately after instantiation

Spawn a child thread running the continuous_poll() method on THIS object instance.

The two threads are coupled via the 3 queues:

settings_queue

Lets the child (WasatchDevice) send a single, one-time copy of a populated SpectrometerSettings object back through the Wrapper to the calling Controller. This is the primary way the Controller knows what kind of spectrometer it has connected to, what hardware features and EEPROM settings are applied etc.

Thereafter both WasatchDevice and Controller will maintain their own copies of SpectrometerSettings, and they are not automatically synchronized (is there a clever way to do this?). They may well drift out-of-sync regarding state, although the command_queue helps keep them somewhat in sync.

command_queue

The Controller will send ControlObject instances (basically (name, value) pairs) through the Wrapper to the WasatchDevice to set attributes or commands on the WasatchDevice spectrometer. These may be volatile hardware settings (laser power, integration time), meta-commands to the WasatchDevice class (scan averaging), or EEPROM updates.

response_queue

The WasatchDevice will stream a continuous series of Reading instances back through the Wrapper to the Controller. These each contain a newly read spectrum, as well as metadata about the spectrometer at the time the spectrum was taken (integration time, laser power), plus additional readings from the spectrometer (detector and laser temperature, secondary ADC).

◆ disconnect()

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.disconnect ( self)

◆ get_final_item()

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.get_final_item ( self,
keep_averaged = False )

Read from the response queue until empty (or we find an averaged item)

In the currently implementation, it seems unlikely that a "True" will ever be passed up (we're basically converting them to None here).

◆ poll_settings()

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.poll_settings ( self)
 @returns SpectrometerResponse(True) on success, (False) otherwise 

◆ reset()

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.reset ( self)

◆ wait_for_settings()

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.wait_for_settings ( self)

Member Data Documentation

◆ ACQUISITION_MODE_KEEP_ALL

int wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.ACQUISITION_MODE_KEEP_ALL = 0
static

◆ ACQUISITION_MODE_KEEP_COMPLETE

int wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.ACQUISITION_MODE_KEEP_COMPLETE = 2
static

◆ ACQUISITION_MODE_LATEST

int wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.ACQUISITION_MODE_LATEST = 1
static

◆ callback

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.callback

◆ closing

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.closing

◆ command_queue

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.command_queue

◆ connect_start_time

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.connect_start_time

◆ connected

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.connected

◆ device_id

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.device_id

◆ DISABLE_RESPONSE_QUEUE

bool wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.DISABLE_RESPONSE_QUEUE = False
static

◆ is_andor

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.is_andor

◆ is_ble

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.is_ble

◆ is_ocean

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.is_ocean

◆ is_spi

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.is_spi

◆ log_level

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.log_level

◆ message_queue

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.message_queue

◆ mock

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.mock

◆ poller

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.poller

◆ previous_reading

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.previous_reading

◆ reset_tries

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.reset_tries

◆ response_queue

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.response_queue

◆ settings

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.settings

◆ settings_queue

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.settings_queue

◆ wrapper_worker

wasatch.WasatchDeviceWrapper.WasatchDeviceWrapper.wrapper_worker

The documentation for this class was generated from the following file: