From 10bb24866a699f1aa7c4b6acf0e14c0c14122d55 Mon Sep 17 00:00:00 2001 From: You Yan Date: Sat, 31 May 2025 23:55:22 -0700 Subject: [PATCH 01/10] update config file --- .../configuration_Squid+_Kinetix_LDI_XLight_Xeryon.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/software/configurations/configuration_Squid+_Kinetix_LDI_XLight_Xeryon.ini b/software/configurations/configuration_Squid+_Kinetix_LDI_XLight_Xeryon.ini index b47b52473..436df6cdb 100644 --- a/software/configurations/configuration_Squid+_Kinetix_LDI_XLight_Xeryon.ini +++ b/software/configurations/configuration_Squid+_Kinetix_LDI_XLight_Xeryon.ini @@ -137,6 +137,8 @@ trackers = ["csrt", "kcf", "mil", "tld", "medianflow", "mosse", "daSiamRPN"] tracking_show_microscope_configurations = False _tracking_show_microscope_configurations_options = [True, False] +tube_lens_mm = 180 + wellplate_format = 384 _wellplate_format_options = [1536, 384, 96, 24, 12, 6] x_mm_384_wellplate_upperleft = 12.41 From 302bf70aa289096aa157e9c3d51cbd166a2a45e3 Mon Sep 17 00:00:00 2001 From: You Yan Date: Sat, 31 May 2025 23:58:41 -0700 Subject: [PATCH 02/10] change naming to better organize light source files --- software/control/gui_hcs.py | 4 ++-- software/control/{celesta.py => lighting_celesta.py} | 0 software/control/microscope.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename software/control/{celesta.py => lighting_celesta.py} (100%) diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index d0db15480..3cb4d7307 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -428,9 +428,9 @@ def loadHardwareObjects(self): if USE_CELESTA_ETHENET_CONTROL: try: - import control.celesta + import control.lighting_celesta - self.celesta = control.celesta.CELESTA() + self.celesta = control.lighting_celesta.CELESTA() self.illuminationController = IlluminationController( self.microcontroller, IntensityControlMode.Software, diff --git a/software/control/celesta.py b/software/control/lighting_celesta.py similarity index 100% rename from software/control/celesta.py rename to software/control/lighting_celesta.py diff --git a/software/control/microscope.py b/software/control/microscope.py index a922cd11b..5577af2e1 100644 --- a/software/control/microscope.py +++ b/software/control/microscope.py @@ -260,9 +260,9 @@ def initialize_peripherals(self): if USE_CELESTA_ETHENET_CONTROL: try: - import control.celesta + import control.lighting_celesta - self.celesta = control.celesta.CELESTA() + self.celesta = control.lighting_celesta.CELESTA() self.illuminationController = IlluminationController( self.microcontroller, IntensityControlMode.Software, From 3ee5035db1f44755216bc10887b9fb0a383a98ea Mon Sep 17 00:00:00 2001 From: You Yan Date: Sun, 1 Jun 2025 00:20:29 -0700 Subject: [PATCH 03/10] initial commit --- software/control/lighting_versalase.py | 338 +++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 software/control/lighting_versalase.py diff --git a/software/control/lighting_versalase.py b/software/control/lighting_versalase.py new file mode 100644 index 000000000..4ead50eca --- /dev/null +++ b/software/control/lighting_versalase.py @@ -0,0 +1,338 @@ +""" +https://github.com/jmtayloruk/scripts/blob/main/versalase-usb-comms-demonstration.ipynb +""" + +import usb.core +import sys, time + +from squid.abc import LightSource +from control.lighting import ShutterControlMode, IntensityControlMode + +import squid.logging + + +class VersaLase(LightSource): + """ + Controls a Stradus VersaLase laser system via USB. + + The VersaLase can control up to 4 laser channels (a, b, c, d) with + individual power and shutter control for each channel. + """ + + def __init__(self, vendor_id=0x201A, product_id=0x0003, **kwds): + """ + Initialize the VersaLase controller and establish USB communication. + + Args: + vendor_id: USB vendor ID (default: 0x201a) + product_id: USB product ID (default: 0x0003) + """ + self._log = squid.logging.get_logger(__name__) + + self.vendor_id = vendor_id + self.product_id = product_id + self.dev = None + self.live = False + self.laser_channels = ["a", "b", "c", "d"] + self.active_channels = {} + self.channel_info = {} + self.intensity_mode = IntensityControlMode.Software + self.shutter_mode = ShutterControlMode.Software + + # Channel mapping for common wavelengths + self.wavelength_to_channel = {405: "d", 488: "c", 490: "c", 561: "b", 640: "a", 638: "a"} + + try: + self.initialize() + except Exception as e: + self._log.error(f"Failed to initialize VersaLase: {e}") + + def initialize(self): + """ + Initialize the connection and settings for the VersaLase. + Returns True if successful, False otherwise. + """ + try: + # Find and connect to the device + self.dev = usb.core.find(idVendor=self.vendor_id, idProduct=self.product_id) + if self.dev is None: + raise ValueError("VersaLase device not found") + + self._log.info("Connected to VersaLase") + + # Query information about installed lasers + for channel in self.laser_channels: + laser_info = self._send_command(f"{channel}.?li") + if laser_info is not None: + # This laser is installed + self.active_channels[channel] = True + self.channel_info[channel] = { + "info": laser_info, + "wavelength": self._parse_float_query(f"{channel}.?lw"), + "max_power": self._parse_float_query(f"{channel}.?maxp"), + "rated_power": self._parse_float_query(f"{channel}.?rp"), + } + self._log.info(f"Found laser {channel}: {laser_info}") + + # Initialize laser to safe state + self._send_command(f"{channel}.le=0") # Turn off + self._send_command(f"{channel}.epc=0") # Disable external power control + self._send_command(f"{channel}.lp=0") # Set power to 0 + else: + self.active_channels[channel] = False + + self.live = True + return True + + except Exception as e: + self._log.error(f"Initialization failed: {e}") + self.live = False + return False + + def _get_a1_response(self, timeout=2.0, min_length=2, timeout_acceptable=False): + """Read response from A1 control transfer.""" + t0 = time.time() + did_find = None + first_seen = None + + while time.time() < t0 + timeout: + result = self.dev.ctrl_transfer(0xC0, 0xA1, 0x0000, 0, 256) + sret = "".join([chr(x) for x in result]) + if len(sret) > min_length: + return sret + elif len(sret) > 0: + if first_seen is None: + first_seen = time.time() - t0 + did_find = sret + time.sleep(0.01) + + if timeout_acceptable: + return "" + + self._log.debug("Read timed out") + if did_find is not None: + self._log.debug(f"Did see '{did_find}' after {first_seen:.3f}s") + raise TimeoutError("A1 response timeout") + + def _send_a0_text_command(self, cmd): + """Send text command via A0 control transfer.""" + self.dev.ctrl_transfer(0x40, 0xA0, 0x0000, 0, cmd + "\r") + + def _get_a2(self): + """Read status from A2 control transfer.""" + result = self.dev.ctrl_transfer(0xC0, 0xA2, 0x0000, 0, 1) + return result[0] if len(result) == 1 else 0 + + def _send_a3(self): + """Send acknowledgment via A3 control transfer.""" + self.dev.ctrl_transfer(0x40, 0xA3, 0x0000, 0, 0) + + def _send_command(self, cmd, log_level=0): + """ + Send a text command to the laser and receive the response. + + Args: + cmd: Command string to send + log_level: Logging verbosity (0=quiet, 1=normal, 2=verbose) + + Returns: + Response string or None + """ + if not self.live: + return None + + result = None + + try: + # Send command + self._send_a0_text_command(cmd) + + # Initial A1 query (may be empty) + resp = self._get_a1_response(min_length=0, timeout=0.5, timeout_acceptable=True) + + # Wait for response to be available + t0 = time.time() + initially_zero = False + while self._get_a2() == 0: + initially_zero = True + if time.time() > t0 + 5: + self._log.debug("A2 never returned 1") + break + + # Read all available responses + while self._get_a2() == 1: + resp = self._get_a1_response(min_length=0)[2:] # Skip \r\n + if resp and resp != "Stradus> ": + result = resp + self._send_a3() # Acknowledge + + if log_level >= 1: + self._log.info(f"Sent {cmd}, got response '{result}'") + + return result + + except Exception as e: + self._log.error(f"Command failed: {cmd}, error: {e}") + return None + + def _parse_query(self, cmd): + """Send query command and parse response.""" + response = self._send_command(cmd) + if response: + return response[len(cmd) + 1 :] + return None + + def _parse_float_query(self, cmd): + """Send query command and parse response as float.""" + result = self._parse_query(cmd) + return float(result) if result else 0.0 + + def _parse_int_query(self, cmd): + """Send query command and parse response as int.""" + result = self._parse_query(cmd) + return int(result) if result else 0 + + def _get_channel_for_wavelength(self, wavelength): + """Map wavelength to channel if using wavelength-based addressing.""" + if isinstance(wavelength, (int, float)): + return self.wavelength_to_channel.get(int(wavelength)) + return wavelength # Assume it's already a channel letter + + def set_intensity_control_mode(self, mode): + """ + Set intensity control mode. + + Args: + mode: IntensityControlMode.Software or IntensityControlMode.External + """ + self.intensity_mode = mode + epc_value = 1 if mode == IntensityControlMode.External else 0 + + for channel in self.active_channels: + if self.active_channels[channel]: + self._send_command(f"{channel}.epc={epc_value}") + + def get_intensity_control_mode(self): + """ + Get current intensity control mode. + + Returns: + IntensityControlMode enum value + """ + return self.intensity_mode + + def set_shutter_control_mode(self, mode): + """ + Set shutter control mode. + + Args: + mode: ShutterControlMode enum + """ + self.shutter_mode = mode + # VersaLase doesn't have explicit TTL shutter control in the protocol shown + # This would need to be implemented if the hardware supports it + + def get_shutter_control_mode(self): + """ + Get current shutter control mode. + + Returns: + ShutterControlMode enum value + """ + return self.shutter_mode + + def set_shutter_state(self, channel, state): + """ + Turn a specific channel on or off. + + Args: + channel: Channel ID (letter or wavelength) + state: True to turn on, False to turn off + """ + channel = self._get_channel_for_wavelength(channel) + if channel and channel in self.active_channels and self.active_channels[channel]: + le_value = 1 if state else 0 + response = self._send_command(f"{channel}.le={le_value}") + if response: + self._log.info(f"Set channel {channel} shutter to {state}") + + def get_shutter_state(self, channel): + """ + Get the current shutter state of a specific channel. + + Args: + channel: Channel ID (letter or wavelength) + + Returns: + bool: True if channel is on, False if off + """ + channel = self._get_channel_for_wavelength(channel) + if channel and channel in self.active_channels and self.active_channels[channel]: + return self._parse_int_query(f"{channel}.?le") == 1 + return False + + def set_intensity(self, channel, intensity): + """ + Set the intensity for a specific channel. + + Args: + channel: Channel ID (letter or wavelength) + intensity: Intensity value (0-100 percent) + """ + channel = self._get_channel_for_wavelength(channel) + if channel and channel in self.active_channels and self.active_channels[channel]: + # Convert percentage to power in mW + max_power = self.channel_info[channel]["max_power"] + power_mw = (intensity / 100.0) * max_power + + response = self._send_command(f"{channel}.lp={power_mw:.2f}") + if response: + self._log.info(f"Set channel {channel} intensity to {intensity}% ({power_mw:.2f}mW)") + + def get_intensity(self, channel): + """ + Get the current intensity of a specific channel. + + Args: + channel: Channel ID (letter or wavelength) + + Returns: + float: Current intensity value (0-100 percent) + """ + channel = self._get_channel_for_wavelength(channel) + if channel and channel in self.active_channels and self.active_channels[channel]: + # Get the set power (lps) rather than measured power (lp) + # as measured power might be 0 if shutter is closed + power_mw = self._parse_float_query(f"{channel}.?lps") + max_power = self.channel_info[channel]["max_power"] + if max_power > 0: + return (power_mw / max_power) * 100.0 + return 0.0 + + def shut_down(self): + """Safely shut down the VersaLase.""" + if self.live: + self._log.info("Shutting down VersaLase") + for channel in self.active_channels: + if self.active_channels[channel]: + self.set_intensity(channel, 0) + self.set_shutter_state(channel, False) + self.live = False + + def get_status(self): + """ + Get the status of the VersaLase. + + Returns: + bool: True if connected and operational + """ + return self.live + + def get_channel_info(self): + """ + Get information about all active channels. + + Returns: + dict: Channel information including wavelength and power limits + """ + return self.channel_info From f24e1be26b6454ae34c9aa8a6671a2a9bc3e6dcc Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 2 Jun 2025 22:47:35 -0700 Subject: [PATCH 04/10] product id --- software/control/lighting_versalase.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/software/control/lighting_versalase.py b/software/control/lighting_versalase.py index 4ead50eca..ea2b2c4c3 100644 --- a/software/control/lighting_versalase.py +++ b/software/control/lighting_versalase.py @@ -19,13 +19,13 @@ class VersaLase(LightSource): individual power and shutter control for each channel. """ - def __init__(self, vendor_id=0x201A, product_id=0x0003, **kwds): + def __init__(self, vendor_id=0x201A, product_id=0x1001, **kwds): """ Initialize the VersaLase controller and establish USB communication. Args: vendor_id: USB vendor ID (default: 0x201a) - product_id: USB product ID (default: 0x0003) + product_id: USB product ID (default: 0x1001) """ self._log = squid.logging.get_logger(__name__) @@ -53,7 +53,6 @@ def initialize(self): Returns True if successful, False otherwise. """ try: - # Find and connect to the device self.dev = usb.core.find(idVendor=self.vendor_id, idProduct=self.product_id) if self.dev is None: raise ValueError("VersaLase device not found") From 15829a5a865a0c9e9a89928b041aa7353dfe25e9 Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 16 Jun 2025 05:05:51 -0700 Subject: [PATCH 05/10] temp --- software/control/lighting_versalase.py | 215 +++++-------------------- 1 file changed, 41 insertions(+), 174 deletions(-) diff --git a/software/control/lighting_versalase.py b/software/control/lighting_versalase.py index ea2b2c4c3..ef8bb2f28 100644 --- a/software/control/lighting_versalase.py +++ b/software/control/lighting_versalase.py @@ -1,9 +1,6 @@ -""" -https://github.com/jmtayloruk/scripts/blob/main/versalase-usb-comms-demonstration.ipynb -""" +import time -import usb.core -import sys, time +from laser_sdk import LaserSDK from squid.abc import LightSource from control.lighting import ShutterControlMode, IntensityControlMode @@ -12,35 +9,29 @@ class VersaLase(LightSource): - """ - Controls a Stradus VersaLase laser system via USB. - - The VersaLase can control up to 4 laser channels (a, b, c, d) with - individual power and shutter control for each channel. - """ - - def __init__(self, vendor_id=0x201A, product_id=0x1001, **kwds): - """ - Initialize the VersaLase controller and establish USB communication. - - Args: - vendor_id: USB vendor ID (default: 0x201a) - product_id: USB product ID (default: 0x1001) - """ + def __init__(self, **kwds): self._log = squid.logging.get_logger(__name__) - self.vendor_id = vendor_id - self.product_id = product_id - self.dev = None - self.live = False - self.laser_channels = ["a", "b", "c", "d"] - self.active_channels = {} - self.channel_info = {} + self.sdk = LaserSDK() + self.sdk.discover() + self.intensity_mode = IntensityControlMode.Software self.shutter_mode = ShutterControlMode.Software - # Channel mapping for common wavelengths - self.wavelength_to_channel = {405: "d", 488: "c", 490: "c", 561: "b", 640: "a", 638: "a"} + self.channel_mappings = { + 405: None, + 470: None, + 488: None, + 545: None, + 550: None, + 555: None, + 561: None, + 638: None, + 640: None, + 730: None, + 735: None, + 750: None, + } try: self.initialize() @@ -53,150 +44,30 @@ def initialize(self): Returns True if successful, False otherwise. """ try: - self.dev = usb.core.find(idVendor=self.vendor_id, idProduct=self.product_id) - if self.dev is None: - raise ValueError("VersaLase device not found") - - self._log.info("Connected to VersaLase") - # Query information about installed lasers - for channel in self.laser_channels: - laser_info = self._send_command(f"{channel}.?li") - if laser_info is not None: - # This laser is installed - self.active_channels[channel] = True - self.channel_info[channel] = { - "info": laser_info, - "wavelength": self._parse_float_query(f"{channel}.?lw"), - "max_power": self._parse_float_query(f"{channel}.?maxp"), - "rated_power": self._parse_float_query(f"{channel}.?rp"), - } - self._log.info(f"Found laser {channel}: {laser_info}") - - # Initialize laser to safe state - self._send_command(f"{channel}.le=0") # Turn off - self._send_command(f"{channel}.epc=0") # Disable external power control - self._send_command(f"{channel}.lp=0") # Set power to 0 - else: - self.active_channels[channel] = False - - self.live = True + for laser in self.sdk.get_lasers(): + self.wavelength_to_laser[laser.wavelength] = laser + self._log.info(f"Found laser {laser.wavelength}: {laser.max_power}") + laser.disable() + if laser.wavelength == 405: + self.channel_mappings[405] = laser.id + elif laser.wavelength == 488: + self.channel_mappings[470] = laser.id + self.channel_mappings[488] = laser.id + elif laser.wavelength == 545: + self.channel_mappings[545] = laser.id + self.channel_mappings[550] = laser.id + self.channel_mappings[555] = laser.id + self.channel_mappings[561] = laser.id + elif laser.wavelength == 638: + self.channel_mappings[638] = laser.id + self.channel_mappings[640] = laser.id return True except Exception as e: self._log.error(f"Initialization failed: {e}") - self.live = False return False - def _get_a1_response(self, timeout=2.0, min_length=2, timeout_acceptable=False): - """Read response from A1 control transfer.""" - t0 = time.time() - did_find = None - first_seen = None - - while time.time() < t0 + timeout: - result = self.dev.ctrl_transfer(0xC0, 0xA1, 0x0000, 0, 256) - sret = "".join([chr(x) for x in result]) - if len(sret) > min_length: - return sret - elif len(sret) > 0: - if first_seen is None: - first_seen = time.time() - t0 - did_find = sret - time.sleep(0.01) - - if timeout_acceptable: - return "" - - self._log.debug("Read timed out") - if did_find is not None: - self._log.debug(f"Did see '{did_find}' after {first_seen:.3f}s") - raise TimeoutError("A1 response timeout") - - def _send_a0_text_command(self, cmd): - """Send text command via A0 control transfer.""" - self.dev.ctrl_transfer(0x40, 0xA0, 0x0000, 0, cmd + "\r") - - def _get_a2(self): - """Read status from A2 control transfer.""" - result = self.dev.ctrl_transfer(0xC0, 0xA2, 0x0000, 0, 1) - return result[0] if len(result) == 1 else 0 - - def _send_a3(self): - """Send acknowledgment via A3 control transfer.""" - self.dev.ctrl_transfer(0x40, 0xA3, 0x0000, 0, 0) - - def _send_command(self, cmd, log_level=0): - """ - Send a text command to the laser and receive the response. - - Args: - cmd: Command string to send - log_level: Logging verbosity (0=quiet, 1=normal, 2=verbose) - - Returns: - Response string or None - """ - if not self.live: - return None - - result = None - - try: - # Send command - self._send_a0_text_command(cmd) - - # Initial A1 query (may be empty) - resp = self._get_a1_response(min_length=0, timeout=0.5, timeout_acceptable=True) - - # Wait for response to be available - t0 = time.time() - initially_zero = False - while self._get_a2() == 0: - initially_zero = True - if time.time() > t0 + 5: - self._log.debug("A2 never returned 1") - break - - # Read all available responses - while self._get_a2() == 1: - resp = self._get_a1_response(min_length=0)[2:] # Skip \r\n - if resp and resp != "Stradus> ": - result = resp - self._send_a3() # Acknowledge - - if log_level >= 1: - self._log.info(f"Sent {cmd}, got response '{result}'") - - return result - - except Exception as e: - self._log.error(f"Command failed: {cmd}, error: {e}") - return None - - def _parse_query(self, cmd): - """Send query command and parse response.""" - response = self._send_command(cmd) - if response: - return response[len(cmd) + 1 :] - return None - - def _parse_float_query(self, cmd): - """Send query command and parse response as float.""" - result = self._parse_query(cmd) - return float(result) if result else 0.0 - - def _parse_int_query(self, cmd): - """Send query command and parse response as int.""" - result = self._parse_query(cmd) - return int(result) if result else 0 - - def _get_channel_for_wavelength(self, wavelength): - """Map wavelength to channel if using wavelength-based addressing.""" - if isinstance(wavelength, (int, float)): - return self.wavelength_to_channel.get(int(wavelength)) - return wavelength # Assume it's already a channel letter - def set_intensity_control_mode(self, mode): """ Set intensity control mode. @@ -204,12 +75,7 @@ def set_intensity_control_mode(self, mode): Args: mode: IntensityControlMode.Software or IntensityControlMode.External """ - self.intensity_mode = mode - epc_value = 1 if mode == IntensityControlMode.External else 0 - - for channel in self.active_channels: - if self.active_channels[channel]: - self._send_command(f"{channel}.epc={epc_value}") + raise NotImplementedError("Only software intensity control is supported for VersaLase") def get_intensity_control_mode(self): """ @@ -227,9 +93,10 @@ def set_shutter_control_mode(self, mode): Args: mode: ShutterControlMode enum """ + for laser in self.sdk.get_lasers(): + laser.set_digital_mode(mode == ShutterControlMode.TTL) + self.shutter_mode = mode - # VersaLase doesn't have explicit TTL shutter control in the protocol shown - # This would need to be implemented if the hardware supports it def get_shutter_control_mode(self): """ From 21db131ce2536a8ec83f2424e733410654aff24d Mon Sep 17 00:00:00 2001 From: You Yan Date: Tue, 17 Jun 2025 01:45:30 -0700 Subject: [PATCH 06/10] Vortran laser integration --- software/control/_def.py | 10 +- software/control/gui_hcs.py | 16 ++++ software/control/lighting_versalase.py | 122 +++++++++---------------- 3 files changed, 67 insertions(+), 81 deletions(-) diff --git a/software/control/_def.py b/software/control/_def.py index 2c4c639c0..c298da381 100644 --- a/software/control/_def.py +++ b/software/control/_def.py @@ -610,13 +610,15 @@ class SOFTWARE_POS_LIMIT: CAMERA_TYPE = "Default" FOCUS_CAMERA_TYPE = "Default" -# Spinning disk confocal integration -ENABLE_SPINNING_DISK_CONFOCAL = False USE_LDI_SERIAL_CONTROL = False -LDI_INTENSITY_MODE = "PC" -LDI_SHUTTER_MODE = "PC" +LDI_INTENSITY_MODE = "PC" # "PC" or "EXT" +LDI_SHUTTER_MODE = "PC" # "PC" or "EXT" USE_CELESTA_ETHENET_CONTROL = False +USE_VORTRAN_LASER_USB_CONTROL = False +VORTRAN_SHUTTER_CONTROL_MODE = "PC" # "PC" or "EXT" +# Spinning disk confocal integration +ENABLE_SPINNING_DISK_CONFOCAL = False XLIGHT_EMISSION_FILTER_MAPPING = { 405: 1, 470: 1, diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 3cb4d7307..660e8da08 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -442,6 +442,22 @@ def loadHardwareObjects(self): self.log.error("Error initializing CELESTA") raise + if USE_VORTRAN_LASER_USB_CONTROL: + try: + import control.lighting_versalase + + self.versalase = control.lighting_versalase.VersaLase() + self.illuminationController = IlluminationController( + self.microcontroller, + IntensityControlMode.Software, + ShutterControlMode.TTL if VORTRAN_SHUTTER_CONTROL_MODE == "EXT" else ShutterControlMode.Software, + LightSourceType.VersaLase, + self.versalase, + ) + except Exception: + self.log.error("Error initializing VersaLase") + raise + if USE_ZABER_EMISSION_FILTER_WHEEL: try: self.emission_filter_wheel = serial_peripherals.FilterController( diff --git a/software/control/lighting_versalase.py b/software/control/lighting_versalase.py index ef8bb2f28..3549ac216 100644 --- a/software/control/lighting_versalase.py +++ b/software/control/lighting_versalase.py @@ -1,5 +1,3 @@ -import time - from laser_sdk import LaserSDK from squid.abc import LightSource @@ -15,9 +13,6 @@ def __init__(self, **kwds): self.sdk = LaserSDK() self.sdk.discover() - self.intensity_mode = IntensityControlMode.Software - self.shutter_mode = ShutterControlMode.Software - self.channel_mappings = { 405: None, 470: None, @@ -36,17 +31,16 @@ def __init__(self, **kwds): try: self.initialize() except Exception as e: - self._log.error(f"Failed to initialize VersaLase: {e}") + self._log.error(f"Failed to initialize Vortran laser: {e}") - def initialize(self): + def initialize(self) -> bool: """ - Initialize the connection and settings for the VersaLase. + Initialize the connection and settings for the Vortran laser. Returns True if successful, False otherwise. """ try: # Query information about installed lasers for laser in self.sdk.get_lasers(): - self.wavelength_to_laser[laser.wavelength] = laser self._log.info(f"Found laser {laser.wavelength}: {laser.max_power}") laser.disable() if laser.wavelength == 405: @@ -68,27 +62,28 @@ def initialize(self): self._log.error(f"Initialization failed: {e}") return False - def set_intensity_control_mode(self, mode): + def set_intensity_control_mode(self, mode: IntensityControlMode): """ - Set intensity control mode. + Set intensity control mode. Only software intensity control is supported for Vortran laser. Args: mode: IntensityControlMode.Software or IntensityControlMode.External """ - raise NotImplementedError("Only software intensity control is supported for VersaLase") + self._log.debug("Only software intensity control is supported for Vortran laser") + pass - def get_intensity_control_mode(self): + def get_intensity_control_mode(self) -> IntensityControlMode: """ - Get current intensity control mode. + Get current intensity control mode. Only software intensity control is supported for Vortran laser. Returns: IntensityControlMode enum value """ - return self.intensity_mode + return IntensityControlMode.Software - def set_shutter_control_mode(self, mode): + def set_shutter_control_mode(self, mode: ShutterControlMode): """ - Set shutter control mode. + Set shutter control mode for all lasers. Args: mode: ShutterControlMode enum @@ -98,16 +93,28 @@ def set_shutter_control_mode(self, mode): self.shutter_mode = mode - def get_shutter_control_mode(self): + def get_shutter_control_mode(self) -> ShutterControlMode: """ Get current shutter control mode. Returns: ShutterControlMode enum value """ - return self.shutter_mode + # The lasers in the VersaLase may have different shutter control states. + # We call set_shutter_control_mode() on initialize so they should all be the same. + # Raise an error here if they are not. + digital_mode = None + for laser in self.sdk.get_lasers(): + if digital_mode is None: + digital_mode = laser.digital_mode + elif digital_mode != laser.digital_mode: + raise ValueError("Laser shutter control modes are not consistent") + if digital_mode is None: + raise ValueError("No lasers found") + + return ShutterControlMode.TTL if digital_mode else ShutterControlMode.Software - def set_shutter_state(self, channel, state): + def set_shutter_state(self, channel: int, state: bool): """ Turn a specific channel on or off. @@ -115,14 +122,10 @@ def set_shutter_state(self, channel, state): channel: Channel ID (letter or wavelength) state: True to turn on, False to turn off """ - channel = self._get_channel_for_wavelength(channel) - if channel and channel in self.active_channels and self.active_channels[channel]: - le_value = 1 if state else 0 - response = self._send_command(f"{channel}.le={le_value}") - if response: - self._log.info(f"Set channel {channel} shutter to {state}") + laser = self.sdk.get_laser_by_id(self.channel_mappings[channel]) + laser.enable(state) - def get_shutter_state(self, channel): + def get_shutter_state(self, channel: int) -> bool: """ Get the current shutter state of a specific channel. @@ -132,12 +135,10 @@ def get_shutter_state(self, channel): Returns: bool: True if channel is on, False if off """ - channel = self._get_channel_for_wavelength(channel) - if channel and channel in self.active_channels and self.active_channels[channel]: - return self._parse_int_query(f"{channel}.?le") == 1 - return False + laser = self.sdk.get_laser_by_id(self.channel_mappings[channel]) + return laser.get_emission_status() - def set_intensity(self, channel, intensity): + def set_intensity(self, channel: int, intensity: float): """ Set the intensity for a specific channel. @@ -145,17 +146,10 @@ def set_intensity(self, channel, intensity): channel: Channel ID (letter or wavelength) intensity: Intensity value (0-100 percent) """ - channel = self._get_channel_for_wavelength(channel) - if channel and channel in self.active_channels and self.active_channels[channel]: - # Convert percentage to power in mW - max_power = self.channel_info[channel]["max_power"] - power_mw = (intensity / 100.0) * max_power + laser = self.sdk.get_laser_by_id(self.channel_mappings[channel]) + laser.set_power(laser.max_power * intensity / 100.0) - response = self._send_command(f"{channel}.lp={power_mw:.2f}") - if response: - self._log.info(f"Set channel {channel} intensity to {intensity}% ({power_mw:.2f}mW)") - - def get_intensity(self, channel): + def get_intensity(self, channel: int) -> float: """ Get the current intensity of a specific channel. @@ -165,40 +159,14 @@ def get_intensity(self, channel): Returns: float: Current intensity value (0-100 percent) """ - channel = self._get_channel_for_wavelength(channel) - if channel and channel in self.active_channels and self.active_channels[channel]: - # Get the set power (lps) rather than measured power (lp) - # as measured power might be 0 if shutter is closed - power_mw = self._parse_float_query(f"{channel}.?lps") - max_power = self.channel_info[channel]["max_power"] - if max_power > 0: - return (power_mw / max_power) * 100.0 - return 0.0 + # For Vortran laser, we are able to get the actual intensity of the lasers. + # To keep consistency with other light sources, we return the set power/intensity here. + laser = self.sdk.get_laser_by_id(self.channel_mappings[channel]) + laser_info = laser.get_op2() + return laser_info["LaserSetPower"] / laser.max_power * 100.0 def shut_down(self): - """Safely shut down the VersaLase.""" - if self.live: - self._log.info("Shutting down VersaLase") - for channel in self.active_channels: - if self.active_channels[channel]: - self.set_intensity(channel, 0) - self.set_shutter_state(channel, False) - self.live = False - - def get_status(self): - """ - Get the status of the VersaLase. - - Returns: - bool: True if connected and operational - """ - return self.live - - def get_channel_info(self): - """ - Get information about all active channels. - - Returns: - dict: Channel information including wavelength and power limits - """ - return self.channel_info + """Safely shut down the Vortran laser.""" + for laser in self.sdk.get_lasers(): + laser.disable() + laser.disconnect() From 11b71d9d7b218dcf3264e7d27b7e72f3b245c152 Mon Sep 17 00:00:00 2001 From: You Yan Date: Tue, 17 Jun 2025 02:07:55 -0700 Subject: [PATCH 07/10] VersaLase instructions --- software/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/software/README.md b/software/README.md index 483a574fc..6e4bb214a 100644 --- a/software/README.md +++ b/software/README.md @@ -130,6 +130,18 @@ Follow the instructions during the installation. +
+Installing SDK for VersaLase laser + +Run the following commands: +``` +curl -L -O https://raw.githubusercontent.com/sthronevlt/VersaLSS/main/dist/laser_sdk-0.1.1-py3-none-any.whl +pip3 install laser_sdk-0.1.1-py3-none-any.whl +rm laser_sdk-0.1.1-py3-none-any.whl +``` + +
+ ## Configuring the software Copy the .ini file associated with the microscope configuration to the software folder. Make modifications as needed (e.g. `camera_type`, `support_laser_autofocus`,`focus_camera_exposure_time_ms`) From 5b6c681c2436f803d7332b4734205ab544db1531 Mon Sep 17 00:00:00 2001 From: You Yan Date: Tue, 17 Jun 2025 02:12:13 -0700 Subject: [PATCH 08/10] add laser SDK GitHub link --- software/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/software/README.md b/software/README.md index 6e4bb214a..d3a314d23 100644 --- a/software/README.md +++ b/software/README.md @@ -133,6 +133,8 @@ Follow the instructions during the installation.
Installing SDK for VersaLase laser +Link: https://github.com/sthronevlt/VersaLSS + Run the following commands: ``` curl -L -O https://raw.githubusercontent.com/sthronevlt/VersaLSS/main/dist/laser_sdk-0.1.1-py3-none-any.whl From 752460c0c0966f9970b774e4248cf1394c0da329 Mon Sep 17 00:00:00 2001 From: You Yan Date: Tue, 17 Jun 2025 12:53:51 -0700 Subject: [PATCH 09/10] naming --- software/control/core/core.py | 2 +- software/control/gui_hcs.py | 14 +++++++------- software/control/{lighting.py => illumination.py} | 0 ...lighting_celesta.py => illumination_celesta.py} | 2 +- ...ting_versalase.py => illumination_versalase.py} | 2 +- software/control/microscope.py | 6 +++--- software/control/serial_peripherals.py | 2 +- software/tests/control/gui_test_stubs.py | 10 +++++----- software/tools/evaluate_intensity_calibration.py | 2 +- software/tools/generate_intensity_calibrations.py | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) rename software/control/{lighting.py => illumination.py} (100%) rename software/control/{lighting_celesta.py => illumination_celesta.py} (99%) rename software/control/{lighting_versalase.py => illumination_versalase.py} (98%) diff --git a/software/control/core/core.py b/software/control/core/core.py index c8b495e26..47e9a1642 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -2151,7 +2151,7 @@ def get_acquisition_image_count(self): try: # We have Nt timepoints. For each timepoint, we capture images at all the regions. Each # region has a list of coordinates that we capture at, and at each coordinate we need to - # do a capture for each requested camera + lighting + other configuration selected. So + # do a capture for each requested camera + illumination + other configuration selected. So # total image count is: coords_per_region = [ len(region_coords) for (region_id, region_coords) in self.scanCoordinates.region_fov_coordinates.items() diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 660e8da08..e5f33ed93 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -1,7 +1,7 @@ # set QT_API environment variable import os -import control.lighting +import control.illumination os.environ["QT_API"] = "pyqt5" import serial @@ -23,7 +23,7 @@ import squid.config import squid.stage.utils import control.microscope -from control.lighting import LightSourceType, IntensityControlMode, ShutterControlMode, IlluminationController +from control.illumination import LightSourceType, IntensityControlMode, ShutterControlMode, IlluminationController import squid.camera.utils log = squid.logging.get_logger(__name__) @@ -343,7 +343,7 @@ def loadSimulationObjects(self): if USE_LDI_SERIAL_CONTROL: self.ldi = serial_peripherals.LDI_Simulation() - self.illuminationController = control.lighting.IlluminationController( + self.illuminationController = control.illumination.IlluminationController( self.microcontroller, self.ldi.intensity_mode, self.ldi.shutter_mode, LightSourceType.LDI, self.ldi ) if USE_ZABER_EMISSION_FILTER_WHEEL: @@ -428,9 +428,9 @@ def loadHardwareObjects(self): if USE_CELESTA_ETHENET_CONTROL: try: - import control.lighting_celesta + import control.illumination_celesta - self.celesta = control.lighting_celesta.CELESTA() + self.celesta = control.illumination_celesta.CELESTA() self.illuminationController = IlluminationController( self.microcontroller, IntensityControlMode.Software, @@ -444,9 +444,9 @@ def loadHardwareObjects(self): if USE_VORTRAN_LASER_USB_CONTROL: try: - import control.lighting_versalase + import control.illumination_versalase - self.versalase = control.lighting_versalase.VersaLase() + self.versalase = control.illumination_versalase.VersaLase() self.illuminationController = IlluminationController( self.microcontroller, IntensityControlMode.Software, diff --git a/software/control/lighting.py b/software/control/illumination.py similarity index 100% rename from software/control/lighting.py rename to software/control/illumination.py diff --git a/software/control/lighting_celesta.py b/software/control/illumination_celesta.py similarity index 99% rename from software/control/lighting_celesta.py rename to software/control/illumination_celesta.py index 459a84b28..fc03c74c3 100755 --- a/software/control/lighting_celesta.py +++ b/software/control/illumination_celesta.py @@ -10,7 +10,7 @@ import urllib.request import traceback from squid.abc import LightSource -from control.lighting import ShutterControlMode +from control.illumination import ShutterControlMode import squid.logging diff --git a/software/control/lighting_versalase.py b/software/control/illumination_versalase.py similarity index 98% rename from software/control/lighting_versalase.py rename to software/control/illumination_versalase.py index 3549ac216..4451d1a88 100644 --- a/software/control/lighting_versalase.py +++ b/software/control/illumination_versalase.py @@ -1,7 +1,7 @@ from laser_sdk import LaserSDK from squid.abc import LightSource -from control.lighting import ShutterControlMode, IntensityControlMode +from control.illumination import ShutterControlMode, IntensityControlMode import squid.logging diff --git a/software/control/microscope.py b/software/control/microscope.py index 5577af2e1..bc5929f1f 100644 --- a/software/control/microscope.py +++ b/software/control/microscope.py @@ -16,7 +16,7 @@ import squid.stage.utils import control.microcontroller as microcontroller -from control.lighting import LightSourceType, IntensityControlMode, ShutterControlMode, IlluminationController +from control.illumination import LightSourceType, IntensityControlMode, ShutterControlMode, IlluminationController from control.piezo import PiezoStage import control.serial_peripherals as serial_peripherals import control.filterwheel as filterwheel @@ -260,9 +260,9 @@ def initialize_peripherals(self): if USE_CELESTA_ETHENET_CONTROL: try: - import control.lighting_celesta + import control.illumination_celesta - self.celesta = control.lighting_celesta.CELESTA() + self.celesta = control.illumination_celesta.CELESTA() self.illuminationController = IlluminationController( self.microcontroller, IntensityControlMode.Software, diff --git a/software/control/serial_peripherals.py b/software/control/serial_peripherals.py index 9ac941083..a49a1ce24 100644 --- a/software/control/serial_peripherals.py +++ b/software/control/serial_peripherals.py @@ -3,7 +3,7 @@ import time from typing import Tuple, Optional import struct -from control.lighting import LightSourceType, IntensityControlMode, ShutterControlMode +from control.illumination import LightSourceType, IntensityControlMode, ShutterControlMode from control._def import * from squid.abc import LightSource diff --git a/software/tests/control/gui_test_stubs.py b/software/tests/control/gui_test_stubs.py index b7b6b8669..286bdd051 100644 --- a/software/tests/control/gui_test_stubs.py +++ b/software/tests/control/gui_test_stubs.py @@ -2,7 +2,7 @@ import control.core.core import control.microcontroller -import control.lighting +import control.illumination import squid.abc import control._def @@ -34,11 +34,11 @@ def get_test_configuration_manager() -> control.core.core.ConfigurationManager: def get_test_illumination_controller( microcontroller: control.microcontroller.Microcontroller, -) -> control.lighting.IlluminationController: - return control.lighting.IlluminationController( +) -> control.illumination.IlluminationController: + return control.illumination.IlluminationController( microcontroller=microcontroller, - intensity_control_mode=control.lighting.IntensityControlMode.Software, - shutter_control_mode=control.lighting.ShutterControlMode.Software, + intensity_control_mode=control.illumination.IntensityControlMode.Software, + shutter_control_mode=control.illumination.ShutterControlMode.Software, ) diff --git a/software/tools/evaluate_intensity_calibration.py b/software/tools/evaluate_intensity_calibration.py index 79693d5ce..a9f70c8a5 100644 --- a/software/tools/evaluate_intensity_calibration.py +++ b/software/tools/evaluate_intensity_calibration.py @@ -12,7 +12,7 @@ os.chdir(software_dir) from PM16 import PM16 -from control.lighting import IlluminationController, IntensityControlMode, ShutterControlMode +from control.illumination import IlluminationController, IntensityControlMode, ShutterControlMode import control.microcontroller as microcontroller from control._def import * diff --git a/software/tools/generate_intensity_calibrations.py b/software/tools/generate_intensity_calibrations.py index f1f46dcbd..29dd95270 100644 --- a/software/tools/generate_intensity_calibrations.py +++ b/software/tools/generate_intensity_calibrations.py @@ -12,7 +12,7 @@ os.chdir(software_dir) from PM16 import PM16 -from control.lighting import IlluminationController, IntensityControlMode, ShutterControlMode +from control.illumination import IlluminationController, IntensityControlMode, ShutterControlMode import control.microcontroller as microcontroller from control._def import * From 6f4eac3c4acaf3ce873cdc7c43d13ec4a9a7d33f Mon Sep 17 00:00:00 2001 From: You Yan Date: Tue, 17 Jun 2025 12:57:24 -0700 Subject: [PATCH 10/10] update config file --- .../configuration_Squid+_Cicero_LDI.ini | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/software/configurations/configuration_Squid+_Cicero_LDI.ini b/software/configurations/configuration_Squid+_Cicero_LDI.ini index 675b766a8..29c9132cf 100644 --- a/software/configurations/configuration_Squid+_Cicero_LDI.ini +++ b/software/configurations/configuration_Squid+_Cicero_LDI.ini @@ -1,5 +1,7 @@ [GENERAL] -camera_type = Hamamatsu +version = 2.0 + +camera_type = Toupcam _camera_type_options = [Default, FLIR, Toupcam, Hamamatsu, Tucsen] controller_sn = None support_scimicroscopy_led_array = False @@ -13,7 +15,7 @@ LDI_INTENSITY_MODE = "EXT" LDI_SHUTTER_MODE = "EXT" enable_spinning_disk_confocal = True xlight_emission_filter_mapping = {405: 1, 470: 2, 555: 3, 640: 4, 730: 5} -xlight_serial_number = "AB0PKT2R" +xlight_serial_number = "B000DAO6" xlight_sleep_time_for_wheel = 0.25 xlight_validate_wheel_pos = False use_napari_for_live_view = False @@ -134,11 +136,11 @@ wellplate_offset_y_mm = 0 focus_measure_operator = GLVA controller_version = Teensy -support_laser_autofocus = True +support_laser_autofocus = False focus_camera_type = Default _focus_camera_type_options = [Default, FLIR, Toupcam] _support_laser_autofocus_options = [True, False] -main_camera_model = C15440-20UP +main_camera_model = ITR3CMOS26000KMA focus_camera_model = MER2-630-60U3M focus_camera_exposure_time_ms = 0.8 @@ -157,7 +159,8 @@ default_multipoint_ny = 1 inverted_objective = True _inverted_objective_options = [True, False] -filter_controller_enable = False +filter_controller_enable = Falsefilter_controller_enable = False + _filter_controller_enable_options = [True, False] illumination_intensity_factor = 1 @@ -166,14 +169,24 @@ Z_MOTOR_CONFIG = "STEPPER" _Z_MOTOR_CONFIG_OPTIONS = ["STEPPER", "STEPPER + PIEZO", "PIEZO", "LINEAR"] [CAMERA_CONFIG] +roi_offset_x_default = 0 +roi_offset_y_default = 0 +roi_width_default = 6208 +roi_height_default = 4168 rotate_image_angle = None flip_image = None _flip_image_options = [Vertical, Horizontal, Both, None] -crop_width_unbinned = 2304 -crop_height_unbinned = 2304 -binning_factor_default = 1 -pixel_format_default = MONO16 -_default_pixel_format_options = [MONO8, MONO16] +crop_width = 4168 +crop_height = 4168 +binning_factor_default = 2 +pixel_format_default = MONO8 +_default_pixel_format_options = [MONO8, MONO12, MONO14, MONO16, BAYER_RG8, BAYER_RG12] +temperature_default = 20 +fan_speed_default = 1 +blacklevel_value_default = 3 +awb_ratios_r = 1.375 +awb_ratios_g = 1 +awb_ratios_b = 1.4141 [LIMIT_SWITCH_POLARITY] x_home = 1