ENLIGHTEN
Cross-platform desktop GUI for Wasatch Photonics spectrometers
Loading...
Searching...
No Matches
wasatch.WasatchDevice.WasatchDevice Class Reference

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...

Inheritance diagram for wasatch.WasatchDevice.WasatchDevice:
[legend]
Collaboration diagram for wasatch.WasatchDevice.WasatchDevice:
[legend]

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)
 

Detailed Description

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:

  1. WasatchDevice should probably be renamed to FIDDevice, because Wasatch Photonics is also the manufacturer for spectrometers using AndorDevice, TCPDevice, IDSDevice etc.
  2. This class could be simplified considerably, and possibly merged with FeatureIdentificationDevice.

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:

  • Implements software-based, library-based scan averaging (badly). Although ARM-based spectrometers can do this in firmware, FX2-based models have to do it in software. Between Wasatch.PY and ENLIGHTEN, it seems preferable to have this in the Wasatch.PY library, where it is easier for customers to use.
  • Distinguishes between normal, averaged, AutoRaman and AreaScan
    acquisitions.
History

This class exists because it used to wrap TWO types of spectrometers:

  • FeatureIdentificationDevices, which follow the ENG-0001 API and have EEPROMs
  • StrokerDevices, which didn't have EEPROMs and didn't obey ENG-0001.

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 :-(

Constructor & Destructor Documentation

◆ __init__()

wasatch.WasatchDevice.WasatchDevice.__init__ ( self,
device_id,
message_queue = None,
alert_queue = None )
Parameters
device_ida DeviceID instance OR string label thereof
message_queueif provided, used to send status back to caller
alert_queueif provided, used to receive hints and realtime interrupts from caller

Member Function Documentation

◆ _init_process_funcs()

wasatch.WasatchDevice.WasatchDevice._init_process_funcs ( self)
protected

◆ acquire_area_scan()

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).

Returns
a SpectrometerResponse(data=Reading)

◆ acquire_data()

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().

See also
Controller.acquire_reading

◆ acquire_spectrum()

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.

Scan Averaging

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.

Returns
a Reading wrapped in a SpectrometerResponse
a SpectrometerResponse(data=Reading)

◆ acquire_spectrum_auto_raman()

wasatch.WasatchDevice.WasatchDevice.acquire_spectrum_auto_raman ( self)
Returns
a SpectrometerResponse(data=Reading)
Todo
fold-in a lot of the post-reading sensor measurements (temperature, interlock etc) provided by acquire_spectrum_standard

◆ acquire_spectrum_standard()

wasatch.WasatchDevice.WasatchDevice.acquire_spectrum_standard ( self)
Returns
a SpectrometerResponse(data=Reading)

◆ balance_acquisition()

wasatch.WasatchDevice.WasatchDevice.balance_acquisition ( self,
device = None,
mode = None,
intensity = 45000,
threshold = 2500,
pixel = None,
max_integration_time_ms = 5000,
max_tries = 20 )

◆ change_setting()

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.

Parameters
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

◆ connect()

wasatch.WasatchDevice.WasatchDevice.connect ( self)

Attempt low level connection to the specified DeviceID.

◆ connect_feature_identification()

wasatch.WasatchDevice.WasatchDevice.connect_feature_identification ( self)

Given a specified universal identifier, attempt to connect to the device using FID protocol.

Todo
merge with the hardcoded list in DeviceFinderUSB

◆ disconnect()

wasatch.WasatchDevice.WasatchDevice.disconnect ( self)

◆ handle_requests()

wasatch.WasatchDevice.WasatchDevice.handle_requests ( self,
list[SpectrometerRequest] requests )

◆ initialize_settings()

wasatch.WasatchDevice.WasatchDevice.initialize_settings ( self)

◆ monitor_memory()

wasatch.WasatchDevice.WasatchDevice.monitor_memory ( self)

◆ perform_optional_throwaways()

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.

Todo
This shouldn't be required at all if we're in free-running mode, or if it's been less than a second since the last acquisition.
Returns
SpectrometerResponse IFF error occurred (normally None)

◆ process_commands()

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.

◆ take_one_averaged_reading()

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.

Returns
Reading on success, true or false on "stop processing" conditions

Member Data Documentation

◆ alert_queue

wasatch.WasatchDevice.WasatchDevice.alert_queue = alert_queue

◆ auto_raman

wasatch.WasatchDevice.WasatchDevice.auto_raman = AutoRaman(self)

◆ command_queue

wasatch.WasatchDevice.WasatchDevice.command_queue = []

◆ connected

bool wasatch.WasatchDevice.WasatchDevice.connected = False

◆ device_id

wasatch.WasatchDevice.WasatchDevice.device_id = device_id

◆ hardware

wasatch.WasatchDevice.WasatchDevice.hardware = None

◆ immediate_mode

wasatch.WasatchDevice.WasatchDevice.immediate_mode = False

◆ last_battery_percentage

int wasatch.WasatchDevice.WasatchDevice.last_battery_percentage = 0

◆ last_complete_acquisition

wasatch.WasatchDevice.WasatchDevice.last_complete_acquisition = None

◆ last_memory_check

wasatch.WasatchDevice.WasatchDevice.last_memory_check = datetime.datetime.now()

◆ lock

wasatch.WasatchDevice.WasatchDevice.lock = threading.Lock()

◆ message_queue

wasatch.WasatchDevice.WasatchDevice.message_queue = message_queue

◆ process_id

wasatch.WasatchDevice.WasatchDevice.process_id = os.getpid()

◆ session_reading_count

int wasatch.WasatchDevice.WasatchDevice.session_reading_count = 0

◆ settings

wasatch.WasatchDevice.WasatchDevice.settings = SpectrometerSettings()

◆ sum_count

int wasatch.WasatchDevice.WasatchDevice.sum_count = 0

Aggregate scan averaging.

◆ summed_spectra

wasatch.WasatchDevice.WasatchDevice.summed_spectra = None

Aggregate scan averaging.

◆ take_one_request

wasatch.WasatchDevice.WasatchDevice.take_one_request = None

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