Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/**
* This is the container for all the data needed to operate a microscope with light sheet manager.
*/
public class LightSheetManager implements LightSheetManagerApi {
public class LightSheetManager implements LightSheetManagerApi, AutoCloseable {

private final Studio studio_;
private final CMMCore core_;
Expand All @@ -35,7 +35,8 @@ public class LightSheetManager implements LightSheetManagerApi {
//private final AcquisitionTableData acqTableData_;

public LightSheetManager(final Studio studio) {
studio_ = Objects.requireNonNull(studio);
studio_ = Objects.requireNonNull(studio,
"Micro-Manager Studio context cannot be null!");
core_ = studio_.core();

pluginSettings_ = new PluginSettings();
Expand Down Expand Up @@ -101,15 +102,65 @@ public boolean setup() {
}

/**
* This sets the text to be displayed in the error ui when an error occurs during setup.
* Save the settings and stop polling, should be called before exiting.
*/
@Override
public void close() {

// disable position polling
try {
if (positionUpdater_ != null) {
// message should always be printed
if (studio_ != null) {
studio_.logs().logMessage("Stopping position updater polling...");
}

if (positionUpdater_.isPolling()) {
positionUpdater_.stopPolling();
}
}
} catch (Exception e) {
// log the error so we can still try to save the settings!
if (studio_ != null) {
studio_.logs().logError(e, "Failed to stop position updater polling during close.");
}
}

// save settings
try {
if (userSettings_ != null) {
userSettings_.save();
if (studio_ != null) {
studio_.logs().logMessage("User settings saved.");
}
}
} catch (Exception e) {
if (studio_ != null) {
studio_.logs().logError(e, "Failed to save user settings during close.");
}
}

if (studio_ != null) {
studio_.logs().logMessage("Light Sheet Manager Shutdown");
}
}

/**
* Sets the text in the error ui when an error occurs during setup.
*
* @param text the error message
*/
public void setErrorText(final String text) {
public void setupErrorMessage(final String text) {
errorText_ = text;
}

public String getErrorText() {
/**
* Returns the error message from the setup method, it will be empty
* in the case of no errors detected.
*
* @return the error message
*/
public String setupErrorMessage() {
return errorText_;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,18 @@ public LightSheetManagerFrame(final LightSheetManager model, final boolean isLoa
break;
case SCAPE:
if (model_.devices().adapter().numImagingPaths() > 1) {
model_.setErrorText("SCAPE geometry does not support multiple imaging paths. "
model_.setupErrorMessage("SCAPE geometry does not support multiple imaging paths. "
+ " Use the \"SimultaneousCameras\" property to support multiple cameras.");
createErrorUserInterface();
return;
}
if (model_.devices().adapter().numIlluminationPaths() > 1) {
model_.setErrorText("SCAPE geometry can only have a single illumination path.");
model_.setupErrorMessage("SCAPE geometry can only have a single illumination path.");
createErrorUserInterface();
return;
}
if (model_.devices().adapter().lightSheetType() == LightSheetType.SCANNED) {
model_.setErrorText("Scanned light sheets are not implemented for SCAPE geometry, " +
model_.setupErrorMessage("Scanned light sheets are not implemented for SCAPE geometry, " +
"please contact the developers if you need this feature.");
createErrorUserInterface();
return;
Expand All @@ -64,7 +64,7 @@ public LightSheetManagerFrame(final LightSheetManager model, final boolean isLoa
model_.acquisitions().updateDurationLabels();
break;
default:
model_.setErrorText("Microscope geometry type " + geometry + " is not supported yet.");
model_.setupErrorMessage("Microscope geometry type " + geometry + " is not supported yet.");
createErrorUserInterface();
break;
}
Expand All @@ -86,7 +86,7 @@ private void createErrorUserInterface() {
));

final Label lblTitle = new Label(LightSheetManagerPlugin.menuName, Font.BOLD, 16);
final Label lblError = new Label(model_.getErrorText(), Font.BOLD, 14);
final Label lblError = new Label(model_.setupErrorMessage(), Font.BOLD, 14);

add(lblTitle, "wrap");
add(lblError, "");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,16 @@ public void onPluginSelected() {
frame_ = new LightSheetManagerFrame(model_, isLoaded);
frame_.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

// clean up resources before the window is fully closed
// call the shutdown code if the main ui was loaded, skip if error ui
WindowUtils.registerWindowClosingEvent(frame_, event -> {
// no need to clean up for the error ui
if (isLoaded) {
if (model_.positions().isPolling()) {
model_.positions().stopPolling();
}
model_.userSettings().save();
model_.close();
}
});

// clear references after the window is fully closed
// prevent memory leaks from closed plugin instances
WindowUtils.registerWindowClosedEvent(frame_, event -> {
frame_ = null;
model_ = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ private void createUserInterface() {
pnlLeft.add(pnlCameras_, "growx, span 2");

pnlCenter.add(pnlChannelTable_, "wrap");
pnlCenter.add(new JLabel("Acquisition mode:"), "split 2");
pnlCenter.add(new JLabel("Acquisition Mode:"), "split 2");
pnlCenter.add(cmbAcquisitionModes_, "");

pnlRight_.add(pnlVolumeSettings_, "growx, wrap");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
import org.micromanager.lightsheetmanager.gui.utils.DialogUtils;
import org.micromanager.lightsheetmanager.model.channels.ChannelSpec;

import javax.swing.*;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import java.util.Objects;

/**
Expand Down Expand Up @@ -43,8 +44,8 @@ public ChannelTablePanel(final LightSheetManager model, final CheckBox checkBox)
}

private void createUserInterface() {
lblChannelGroup_ = new JLabel("Channel group:");
lblChangeChannel_ = new JLabel("Change channel:");
lblChannelGroup_ = new JLabel("Channel Group:");
lblChangeChannel_ = new JLabel("Channel Mode:");

btnAddChannel_ = new Button("Add", 74, 24);
btnRemoveChannel_ = new Button("Remove", 74, 24);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ public boolean validateCameras() {
useDefaultImagingCameraOrder();
return true;
} else {
model_.setErrorText(message);
model_.setupErrorMessage(message);
return false;
}
}
Expand All @@ -382,7 +382,7 @@ public boolean validateCameras() {
final String message = "Camera in settings not found in hardware: " + camera.name()
+ ", consider creating a new user profile if the pre-init properties changed.";
model_.studio().logs().logError(message);
model_.setErrorText(message);
model_.setupErrorMessage(message);
return false;
}
}
Expand Down Expand Up @@ -457,7 +457,7 @@ public boolean hasDeviceAdapter() {
deviceAdapterName_ = device;
count++;
if (count > 1) {
model_.setErrorText("You have multiple instances of the LightSheetManager " +
model_.setupErrorMessage("You have multiple instances of the LightSheetManager " +
"device adapter in your hardware configuration.");
break; // exit loop because this a failure condition
}
Expand All @@ -469,7 +469,7 @@ public boolean hasDeviceAdapter() {
}
// no device adapters found
if (count == 0) {
model_.setErrorText("Please add the LightSheetManager device adapter to your " +
model_.setupErrorMessage("Please add the LightSheetManager device adapter to your " +
"hardware configuration to use this plugin.");
}
return count == 1;
Expand Down
91 changes: 91 additions & 0 deletions src/python/lsm_pycromanager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# /// script
# requires-python = ">=3.14"
# dependencies = ["pycromanager>=1.0.2"]
# ///

from pycromanager import Studio, JavaObject
from types import TracebackType
from typing import Optional, Type
import logging

# Set up a logger specific to your light-sheet plugin
logger = logging.getLogger("LightSheetManager")

# uncomment to see errors
# logging.basicConfig(level=logging.DEBUG)

class LightSheetManager:
"""
A high-level Python wrapper for the Micro-Manager Light Sheet Manager plugin.

Handles the cross-language bridge between Python and Java, providing
automatic lifecycle management using the Python 'with' statement.

Explicit lifecycle management is provided though manual calls to 'open' and 'close'.
"""
def __init__(self) -> None:
self.lsm: Optional[JavaObject] = None

def open(self) -> JavaObject:
"""Connect to Micro-Manager and initialize Light Sheet Manager.

Returns:
JavaObject - The initialized Java Light Sheet Manager instance.

Raises:
ConnectionError - the pycromanager bridge or Java initialization failed
"""
if self.lsm is not None:
return self.lsm

try:
studio = Studio()
self.lsm = JavaObject("org.micromanager.lightsheetmanager.LightSheetManager", args=[studio])

if not self.lsm.setup():
raise RuntimeError("Java LightSheetManager setup() returned False.")

logger.info("Light Sheet Manager initialized.")
return self.lsm
except Exception as e:
self.close() # cleanup if initialization fails
raise ConnectionError(f"Could not initialize Light Sheet Manager: {e}") from e

def close(self) -> None:
"""Call the Java AutoCloseable close routine on the LSM JavaObject."""
if self.lsm is None:
return

logger.info("Requesting Light Sheet Manager resource cleanup...")
try:
self.lsm.close()
logger.info("Java close() request sent successfully.")
except Exception as e:
logger.warning(f"Failed to communicate with Java close() routine: {e}")
finally:
self.lsm = None # help garbage collection

def __enter__(self) -> JavaObject:
"""Entering the context block opens the bridge and returns the LSM JavaObject."""
return self.open()

def __exit__(self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType]) -> bool:
"""Automatically clean up LSM resources resources when the exiting the context block."""
if exc_type:
logger.error(f"Context block exited with an error: {exc_val}", exc_info=True)

self.close()
return False # do not suppress exceptions


def main() -> None:
with LightSheetManager() as lsm:
active_camera = lsm.devices().first_active_camera_name()
print(f"Active Camera: {active_camera}")


if __name__ == "__main__":
main()
Loading