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 @@ -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;
Expand All @@ -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_;
Expand All @@ -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);
Expand Down Expand Up @@ -164,6 +167,18 @@ public String setupErrorMessage() {
return errorText_;
}

/**
* The plugin's logging and error-reporting service.
*
* <p>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_;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
Expand Down Expand Up @@ -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_, "");
}

Expand Down Expand Up @@ -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
Expand Down
156 changes: 156 additions & 0 deletions src/main/java/org/micromanager/lightsheetmanager/model/Logging.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Two method families with different contracts:
* <ul>
* <li>{@code logMessage}/{@code logDebugMessage}/{@code logError} write to the CoreLog and
* never show UI, so they are safe from any thread in any mode.</li>
* <li>{@code reportError}/{@code confirmOrDefault} are user-facing and honor the
* "Acquisition failures are quiet" setting: quiet only logs, otherwise the standard
* dialogs are also shown.</li>
* </ul>
*
* <p>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.
*
* <p>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.
*
* <p>{@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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,35 +81,35 @@ 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;
}

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;
}
// canWrite() is advisory on Windows (it reports the read-only attribute, not the ACL), so it
// 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;
}

Expand Down Expand Up @@ -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.");
Expand All @@ -157,6 +157,7 @@ public AcquisitionEngine(final LightSheetManager model) {
acqSettings_ = asb_.build();
}


//public abstract DefaultAcquisitionSettingsDISPIM settings();

//public abstract <T extends DefaultAcquisitionSettings.Builder<Builder>> T settingsBuilder();
Expand Down Expand Up @@ -203,7 +204,7 @@ public Future<?> requestRun(boolean speedTest) {
// Run on a new thread, so it doesn't block the EDT
Future<?> acqFinished = acquisitionExecutor_.submit(() -> {
if (currentAcquisition_ != null) {
studio_.logs().showError("Acquisition is already running.");
model_.logging().reportError("Acquisition is already running.");
return;
}

Expand All @@ -220,7 +221,7 @@ public Future<?> requestRun(boolean speedTest) {
acqSettings_.saveNamePrefix(),
core_, acqSettings_.numTimePoints(), true);
} catch (Exception e) {
studio_.logs().showError(e);
model_.logging().reportError(e);
}
return; // early exit => do speed test
}
Expand All @@ -241,17 +242,17 @@ public Future<?> requestRun(boolean speedTest) {
return; // early exit => stop acquisition
}
} catch (Exception e) {
studio_.logs().showError(e, "Error during acquisition setup");
model_.logging().reportError(e, "Error during acquisition setup");
return; // early exit => stop acquisition
}
run(); // run the acquisition and block until complete
} catch (Exception e) {
studio_.logs().showError(e);
model_.logging().reportError(e);
} finally {
try {
finish(); // cleanup any resources
} catch (Exception e) {
studio_.logs().showError(e, "Error during acquisition cleanup");
model_.logging().reportError(e, "Error during acquisition cleanup");
} finally {
// must ALWAYS run: if currentAcquisition_ is left set, every future
// acquisition is rejected until the plugin restarts
Expand All @@ -273,7 +274,7 @@ public Future<?> requestRun(boolean speedTest) {
@Override
public void requestStop() {
if (currentAcquisition_ == null || currentAcquisition_.getDataSink().isFinished()) {
studio_.logs().showError("Acquisition is not running.");
model_.logging().reportError("Acquisition is not running.");
return;
}
currentAcquisition_.abort();
Expand All @@ -282,7 +283,7 @@ public void requestStop() {
@Override
public void requestPause() {
if (currentAcquisition_ == null) {
studio_.logs().showError("Acquisition is not running.");
model_.logging().reportError("Acquisition is not running.");
} else {
currentAcquisition_.setPaused(true);
}
Expand Down
Loading
Loading