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 > T settingsBuilder(); @@ -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; } @@ -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 } @@ -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 @@ -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(); @@ -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); } diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineDispim.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineDispim.java index 851953a..f344960 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineDispim.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineDispim.java @@ -367,6 +367,14 @@ public AcquisitionEvent run(AcquisitionEvent event) { // TODO: Cameras are now ready to receive triggers, so we can send (software) trigger // to the tiger to tell it to start outputting TTLs + // When the acquisition finishes, AcqEngJ runs every hook once more with the + // finished event so they can shut down. No cameras are armed for it, so triggering + // the controller here starts a scan whose frames nobody collects and leaves the + // scanner RUNNING as teardown begins. + if (event.isAcquisitionFinishedEvent()) { + return event; + } + if (isUsingPLC && controllerInstance != null) { // if not in demo mode int side = 0; // TODO: enable 2 sided acquisition diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java index 6fe60a1..eac12c7 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java @@ -23,7 +23,6 @@ import org.micromanager.lightsheetmanager.api.data.SaveMode; import org.micromanager.lightsheetmanager.api.internal.DefaultTimingSettings; import org.micromanager.lightsheetmanager.api.internal.ScapeAcquisitionSettings; -import org.micromanager.lightsheetmanager.gui.utils.DialogUtils; import org.micromanager.lightsheetmanager.LightSheetManager; import org.micromanager.lightsheetmanager.model.PLogicScape; import org.micromanager.lightsheetmanager.model.devices.DeviceAdapter; @@ -179,9 +178,11 @@ boolean run() { // if X speed is less than 0.2 mm/s then it probably wasn't restored to correct speed some other time if (origSpeedX_ < 0.2) { - final boolean result = DialogUtils.showYesNoDialog(null, "Change Speed", + // quiet answer is "Yes": the small speed is residue of an interrupted scan, and + // an unattended run should take the recovery path rather than hang on a dialog + final boolean result = model_.logging().confirmOrDefault("Change Speed", "Max speed of X axis is small, perhaps it was not correctly restored after " + - "stage scanning previously. Do you want to set it to 1 mm/s now?"); + "stage scanning previously. Do you want to set it to 1 mm/s now?", true); if (result) { xyStage.setSpeedX(1.0); // origSpeedX_ is the value finish() restores, so it has to track the change we @@ -476,6 +477,16 @@ public void close() { public AcquisitionEvent run(AcquisitionEvent event) { // TODO: Cameras are now ready to receive triggers, so we can send (software) trigger // to the tiger to tell it to start outputting TTLs + + // When the acquisition finishes, AcqEngJ runs every hook once more with the + // finished event so they can shut down. No cameras are armed for it, so triggering + // the controller here starts a scan whose frames nobody collects and leaves the + // scanner RUNNING as teardown begins. The before-hardware hook above already + // guards this; this one did not. + if (event.isAcquisitionFinishedEvent()) { + return event; + } + if (isUsingPLC) { if (acqSettings_.stageScan().enabled() && acqSettings_.isUsingMultiplePositions()) { final ASIXYStage xyStage = model_.devices().device("SampleXY"); diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/AndorCamera.java b/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/AndorCamera.java index a5a5702..4aaa6f3 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/AndorCamera.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/AndorCamera.java @@ -173,7 +173,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) { @@ -186,7 +186,7 @@ public double getReadoutTime(CameraMode cameraMode) { double rowReadoutTime2 = getRowReadoutTime(); int numReadoutRows; - Rectangle roi2 = getROI(); + Rectangle roi2 = getUnbinnedROI(); Rectangle sensorSize = getResolution(); numReadoutRows = roiReadoutRowsSplitReadout(roi2, sensorSize); readoutTimeMs = numReadoutRows * rowReadoutTime2; diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/CameraBase.java b/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/CameraBase.java index fe88552..5df1c6e 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/CameraBase.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/devices/cameras/CameraBase.java @@ -46,8 +46,11 @@ public double getExposure() { /** * Returns this camera's ROI in binned pixels, the unit {@code core.setROI()} uses. + * + *

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