diff --git a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java index 3966e3b..8bd692e 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java +++ b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java @@ -3,6 +3,7 @@ import mmcorej.CMMCore; import org.micromanager.Studio; import org.micromanager.lightsheetmanager.api.LightSheetManagerApi; +import org.micromanager.lightsheetmanager.model.Logging; import org.micromanager.lightsheetmanager.api.data.GeometryType; import org.micromanager.lightsheetmanager.model.DeviceManager; import org.micromanager.lightsheetmanager.model.PluginSettings; @@ -27,6 +28,7 @@ public class LightSheetManager implements LightSheetManagerApi, AutoCloseable { private PluginSettings pluginSettings_; + private final Logging logging_; private final UserSettings userSettings_; private final DeviceManager deviceManager_; private final PositionUpdater positionUpdater_; @@ -40,6 +42,7 @@ public LightSheetManager(final Studio studio) { core_ = studio_.core(); pluginSettings_ = new PluginSettings(); + logging_ = new Logging(this); userSettings_ = new UserSettings(this); deviceManager_ = new DeviceManager(studio_, this); @@ -164,6 +167,18 @@ public String setupErrorMessage() { return errorText_; } + /** + * The plugin's logging and error-reporting service. + * + *
This lives on the model rather than the acquisition engine because callers like + * {@code DeviceManager} report errors during {@code setup()}, before the engine exists. + * + * @return the logging service + */ + public Logging logging() { + return logging_; + } + public UserSettings userSettings() { return userSettings_; } diff --git a/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/SettingsTab.java b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/SettingsTab.java index 17d19ce..b6aacc9 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/SettingsTab.java +++ b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/SettingsTab.java @@ -31,6 +31,8 @@ public class SettingsTab extends Panel implements ListeningPanel { private Button btnCreateConfigGroup_; + private CheckBox cbxAcquireFailQuietly_; + // changes the ui setup private boolean isUsingPLogic_; private boolean isUsingScanSettings_; @@ -154,11 +156,17 @@ private void createUserInterface() { pnlLightSheet.add(spnLiveScanPeriod_, ""); } + cbxAcquireFailQuietly_ = new CheckBox("Acquisition failures are quiet", + model_.pluginSettings().isAcquireFailQuietly()); + cbxAcquireFailQuietly_.setToolTipText("Log acquisition errors instead of showing dialog " + + "boxes, so unattended runs (playlist or scripting) can never hang on an error dialog."); + add(pnlScanSettings, "wrap"); if (isUsingPLogic_) { add(pnlLightSheet, "growx, wrap"); } + add(cbxAcquireFailQuietly_, "wrap"); add(btnCreateConfigGroup_, ""); } @@ -201,6 +209,9 @@ private void createEventHandlers() { () -> scanner.setFilterFreqY(spnSliceAxisFilterFreq_.getDouble())); } + cbxAcquireFailQuietly_.registerListener(() -> model_.pluginSettings() + .setAcquireFailQuietly(cbxAcquireFailQuietly_.isSelected())); + btnCreateConfigGroup_.registerListener(() -> model_.devices().createConfigGroup()); // TODO: make this work with diSPIM settings diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/Logging.java b/src/main/java/org/micromanager/lightsheetmanager/model/Logging.java new file mode 100644 index 0000000..a97a03f --- /dev/null +++ b/src/main/java/org/micromanager/lightsheetmanager/model/Logging.java @@ -0,0 +1,156 @@ +package org.micromanager.lightsheetmanager.model; + +import org.micromanager.lightsheetmanager.LightSheetManager; +import org.micromanager.lightsheetmanager.gui.utils.DialogUtils; + +import java.util.Objects; + +/** + * The plugin's logging and error-reporting service. + * + *
Two method families with different contracts: + *
Dialogs shown from the acquisition thread block it until someone clicks OK, so an + * unattended run (Playlist, scripting) can hang indefinitely on an error. The 1.4 plugin + * solved this with {@code MyDialogUtils.showError(..., hideErrors)} and threaded the flag + * through the call chain; reading the setting at the report site instead also covers + * stop/pause requests arriving outside a run. + * + *
This is deliberately an instance class, not static utilities: the backend it writes + * through must be swappable per model instance for headless operation. + */ +public final class Logging { + + private final LightSheetManager model_; + + public Logging(final LightSheetManager model) { + model_ = Objects.requireNonNull(model); + } + + // The setting is read through the model on every call because loading user settings + // replaces the PluginSettings object; a reference captured here would go stale. + private boolean isQuiet() { + return model_.pluginSettings().isAcquireFailQuietly(); + } + + // --- pure logging - never shows UI --- + + /** + * Logs a message. + * + * @param message the message to log + */ + public void logMessage(final String message) { + model_.studio().logs().logMessage(message); + } + + /** + * Logs a debug message. + * + * @param message the message to log + */ + public void logDebugMessage(final String message) { + model_.studio().logs().logDebugMessage(message); + } + + /** + * Logs an error message. + * + * @param message the error message + */ + public void logError(final String message) { + model_.studio().logs().logError(message); + } + + /** + * Logs an exception. + * + * @param e the exception to log + */ + public void logError(final Exception e) { + model_.studio().logs().logError(e); + } + + /** + * Logs an exception with a message. + * + * @param e the exception to log + * @param message the error message + */ + public void logError(final Exception e, final String message) { + model_.studio().logs().logError(e, message); + } + + // --- user-facing - honors "Acquisition failures are quiet" --- + + /** + * Reports an error message. + * + * @param message the error message + */ + public void reportError(final String message) { + if (isQuiet()) { + logError(message); + } else { + model_.studio().logs().showError(message); + } + } + + /** + * Reports an exception. + * + * @param e the exception to report + */ + public void reportError(final Exception e) { + if (isQuiet()) { + logError(e); + } else { + model_.studio().logs().showError(e); + } + } + + /** + * Reports an exception with a message. + * + * @param e the exception to report + * @param message the error message + */ + public void reportError(final Exception e, final String message) { + if (isQuiet()) { + logError(e, message); + } else { + model_.studio().logs().showError(e, message); + } + } + + /** + * Asks the user a yes/no question, unless acquisition failures are quiet, in which case + * {@code quietAnswer} is returned without showing a dialog and the choice is logged. + * + *
{@code quietAnswer} should be the answer an attended user is expected to give, so quiet
+ * runs follow the recommended path rather than silently declining it. 1.4 had no quiet
+ * variant of {@code getConfirmDialogResult} at all, so its confirm dialogs could still hang
+ * an unattended acquisition.
+ *
+ * @param title the dialog title
+ * @param message the yes/no question
+ * @param quietAnswer the answer to use without asking when failures are quiet
+ * @return the user's answer, or {@code quietAnswer} when failures are quiet
+ */
+ public boolean confirmOrDefault(final String title, final String message, final boolean quietAnswer) {
+ if (isQuiet()) {
+ logMessage("Quiet acquisition: answered \"" + (quietAnswer ? "Yes" : "No")
+ + "\" without showing the dialog [" + title + "]: " + message);
+ return quietAnswer;
+ }
+ return DialogUtils.showYesNoDialog(null, title, message);
+ }
+
+}
diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/PLogicDispim.java b/src/main/java/org/micromanager/lightsheetmanager/model/PLogicDispim.java
index 9341109..48085d1 100644
--- a/src/main/java/org/micromanager/lightsheetmanager/model/PLogicDispim.java
+++ b/src/main/java/org/micromanager/lightsheetmanager/model/PLogicDispim.java
@@ -1033,7 +1033,8 @@ public double getSheetWidth(CameraMode cameraMode, int view) {
} else {
final boolean autoSheet = model_.acquisitions().settings().sheetCalibration().autoSheetWidthEnabled();
if (autoSheet) {
- Rectangle roi = camera.getROI();
+ // unbinned, so the sheet width stays the same regardless of binning
+ Rectangle roi = camera.getUnbinnedROI();
if (roi == null || roi.height == 0) {
studio_.logs().logDebugMessage("Could not get camera ROI for auto sheet mode");
}
diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/PLogicScape.java b/src/main/java/org/micromanager/lightsheetmanager/model/PLogicScape.java
index f19c82e..c7db815 100644
--- a/src/main/java/org/micromanager/lightsheetmanager/model/PLogicScape.java
+++ b/src/main/java/org/micromanager/lightsheetmanager/model/PLogicScape.java
@@ -941,7 +941,8 @@ public double getSheetWidth(CameraMode cameraMode, int view) {
} else {
final boolean autoSheet = model_.acquisitions().settings().sheetCalibration().autoSheetWidthEnabled();
if (autoSheet) {
- Rectangle roi = camera.getROI();
+ // unbinned, so the sheet width stays the same regardless of binning
+ Rectangle roi = camera.getUnbinnedROI();
if (roi == null || roi.height == 0) {
studio_.logs().logDebugMessage("Could not get camera ROI for auto sheet mode");
}
diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/PluginSettings.java b/src/main/java/org/micromanager/lightsheetmanager/model/PluginSettings.java
index e411048..749cd27 100644
--- a/src/main/java/org/micromanager/lightsheetmanager/model/PluginSettings.java
+++ b/src/main/java/org/micromanager/lightsheetmanager/model/PluginSettings.java
@@ -10,6 +10,9 @@ public class PluginSettings {
private boolean isPollingPositions_ = true;
+ // volatile: written from the EDT (Settings tab) and read from the acquisition thread
+ private volatile boolean acquireFailQuietly_ = false;
+
private final JoystickData joystick_ = new JoystickData();
private final XYZGrid xyzGrid_ = new XYZGrid();
@@ -30,6 +33,14 @@ public boolean isPollingPositions() {
return isPollingPositions_;
}
+ public void setAcquireFailQuietly(final boolean state) {
+ acquireFailQuietly_ = state;
+ }
+
+ public boolean isAcquireFailQuietly() {
+ return acquireFailQuietly_;
+ }
+
public String toJson() {
return new Gson().toJson(this);
}
diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java
index 03425fa..aa2649c 100644
--- a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java
+++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java
@@ -81,14 +81,14 @@ protected boolean validateSaveLocation() {
final String saveNamePrefix = acqSettings_.saveNamePrefix();
if (saveNamePrefix == null || saveNamePrefix.trim().isEmpty()) {
- studio_.logs().showError("The save name prefix is empty.\n\n"
+ model_.logging().reportError("The save name prefix is empty.\n\n"
+ "Set a name on the Datastore panel, or uncheck \"Save images during acquisition\".");
return false;
}
final String saveDirectory = acqSettings_.saveDirectory();
if (saveDirectory == null || saveDirectory.trim().isEmpty()) {
- studio_.logs().showError("The save directory is not set.\n\n"
+ model_.logging().reportError("The save directory is not set.\n\n"
+ "Set a directory on the Datastore panel, or uncheck "
+ "\"Save images during acquisition\".");
return false;
@@ -96,12 +96,12 @@ protected boolean validateSaveLocation() {
final File directory = new File(saveDirectory);
if (!directory.exists()) {
- studio_.logs().showError("The save directory does not exist:\n\n" + saveDirectory
+ model_.logging().reportError("The save directory does not exist:\n\n" + saveDirectory
+ "\n\nCreate it, or choose another directory on the Datastore panel.");
return false;
}
if (!directory.isDirectory()) {
- studio_.logs().showError("The save directory is a file, not a directory:\n\n"
+ model_.logging().reportError("The save directory is a file, not a directory:\n\n"
+ saveDirectory);
return false;
}
@@ -109,7 +109,7 @@ protected boolean validateSaveLocation() {
// catches the common cases without being authoritative; a real write failure still surfaces
// at finish(). Cheap enough to be worth keeping.
if (!directory.canWrite()) {
- studio_.logs().showError("The save directory is not writable:\n\n" + saveDirectory);
+ model_.logging().reportError("The save directory is not writable:\n\n" + saveDirectory);
return false;
}
@@ -138,7 +138,7 @@ protected boolean validateCameraFrameSizes() {
if (mismatch == null) {
return true;
}
- studio_.logs().showError("The imaging cameras have different frame sizes: " + mismatch
+ model_.logging().reportError("The imaging cameras have different frame sizes: " + mismatch
+ ".\n\nAcquiring with mismatched frame sizes crashes Micro-Manager outright, so this "
+ "acquisition was not started.\n\nSet the same ROI and binning on every imaging "
+ "camera from the Camera tab, then try again.");
@@ -157,6 +157,7 @@ public AcquisitionEngine(final LightSheetManager model) {
acqSettings_ = asb_.build();
}
+
//public abstract DefaultAcquisitionSettingsDISPIM settings();
//public abstract This is the right unit for talking to the Core and for describing the images the camera
+ * actually delivers. Anything that scales with the rows the sensor physically clocks out wants
+ * {@link #getUnbinnedROI()} instead.
*/
- // TODO: take binning into account
public Rectangle getROI() {
Rectangle roi = new Rectangle();
try {
@@ -58,6 +61,49 @@ public Rectangle getROI() {
return roi;
}
+ /**
+ * Returns this camera's ROI in unbinned pixels, the physical rows the sensor reads out.
+ *
+ * Use this for every value derived from how much of the sensor is read: readout time, reset
+ * time, and the auto sheet width. Binning does not change how many rows are clocked out, only
+ * how they are combined on the way off the chip, so a 2304-row sensor takes the same time to
+ * read at 2x2 as at 1x1. {@code core.getROI()} reports 1152 rows there, so using it directly
+ * halves every derived time and the slice ends up shorter than the camera can service.
+ *
+ * A camera that cannot report its binning falls back to a factor of 1 and logs it. That
+ * reproduces the behaviour from before binning was handled at all rather than failing the run
+ * outright, which matters because this feeds timing on paths that otherwise work.
+ *
+ * @return the ROI in unbinned pixels
+ */
+ public Rectangle getUnbinnedROI() {
+ final Rectangle roi = getROI();
+ final int binning = binningOrOne();
+ if (binning <= 1) {
+ return roi;
+ }
+ // some cameras report a negative offset, so clamp before scaling it up
+ return new Rectangle(
+ Math.max(0, roi.x) * binning,
+ Math.max(0, roi.y) * binning,
+ roi.width * binning,
+ roi.height * binning);
+ }
+
+ /**
+ * Reads this camera's binning for scaling an ROI, or returns 1 and logs if it cannot.
+ */
+ private int binningOrOne() {
+ final int binning = binningOrUnknown(this);
+ if (binning == UNKNOWN_BINNING) {
+ studio_.logs().logError("could not read binning for camera " + deviceName_
+ + "; treating its ROI as unbinned, which underestimates readout and reset "
+ + "time whenever binning is actually in use");
+ return 1;
+ }
+ return binning;
+ }
+
/**
* Applies an ROI to this camera, in binned pixels.
*
diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/HamamatsuCamera.java b/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/HamamatsuCamera.java
index 8a7104b..f8004f8 100644
--- a/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/HamamatsuCamera.java
+++ b/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/HamamatsuCamera.java
@@ -149,7 +149,7 @@ public double getReadoutTime(final CameraMode cameraMode) {
if (getProperty(Properties.CAMERA_BUS).equals(Values.USB3)) {
readoutTimeMs = 10000; // absurdly large, light sheet mode over USB3 isn't supported by Flash4, but we are set up to decide available modes by device library and not a property
} else {
- Rectangle roi = getROI();
+ Rectangle roi = getUnbinnedROI();
readoutTimeMs = getRowReadoutTime() * roi.height;
}
break;
@@ -158,7 +158,7 @@ public double getReadoutTime(final CameraMode cameraMode) {
double rowReadoutTime = getRowReadoutTime();
int numReadoutRows;
- Rectangle roi = getROI();
+ Rectangle roi = getUnbinnedROI();
Rectangle sensorSize = getResolution();
if (getProperty(Properties.CAMERA_BUS).equals(Values.USB3)) {
diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/PcoCamera.java b/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/PcoCamera.java
index 1e45c41..e055736 100644
--- a/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/PcoCamera.java
+++ b/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/PcoCamera.java
@@ -122,7 +122,7 @@ public double getReadoutTime(CameraMode cameraMode) {
double readoutTimeMs = 10.0;
switch (cameraMode) {
case VIRTUAL_SLIT:
- Rectangle roi = getROI();
+ Rectangle roi = getUnbinnedROI();
final double rowReadoutTime = getRowReadoutTime();
int speedFactor = 1; // props_.getPropValueInteger(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_LS_SHUTTER_SPEED);
// if (speedFactor < 1) {
@@ -135,7 +135,7 @@ public double getReadoutTime(CameraMode cameraMode) {
double rowReadoutTime2 = getRowReadoutTime();
int numReadoutRows;
- Rectangle roi2 = getROI();
+ Rectangle roi2 = getUnbinnedROI();
Rectangle sensorSize = getResolution();
numReadoutRows = roiReadoutRowsSplitReadout(roi2, sensorSize);
diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/PvCamera.java b/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/PvCamera.java
index bfac4b4..a5d9623 100644
--- a/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/PvCamera.java
+++ b/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/PvCamera.java
@@ -88,7 +88,7 @@ public Rectangle getResolution() {
@Override
public double getRowReadoutTime() {
- Rectangle roi = getROI();
+ Rectangle roi = getUnbinnedROI();
if (hasProperty(Properties.READOUT_TIME)) {
final double readoutTimeMs = getPropertyFloat(Properties.READOUT_TIME) / 1e6;
return (readoutTimeMs / roi.height);