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 @@ -67,6 +67,17 @@ public void changedUpdate(DocumentEvent e) {
getDocument().addDocumentListener(docListener);
}

/**
* Marks the field valid or invalid by setting its background color.
* <p>
* The caller decides what valid means and owns any tooltip explaining it.
*
* @param isValid {@code false} to show the error color
*/
public void setValid(final boolean isValid) {
setBackground(isValid ? defaultColor_ : ERROR_COLOR);
}

/**
* Returns {@code true} if the filename is valid on Windows.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public void createUserInterface() {
txtSaveDirectory_ = new TextField();
txtSaveDirectory_.setEditable(false);
txtSaveDirectory_.setColumns(18);
txtSaveDirectory_.setForeground(Color.BLACK);
txtSaveDirectory_.setForeground(Color.WHITE);
txtSaveDirectory_.setText(acqSettings.saveDirectory());

txtSaveFileName_ = new TextField();
Expand All @@ -118,6 +118,7 @@ public void createUserInterface() {
btnLoadSettings_ = new Button("Load", 60, 20);
btnConvertSettings_ = new Button("Convert", 72, 20);

txtSaveDirectory_.setToolTipText("The directory to save images to.");
btnBrowse_.setToolTipText("Select the save directory with the file browser.");
btnOpen_.setToolTipText("Open the file explorer to the save directory.");
btnSaveSettings_.setToolTipText("Save the current acquisition settings to JSON.");
Expand Down Expand Up @@ -149,9 +150,13 @@ public void createEventHandlers() {
if (result != null) {
model_.acquisitions().settingsBuilder().saveDirectory(result.toString());
txtSaveDirectory_.setText(result.toString());
validateSaveDirectory();
}
});

// flag a saved directory that no longer exists as soon as the plugin opens
validateSaveDirectory();

// use the text field so we don't need to update settings
btnOpen_.registerListener(
() -> openDirectory(txtSaveDirectory_.getText()));
Expand Down Expand Up @@ -214,6 +219,31 @@ public void createEventHandlers() {
});
}

// Colors the save directory field and explains why in its tooltip when the path is unusable.
// Only called where the value arrives from outside the field (plugin load, browse, settings
// change); this touches the filesystem, and checking a dead network path can block the EDT.
private void validateSaveDirectory() {
final String path = txtSaveDirectory_.getText();
final String problem;
if (path == null || path.trim().isEmpty()) {
problem = "The save directory is not set.";
} else {
final File directory = new File(path);
if (!directory.exists()) {
problem = "This directory does not exist: " + path;
} else if (!directory.isDirectory()) {
problem = "This is a file, not a directory: " + path;
} else if (!directory.canWrite()) {
problem = "This directory is not writable: " + path;
} else {
problem = null;
}
}
txtSaveDirectory_.setValid(problem == null);
txtSaveDirectory_.setToolTipText(
problem == null ? "The directory to save images to." : problem);
}

// Opens the file explorer to the save directory
private void openDirectory(final String path) {
final File directory = new File(path);
Expand All @@ -223,21 +253,22 @@ private void openDirectory(final String path) {
try {
desktop.open(directory);
} catch (IOException e) {
model_.studio().logs().logError(
"Could not open the save directory.");
model_.studio().logs().showError(
"Could not open the save directory:\n\n" + path);
}
} else {
model_.studio().logs().logError(
"Desktop is not supported on this platform.");
model_.studio().logs().showError(
"Opening a file explorer is not supported on this platform.");
}
} else {
model_.studio().logs().logError("Directory does not exist.");
model_.studio().logs().showError("The save directory does not exist:\n\n" + path);
}
}

@Override
public void onSettingsChanged(final AcquisitionSettings settings) {
txtSaveDirectory_.setText(settings.saveDirectory());
validateSaveDirectory();
txtSaveFileName_.setText(settings.saveNamePrefix());
cbxSaveMode_.setSelected(settings.saveMode());
cbxSaveWhileAcquiring_.setSelected(settings.isSavingImagesDuringAcquisition());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -484,9 +484,9 @@ public boolean prepareControllerForAcquisitionSide(
// The galvo/sheet offset must always land on the true imaging plane. For a stage scan the
// piezo convention parks piezoCenter at home (~0); using that here would place the sheet on
// the home plane instead of the imaging center, so the acquired volume is offset from the
// stage sweep -- the #404 "scan not centered" symptom. diSPIM 1.4 SCOPE never writes the
// galvo offset during a stage scan (it holds the value the Setup tab computed from the
// imaging center); LSM writes it at acq time, so derive it from the imaging center here.
// stage sweep. diSPIM 1.4 SCOPE never writes the galvo offset during a stage scan (it holds
// the value the Setup tab computed from the imaging center); LSM writes it at acq time, so
// derive it from the imaging center here.
final double galvoCenter = settings.stageScan().enabled() ? imagingCenter : piezoCenter;
double sliceCenter = (galvoCenter - sliceOffset) / sliceRate;

Expand Down Expand Up @@ -727,7 +727,7 @@ public boolean setupHardwareChannelSwitching(final ScapeAcquisitionSettings sett
// 2. (newer) with 7-channel TTL-triggered on PLogic card shared with single camera trigger output (i.e. not dual-view system)
// however they share some things like using cells 17-24 and building a 3-input LUT which code is just copy/paste right now
final boolean isSevenChannelShutter = plcLaser_.getShutterMode() == ASIPLogic.ShutterMode.SEVEN_CHANNEL_SHUTTER;
final boolean isSevenChannelShutterTTL = plcLaser_.getShutterMode() == ASIPLogic.ShutterMode.SEVEN_CHANNEL_SHUTTER;
final boolean isSevenChannelShutterTTL = plcLaser_.getShutterMode() == ASIPLogic.ShutterMode.SEVEN_CHANNEL_TTL_SHUTTER;

if (isSevenChannelShutter) {
// original special 7-channel case
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.micromanager.lightsheetmanager.model.channels.ChannelSpec;
import org.micromanager.lightsheetmanager.model.devices.cameras.CameraBase;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -56,6 +57,64 @@ public abstract class AcquisitionEngine implements AcquisitionManager, MMAcquist

protected final LightSheetManager model_;

/**
* Validates that the acquisition can actually be written to disk, before anything is acquired.
* <p>
* The images are written by {@code finish()}, i.e. only AFTER the run completes, so without this
* check an unusable save location is discovered at the very end and the data is lost. Observed
* 2026-07-27: a 41 s dual-camera run ended in "could not save the acquisition data to:
* D:\SCOPE\test\Test" because {@code D:\SCOPE\test} did not exist. The same path also silently
* drops {@code acq_settings.json} and {@code position_list.pos}, which are written up front.
* <p>
* Called from both geometry engines' {@code setup()} before any hardware is touched, so a failure
* costs nothing and leaves the microscope untouched.
*
* @return true if saving is off, or the save location is usable; false to abort setup
*/
protected boolean validateSaveLocation() {
if (!acqSettings_.isSavingImagesDuringAcquisition()) {
return true; // not saving => nothing to validate
}

final String saveNamePrefix = acqSettings_.saveNamePrefix();
if (saveNamePrefix == null || saveNamePrefix.trim().isEmpty()) {
studio_.logs().showError("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"
+ "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
+ "\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"
+ 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);
return false;
}

studio_.logs().logMessage("save location validated: " + saveDirectory
+ File.separator + saveNamePrefix);
return true;
}

public AcquisitionEngine(final LightSheetManager model) {
model_ = Objects.requireNonNull(model);
studio_ = model.studio();
Expand Down Expand Up @@ -146,7 +205,9 @@ public Future<?> requestRun(boolean speedTest) {

try {
if (!setup()) {
studio_.logs().showError("Error during setup!");
// every setup() failure path already showed its own specific error,
// so log this rather than stacking a second dialog on top of it
studio_.logs().logMessage("Error during setup!");
return; // early exit => stop acquisition
}
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ boolean setup() {
// make settings current
updateSettings();

// fail before touching any hardware: the datastore is written by finish(), so an unusable
// save location would otherwise cost a full acquisition before it is discovered
if (!validateSaveLocation()) {
return false; // early exit => save location unusable
}

return true;
}

Expand Down Expand Up @@ -248,7 +254,7 @@ boolean run() {

//////////// Acquisition hooks ////////////////////
// These functions will be run on different threads during the acquisition process
// Hooks will run on the Acquisition Engine thread--the one that controls all hardware
// Hooks will run on the Acquisition Engine thread, the one that controls all hardware

// TODO add any code that needs to be executed on the acquisition thread (i.e. the one
// that controls hardware)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ boolean setup() {
// make settings current
updateSettings();

// fail before touching any hardware: the datastore is written by finish(), so an unusable
// save location would otherwise cost a full acquisition before it is discovered
if (!validateSaveLocation()) {
return false; // early exit => save location unusable
}

// // check pixel size
// if (core_.getPixelSizeUm() < 1e-6) {
// studio_.logs().showError(
Expand Down Expand Up @@ -321,7 +327,7 @@ boolean run() {

//////////// Acquisition hooks ////////////////////
// These functions will be run on different threads during the acquisition process
// Hooks will run on the Acquisition Engine thread--the one that controls all hardware
// Hooks will run on the Acquisition Engine thread, the one that controls all hardware

// TODO add any code that needs to be executed on the acquisition thread (i.e. the one
// that controls hardware)
Expand Down Expand Up @@ -628,7 +634,7 @@ public void close() {
// software "Every Volume" multichannel submits ONE event
// iterator PER channel, so AcqEngJ flushes a SequenceEnd between
// channels (Engine.java:187) and the controller re-fires once per
// channel-volume -- mirroring 1.4's per-channel loop and LSM's own
// channel-volume, mirroring 1.4's per-channel loop and LSM's own
// per-timepoint loop. Submitting all channels in a single iterator lets
// AcqEngJ merge identical-preset channels into one sequence that fires the
// controller once, collapsing the channel dimension (hang on hardware,
Expand All @@ -643,7 +649,7 @@ public void close() {
} else {
// Hardware channel modes (SLICE_HW / VOLUME_HW with hardware timepoints
// off): the controller emits all channels in one trigger, so a single
// merged iterator is correct here -- unchanged.
// merged iterator is correct here, unchanged.
currentAcquisition_.submitEventIterator(
LightSheetEventAdapter.createChannelAcqEvents(
baseEvent.copy(), acqSettings_, cameraNames, null));
Expand Down Expand Up @@ -959,20 +965,22 @@ private boolean doHardwareCalculations(PLogicScape plc) {
// TODO: implement multiple positions using hardware time points, currently
// set hardware time points to false if using multiple positions
if (acqSettings_.isUsingMultiplePositions()) {
if (acqSettings_.isUsingHardwareTimePoints()) {
if (isUsingHardwareTimePoints) {
// || acqSettings_.numTimePoints() > 1)
// && (timepointIntervalMs < timepointDuration*1.2)) {
asb_.useHardwareTimePoints(false);
isUsingHardwareTimePoints = false;
// studio_.logs().showError("Time point interval may not be sufficient "
// + "depending on actual time required to change positions. "
// + "Proceed at your own risk.");
}
}

// only use hardware time points when use time points is checked
if (acqSettings_.isUsingHardwareTimePoints()) {
if (isUsingHardwareTimePoints) {
if (!acqSettings_.isUsingTimePoints()) {
asb_.useHardwareTimePoints(false);
isUsingHardwareTimePoints = false;
}
}

Expand Down
Loading
Loading