From f86a9483cd3e89b7ebd39c74b15f24115303e5a0 Mon Sep 17 00:00:00 2001 From: Jerome Mutterer Date: Tue, 2 Jun 2026 14:12:21 +0200 Subject: [PATCH 1/5] add enderscope stage device adapter --- DeviceAdapters/EnderscopeStage/CMakeLists.txt | 18 + .../EnderscopeStage/EnderscopeStage.cpp | 1020 +++++++++++++++++ .../EnderscopeStage/EnderscopeStage.h | 144 +++ DeviceAdapters/EnderscopeStage/Makefile.am | 10 + DeviceAdapters/EnderscopeStage/README.md | 46 + 5 files changed, 1238 insertions(+) create mode 100644 DeviceAdapters/EnderscopeStage/CMakeLists.txt create mode 100644 DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp create mode 100644 DeviceAdapters/EnderscopeStage/EnderscopeStage.h create mode 100644 DeviceAdapters/EnderscopeStage/Makefile.am create mode 100644 DeviceAdapters/EnderscopeStage/README.md diff --git a/DeviceAdapters/EnderscopeStage/CMakeLists.txt b/DeviceAdapters/EnderscopeStage/CMakeLists.txt new file mode 100644 index 000000000..c427b6b66 --- /dev/null +++ b/DeviceAdapters/EnderscopeStage/CMakeLists.txt @@ -0,0 +1,18 @@ +set(module_name EnderscopeStage) + +set(module_srcs + EnderscopeStage.cpp +) + +set(module_hdrs + EnderscopeStage.h +) + +# In mmCoreAndDevices, this target is usually created by helper macros in parent CMake. +# If your parent build uses a different pattern, adapt this block accordingly. +add_library(${module_name} MODULE ${module_srcs} ${module_hdrs}) + +target_include_directories(${module_name} + PRIVATE + ${CMAKE_SOURCE_DIR}/MMDevice +) diff --git a/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp b/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp new file mode 100644 index 000000000..c07d30428 --- /dev/null +++ b/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp @@ -0,0 +1,1020 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EnderscopeStage.cpp +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +// +// DESCRIPTION: Enderscope Stage adapter (Marlin/Enderscope-compatible) +// Adapted in spirit from the Marzhauser-LStep adapter shape. +/////////////////////////////////////////////////////////////////////////////// + +#ifdef WIN32 +#pragma warning(disable : 4355) +#endif + +#include "EnderscopeStage.h" + +#include "MMDevice.h" +#include "ModuleInterface.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +const char* g_EnderscopeXYStageDeviceName = "EnderscopeXYStage"; +const char* g_EnderscopeZStageDeviceName = "EnderscopeZStage"; + +namespace +{ +const long kDefaultBaudRate = 115200; +const long kDefaultReadTimeoutMs = 1000; +const double kDefaultStepSizeUm = 1.0; + +const char* kGCodeAbsolute = "G90"; +const char* kGCodeRelative = "G91"; +const char* kGCodeHomeAll = "G28"; +const char* kGCodeHomeXY = "G28 X Y"; +const char* kGCodeHomeZ = "G28 Z"; +const char* kGCodeFinish = "M400"; +const char* kGCodePosition = "M114"; +const char* kGCodeStop = "M410"; + +inline long RoundToLong(double value) +{ + return static_cast(value >= 0.0 ? value + 0.5 : value - 0.5); +} +} // namespace + +MODULE_API void InitializeModuleData() +{ + RegisterDevice(g_EnderscopeXYStageDeviceName, MM::XYStageDevice, "Enderscope XY Stage (Marlin G-code)"); + RegisterDevice(g_EnderscopeZStageDeviceName, MM::StageDevice, "Enderscope Z Stage (Marlin G-code)"); +} + +MODULE_API MM::Device* CreateDevice(const char* deviceName) +{ + if (deviceName == 0) + { + return 0; + } + + if (strcmp(deviceName, g_EnderscopeXYStageDeviceName) == 0) + { + return new EnderscopeXYStage(); + } + + if (strcmp(deviceName, g_EnderscopeZStageDeviceName) == 0) + { + return new EnderscopeZStage(); + } + + return 0; +} + +MODULE_API void DeleteDevice(MM::Device* pDevice) +{ + delete pDevice; +} + +EnderscopeBase::EnderscopeBase(MM::Device* device) + : initialized_(false), + port_("Undefined"), + baudRate_(kDefaultBaudRate), + readTimeoutMs_(kDefaultReadTimeoutMs), + device_(device), + core_(0) +{ +} + +EnderscopeBase::~EnderscopeBase() {} + +int EnderscopeBase::CheckDeviceStatus() +{ + if (core_ == 0) + { + return DEVICE_NOT_CONNECTED; + } + + int ret = ClearPort(); + if (ret != DEVICE_OK) + { + return ret; + } + + double x = 0.0; + double y = 0.0; + double z = 0.0; + ret = QueryPositionMm(x, y, z); + if (ret != DEVICE_OK) + { + return ret; + } + + initialized_ = true; + return DEVICE_OK; +} + +int EnderscopeBase::ClearPort() +{ + if (core_ == 0) + { + return DEVICE_NOT_CONNECTED; + } + + const int bufSize = 255; + unsigned char clear[bufSize]; + unsigned long read = bufSize; + + int ret = DEVICE_OK; + while (static_cast(read) == bufSize) + { + ret = core_->ReadFromSerial(device_, port_.c_str(), clear, bufSize, read); + if (ret != DEVICE_OK) + { + return ret; + } + } + + return DEVICE_OK; +} + +int EnderscopeBase::SendCommand(const std::string& command) const +{ + if (core_ == 0) + { + return DEVICE_NOT_CONNECTED; + } + + const char* txTerm = "\n"; + return core_->SetSerialCommand(device_, port_.c_str(), command.c_str(), txTerm); +} + +int EnderscopeBase::ReadLine(std::string& line) const +{ + if (core_ == 0) + { + return DEVICE_NOT_CONNECTED; + } + + const size_t bufSize = 2048; + char buffer[bufSize]; + memset(buffer, 0, bufSize); + + const char* rxTerm = "\n"; + int ret = core_->GetSerialAnswer(device_, port_.c_str(), bufSize, buffer, rxTerm); + if (ret != DEVICE_OK) + { + return ret; + } + + line = Trim(buffer); + return DEVICE_OK; +} + +int EnderscopeBase::CommandExpectOk(const std::string& command) const +{ + int ret = SendCommand(command); + if (ret != DEVICE_OK) + { + return ret; + } + + const long maxReads = std::max(1L, readTimeoutMs_ / 10L); + for (long i = 0; i < maxReads; ++i) + { + std::string line; + ret = ReadLine(line); + if (ret != DEVICE_OK) + { + return ret; + } + + if (line.empty()) + { + continue; + } + + if (line.rfind("ok", 0) == 0) + { + return DEVICE_OK; + } + } + + return DEVICE_SERIAL_TIMEOUT; +} + +int EnderscopeBase::QueryPositionMm(double& x, double& y, double& z) const +{ + int ret = SendCommand(kGCodePosition); + if (ret != DEVICE_OK) + { + return ret; + } + + bool sawDataLine = false; + bool sawOk = false; + std::string dataLine; + + const long maxReads = std::max(2L, readTimeoutMs_ / 10L + 2L); + for (long i = 0; i < maxReads; ++i) + { + std::string line; + ret = ReadLine(line); + if (ret != DEVICE_OK) + { + return ret; + } + + if (line.empty()) + { + continue; + } + + if (line.rfind("ok", 0) == 0) + { + sawOk = true; + if (sawDataLine) + { + break; + } + continue; + } + + if (!sawDataLine) + { + dataLine = line; + sawDataLine = true; + } + } + + if (!sawDataLine || !sawOk) + { + return DEVICE_SERIAL_INVALID_RESPONSE; + } + + if (!ParseAxisValue(dataLine, 'X', x) || !ParseAxisValue(dataLine, 'Y', y) || !ParseAxisValue(dataLine, 'Z', z)) + { + return DEVICE_SERIAL_INVALID_RESPONSE; + } + + return DEVICE_OK; +} + +std::string EnderscopeBase::Trim(const std::string& input) +{ + const std::string whitespace = " \t\r\n"; + const size_t first = input.find_first_not_of(whitespace); + if (first == std::string::npos) + { + return std::string(); + } + + const size_t last = input.find_last_not_of(whitespace); + return input.substr(first, last - first + 1); +} + +bool EnderscopeBase::ParseAxisValue(const std::string& line, char axis, double& value) +{ + const std::string key = std::string(1, axis) + ":"; + const size_t pos = line.find(key); + if (pos == std::string::npos) + { + return false; + } + + const size_t numberStart = pos + key.size(); + size_t numberEnd = numberStart; + while (numberEnd < line.size()) + { + const char c = line[numberEnd]; + const bool numeric = (c == '+') || (c == '-') || (c == '.') || (c == 'e') || (c == 'E') || std::isdigit(static_cast(c)); + if (!numeric) + { + break; + } + ++numberEnd; + } + + if (numberEnd == numberStart) + { + return false; + } + + const std::string num = line.substr(numberStart, numberEnd - numberStart); + char* endPtr = 0; + value = std::strtod(num.c_str(), &endPtr); + return endPtr != num.c_str(); +} + +EnderscopeXYStage::EnderscopeXYStage() + : EnderscopeBase(this), + stepSizeXUm_(kDefaultStepSizeUm), + stepSizeYUm_(kDefaultStepSizeUm), + originXUm_(0.0), + originYUm_(0.0), + lastXUm_(0.0), + lastYUm_(0.0) +{ + InitializeDefaultErrorMessages(); + + SetErrorText(ERR_PORT_CHANGE_FORBIDDEN, "Port property cannot be changed after initialization."); + + CreateProperty(MM::g_Keyword_Name, g_EnderscopeXYStageDeviceName, MM::String, true); + CreateProperty(MM::g_Keyword_Description, "Enderscope XY stage adapter", MM::String, true); + + CPropertyAction* pAct = new CPropertyAction(this, &EnderscopeXYStage::OnPort); + CreateProperty(MM::g_Keyword_Port, "Undefined", MM::String, false, pAct, true); + + pAct = new CPropertyAction(this, &EnderscopeXYStage::OnBaudRate); + CreateProperty("BaudRate", CDeviceUtils::ConvertToString(baudRate_), MM::Integer, false, pAct, true); + + AddAllowedValue("BaudRate", "9600"); + AddAllowedValue("BaudRate", "57600"); + AddAllowedValue("BaudRate", "115200"); + AddAllowedValue("BaudRate", "250000"); + + pAct = new CPropertyAction(this, &EnderscopeXYStage::OnReadTimeout); + CreateProperty("ReadTimeoutMs", CDeviceUtils::ConvertToString(readTimeoutMs_), MM::Integer, false, pAct, true); + SetPropertyLimits("ReadTimeoutMs", 100, 10000); +} + +EnderscopeXYStage::~EnderscopeXYStage() +{ + Shutdown(); +} + +void EnderscopeXYStage::GetName(char* name) const +{ + CDeviceUtils::CopyLimitedString(name, g_EnderscopeXYStageDeviceName); +} + +int EnderscopeXYStage::Initialize() +{ + core_ = GetCoreCallback(); + + int ret = CheckDeviceStatus(); + if (ret != DEVICE_OK) + { + return ret; + } + + CPropertyAction* pAct = new CPropertyAction(this, &EnderscopeXYStage::OnStepSizeX); + ret = CreateProperty("StepSizeX [um]", CDeviceUtils::ConvertToString(stepSizeXUm_), MM::Float, false, pAct); + if (ret != DEVICE_OK) + { + return ret; + } + + pAct = new CPropertyAction(this, &EnderscopeXYStage::OnStepSizeY); + ret = CreateProperty("StepSizeY [um]", CDeviceUtils::ConvertToString(stepSizeYUm_), MM::Float, false, pAct); + if (ret != DEVICE_OK) + { + return ret; + } + + ret = UpdateStatus(); + if (ret != DEVICE_OK) + { + return ret; + } + + initialized_ = true; + return DEVICE_OK; +} + +int EnderscopeXYStage::Shutdown() +{ + initialized_ = false; + return DEVICE_OK; +} + +bool EnderscopeXYStage::Busy() +{ + return false; +} + +int EnderscopeXYStage::SetAbsoluteMm(double xMm, double yMm) +{ + int ret = CommandExpectOk(kGCodeAbsolute); + if (ret != DEVICE_OK) + { + return ret; + } + + ostringstream cmd; + cmd << "G0 X " << xMm << " Y " << yMm; + ret = CommandExpectOk(cmd.str()); + if (ret != DEVICE_OK) + { + return ret; + } + + return CommandExpectOk(kGCodeFinish); +} + +int EnderscopeXYStage::SetRelativeMm(double dxMm, double dyMm) +{ + int ret = CommandExpectOk(kGCodeRelative); + if (ret != DEVICE_OK) + { + return ret; + } + + ostringstream cmd; + cmd << "G0 X " << dxMm << " Y " << dyMm; + ret = CommandExpectOk(cmd.str()); + if (ret != DEVICE_OK) + { + return ret; + } + + return CommandExpectOk(kGCodeFinish); +} + +int EnderscopeXYStage::SetPositionUm(double x, double y) +{ + const double xMm = (x + originXUm_) / 1000.0; + const double yMm = (y + originYUm_) / 1000.0; + + const int ret = SetAbsoluteMm(xMm, yMm); + if (ret != DEVICE_OK) + { + return ret; + } + + lastXUm_ = x; + lastYUm_ = y; + return DEVICE_OK; +} + +int EnderscopeXYStage::GetPositionUm(double& x, double& y) +{ + double xMm = 0.0; + double yMm = 0.0; + double zMm = 0.0; + + int ret = QueryPositionMm(xMm, yMm, zMm); + if (ret != DEVICE_OK) + { + x = lastXUm_; + y = lastYUm_; + return ret; + } + + x = xMm * 1000.0 - originXUm_; + y = yMm * 1000.0 - originYUm_; + + lastXUm_ = x; + lastYUm_ = y; + return DEVICE_OK; +} + +int EnderscopeXYStage::SetRelativePositionUm(double dx, double dy) +{ + const int ret = SetRelativeMm(dx / 1000.0, dy / 1000.0); + if (ret != DEVICE_OK) + { + return ret; + } + + lastXUm_ += dx; + lastYUm_ += dy; + return DEVICE_OK; +} + +int EnderscopeXYStage::SetPositionSteps(long x, long y) +{ + const double xUm = static_cast(x) * stepSizeXUm_; + const double yUm = static_cast(y) * stepSizeYUm_; + return SetPositionUm(xUm, yUm); +} + +int EnderscopeXYStage::GetPositionSteps(long& x, long& y) +{ + double xUm = 0.0; + double yUm = 0.0; + int ret = GetPositionUm(xUm, yUm); + if (ret != DEVICE_OK) + { + return ret; + } + + x = RoundToLong(xUm / stepSizeXUm_); + y = RoundToLong(yUm / stepSizeYUm_); + return DEVICE_OK; +} + +int EnderscopeXYStage::SetRelativePositionSteps(long x, long y) +{ + const double dxUm = static_cast(x) * stepSizeXUm_; + const double dyUm = static_cast(y) * stepSizeYUm_; + return SetRelativePositionUm(dxUm, dyUm); +} + +int EnderscopeXYStage::Home() +{ + int ret = CommandExpectOk(kGCodeHomeXY); + if (ret != DEVICE_OK) + { + ret = CommandExpectOk(kGCodeHomeAll); + if (ret != DEVICE_OK) + { + return ret; + } + } + + ret = CommandExpectOk(kGCodeFinish); + if (ret != DEVICE_OK) + { + return ret; + } + + lastXUm_ = 0.0; + lastYUm_ = 0.0; + originXUm_ = 0.0; + originYUm_ = 0.0; + return DEVICE_OK; +} + +int EnderscopeXYStage::Stop() +{ + return CommandExpectOk(kGCodeStop); +} + +int EnderscopeXYStage::SetOrigin() +{ + double x = 0.0; + double y = 0.0; + int ret = GetPositionUm(x, y); + if (ret != DEVICE_OK) + { + return ret; + } + + originXUm_ += x; + originYUm_ += y; + lastXUm_ = 0.0; + lastYUm_ = 0.0; + return DEVICE_OK; +} + +int EnderscopeXYStage::SetAdapterOriginUm(double x, double y) +{ + double hwXmm = 0.0; + double hwYmm = 0.0; + double hwZmm = 0.0; + int ret = QueryPositionMm(hwXmm, hwYmm, hwZmm); + if (ret != DEVICE_OK) + { + return ret; + } + + originXUm_ = hwXmm * 1000.0 - x; + originYUm_ = hwYmm * 1000.0 - y; + lastXUm_ = x; + lastYUm_ = y; + return DEVICE_OK; +} + +int EnderscopeXYStage::GetLimitsUm(double& xMin, double& xMax, double& yMin, double& yMax) +{ + xMin = -std::numeric_limits::max(); + xMax = std::numeric_limits::max(); + yMin = -std::numeric_limits::max(); + yMax = std::numeric_limits::max(); + return DEVICE_OK; +} + +int EnderscopeXYStage::GetStepLimits(long& xMin, long& xMax, long& yMin, long& yMax) +{ + xMin = std::numeric_limits::min(); + xMax = std::numeric_limits::max(); + yMin = std::numeric_limits::min(); + yMax = std::numeric_limits::max(); + return DEVICE_OK; +} + +int EnderscopeXYStage::OnPort(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(port_.c_str()); + } + else if (eAct == MM::AfterSet) + { + if (initialized_) + { + pProp->Set(port_.c_str()); + return ERR_PORT_CHANGE_FORBIDDEN; + } + pProp->Get(port_); + } + + return DEVICE_OK; +} + +int EnderscopeXYStage::OnBaudRate(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(baudRate_); + } + else if (eAct == MM::AfterSet) + { + if (initialized_) + { + pProp->Set(baudRate_); + return DEVICE_CAN_NOT_SET_PROPERTY; + } + pProp->Get(baudRate_); + } + + return DEVICE_OK; +} + +int EnderscopeXYStage::OnReadTimeout(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(readTimeoutMs_); + } + else if (eAct == MM::AfterSet) + { + if (initialized_) + { + pProp->Set(readTimeoutMs_); + return DEVICE_CAN_NOT_SET_PROPERTY; + } + pProp->Get(readTimeoutMs_); + } + + return DEVICE_OK; +} + +int EnderscopeXYStage::OnStepSizeX(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(stepSizeXUm_); + } + else if (eAct == MM::AfterSet) + { + double value = 0.0; + pProp->Get(value); + if (value <= 0.0) + { + pProp->Set(stepSizeXUm_); + return DEVICE_INVALID_INPUT_PARAM; + } + stepSizeXUm_ = value; + } + + return DEVICE_OK; +} + +int EnderscopeXYStage::OnStepSizeY(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(stepSizeYUm_); + } + else if (eAct == MM::AfterSet) + { + double value = 0.0; + pProp->Get(value); + if (value <= 0.0) + { + pProp->Set(stepSizeYUm_); + return DEVICE_INVALID_INPUT_PARAM; + } + stepSizeYUm_ = value; + } + + return DEVICE_OK; +} + +EnderscopeZStage::EnderscopeZStage() + : EnderscopeBase(this), + stepSizeUm_(kDefaultStepSizeUm), + originZUm_(0.0), + lastZUm_(0.0) +{ + InitializeDefaultErrorMessages(); + + SetErrorText(ERR_PORT_CHANGE_FORBIDDEN, "Port property cannot be changed after initialization."); + + CreateProperty(MM::g_Keyword_Name, g_EnderscopeZStageDeviceName, MM::String, true); + CreateProperty(MM::g_Keyword_Description, "Enderscope Z stage adapter", MM::String, true); + + CPropertyAction* pAct = new CPropertyAction(this, &EnderscopeZStage::OnPort); + CreateProperty(MM::g_Keyword_Port, "Undefined", MM::String, false, pAct, true); + + pAct = new CPropertyAction(this, &EnderscopeZStage::OnBaudRate); + CreateProperty("BaudRate", CDeviceUtils::ConvertToString(baudRate_), MM::Integer, false, pAct, true); + + AddAllowedValue("BaudRate", "9600"); + AddAllowedValue("BaudRate", "57600"); + AddAllowedValue("BaudRate", "115200"); + AddAllowedValue("BaudRate", "250000"); + + pAct = new CPropertyAction(this, &EnderscopeZStage::OnReadTimeout); + CreateProperty("ReadTimeoutMs", CDeviceUtils::ConvertToString(readTimeoutMs_), MM::Integer, false, pAct, true); + SetPropertyLimits("ReadTimeoutMs", 100, 10000); +} + +EnderscopeZStage::~EnderscopeZStage() +{ + Shutdown(); +} + +void EnderscopeZStage::GetName(char* name) const +{ + CDeviceUtils::CopyLimitedString(name, g_EnderscopeZStageDeviceName); +} + +int EnderscopeZStage::Initialize() +{ + core_ = GetCoreCallback(); + + int ret = CheckDeviceStatus(); + if (ret != DEVICE_OK) + { + return ret; + } + + CPropertyAction* pAct = new CPropertyAction(this, &EnderscopeZStage::OnStepSize); + ret = CreateProperty("StepSize [um]", CDeviceUtils::ConvertToString(stepSizeUm_), MM::Float, false, pAct); + if (ret != DEVICE_OK) + { + return ret; + } + + ret = UpdateStatus(); + if (ret != DEVICE_OK) + { + return ret; + } + + initialized_ = true; + return DEVICE_OK; +} + +int EnderscopeZStage::Shutdown() +{ + initialized_ = false; + return DEVICE_OK; +} + +bool EnderscopeZStage::Busy() +{ + return false; +} + +int EnderscopeZStage::SetAbsoluteMm(double zMm) +{ + int ret = CommandExpectOk(kGCodeAbsolute); + if (ret != DEVICE_OK) + { + return ret; + } + + ostringstream cmd; + cmd << "G0 Z " << zMm; + ret = CommandExpectOk(cmd.str()); + if (ret != DEVICE_OK) + { + return ret; + } + + return CommandExpectOk(kGCodeFinish); +} + +int EnderscopeZStage::SetRelativeMm(double dzMm) +{ + int ret = CommandExpectOk(kGCodeRelative); + if (ret != DEVICE_OK) + { + return ret; + } + + ostringstream cmd; + cmd << "G0 Z " << dzMm; + ret = CommandExpectOk(cmd.str()); + if (ret != DEVICE_OK) + { + return ret; + } + + return CommandExpectOk(kGCodeFinish); +} + +int EnderscopeZStage::SetPositionUm(double pos) +{ + const double zMm = (pos + originZUm_) / 1000.0; + const int ret = SetAbsoluteMm(zMm); + if (ret != DEVICE_OK) + { + return ret; + } + + lastZUm_ = pos; + return DEVICE_OK; +} + +int EnderscopeZStage::SetRelativePositionUm(double d) +{ + const int ret = SetRelativeMm(d / 1000.0); + if (ret != DEVICE_OK) + { + return ret; + } + + lastZUm_ += d; + return DEVICE_OK; +} + +int EnderscopeZStage::GetPositionUm(double& pos) +{ + double xMm = 0.0; + double yMm = 0.0; + double zMm = 0.0; + + int ret = QueryPositionMm(xMm, yMm, zMm); + if (ret != DEVICE_OK) + { + pos = lastZUm_; + return ret; + } + + pos = zMm * 1000.0 - originZUm_; + lastZUm_ = pos; + return DEVICE_OK; +} + +int EnderscopeZStage::SetPositionSteps(long steps) +{ + const double posUm = static_cast(steps) * stepSizeUm_; + return SetPositionUm(posUm); +} + +int EnderscopeZStage::GetPositionSteps(long& steps) +{ + double posUm = 0.0; + int ret = GetPositionUm(posUm); + if (ret != DEVICE_OK) + { + return ret; + } + + steps = RoundToLong(posUm / stepSizeUm_); + return DEVICE_OK; +} + +int EnderscopeZStage::Stop() +{ + return CommandExpectOk(kGCodeStop); +} + +int EnderscopeZStage::Home() +{ + int ret = CommandExpectOk(kGCodeHomeZ); + if (ret != DEVICE_OK) + { + ret = CommandExpectOk(kGCodeHomeAll); + if (ret != DEVICE_OK) + { + return ret; + } + } + + ret = CommandExpectOk(kGCodeFinish); + if (ret != DEVICE_OK) + { + return ret; + } + + lastZUm_ = 0.0; + originZUm_ = 0.0; + return DEVICE_OK; +} + +int EnderscopeZStage::SetOrigin() +{ + double z = 0.0; + int ret = GetPositionUm(z); + if (ret != DEVICE_OK) + { + return ret; + } + + originZUm_ += z; + lastZUm_ = 0.0; + return DEVICE_OK; +} + +int EnderscopeZStage::SetAdapterOriginUm(double d) +{ + double xMm = 0.0; + double yMm = 0.0; + double zMm = 0.0; + int ret = QueryPositionMm(xMm, yMm, zMm); + if (ret != DEVICE_OK) + { + return ret; + } + + originZUm_ = zMm * 1000.0 - d; + lastZUm_ = d; + return DEVICE_OK; +} + +int EnderscopeZStage::GetLimits(double& min, double& max) +{ + min = -std::numeric_limits::max(); + max = std::numeric_limits::max(); + return DEVICE_OK; +} + +int EnderscopeZStage::OnPort(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(port_.c_str()); + } + else if (eAct == MM::AfterSet) + { + if (initialized_) + { + pProp->Set(port_.c_str()); + return ERR_PORT_CHANGE_FORBIDDEN; + } + pProp->Get(port_); + } + + return DEVICE_OK; +} + +int EnderscopeZStage::OnBaudRate(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(baudRate_); + } + else if (eAct == MM::AfterSet) + { + if (initialized_) + { + pProp->Set(baudRate_); + return DEVICE_CAN_NOT_SET_PROPERTY; + } + pProp->Get(baudRate_); + } + + return DEVICE_OK; +} + +int EnderscopeZStage::OnReadTimeout(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(readTimeoutMs_); + } + else if (eAct == MM::AfterSet) + { + if (initialized_) + { + pProp->Set(readTimeoutMs_); + return DEVICE_CAN_NOT_SET_PROPERTY; + } + pProp->Get(readTimeoutMs_); + } + + return DEVICE_OK; +} + +int EnderscopeZStage::OnStepSize(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(stepSizeUm_); + } + else if (eAct == MM::AfterSet) + { + double value = 0.0; + pProp->Get(value); + if (value <= 0.0) + { + pProp->Set(stepSizeUm_); + return DEVICE_INVALID_INPUT_PARAM; + } + stepSizeUm_ = value; + } + + return DEVICE_OK; +} diff --git a/DeviceAdapters/EnderscopeStage/EnderscopeStage.h b/DeviceAdapters/EnderscopeStage/EnderscopeStage.h new file mode 100644 index 000000000..b59e05cba --- /dev/null +++ b/DeviceAdapters/EnderscopeStage/EnderscopeStage.h @@ -0,0 +1,144 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EnderscopeStage.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +// +// DESCRIPTION: Enderscope Stage adapter (Marlin/Enderscope-compatible) +// Inspired by adapter structure used in Marzhauser-LStep. +/////////////////////////////////////////////////////////////////////////////// + +#ifndef _ENDERSCOPE_STAGE_H_ +#define _ENDERSCOPE_STAGE_H_ + +#include "DeviceBase.h" +#include "MMDeviceConstants.h" +#include + +extern const char* g_EnderscopeXYStageDeviceName; +extern const char* g_EnderscopeZStageDeviceName; + +class EnderscopeBase +{ +public: + explicit EnderscopeBase(MM::Device* device); + virtual ~EnderscopeBase(); + +protected: + int CheckDeviceStatus(); + int ClearPort(); + int SendCommand(const std::string& command) const; + int ReadLine(std::string& line) const; + int CommandExpectOk(const std::string& command) const; + int QueryPositionMm(double& x, double& y, double& z) const; + + static std::string Trim(const std::string& input); + static bool ParseAxisValue(const std::string& line, char axis, double& value); + +protected: + bool initialized_; + std::string port_; + long baudRate_; + long readTimeoutMs_; + + MM::Device* device_; + MM::Core* core_; +}; + +class EnderscopeXYStage : public CXYStageBase, public EnderscopeBase +{ +public: + EnderscopeXYStage(); + ~EnderscopeXYStage() override; + + int Initialize() override; + int Shutdown() override; + void GetName(char* name) const override; + bool Busy() override; + + int SetPositionUm(double x, double y) override; + int GetPositionUm(double& x, double& y) override; + int SetRelativePositionUm(double dx, double dy) override; + int SetPositionSteps(long x, long y) override; + int GetPositionSteps(long& x, long& y) override; + int SetRelativePositionSteps(long x, long y) override; + int Home() override; + int Stop() override; + int SetOrigin() override; + int SetAdapterOriginUm(double x, double y) override; + int GetLimitsUm(double& xMin, double& xMax, double& yMin, double& yMax) override; + int GetStepLimits(long& xMin, long& xMax, long& yMin, long& yMax) override; + + int IsXYStageSequenceable(bool& isSequenceable) const override + { + isSequenceable = false; + return DEVICE_OK; + } + + double GetStepSizeXUm() override { return stepSizeXUm_; } + double GetStepSizeYUm() override { return stepSizeYUm_; } + + int OnPort(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnBaudRate(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnReadTimeout(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnStepSizeX(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnStepSizeY(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + int SetAbsoluteMm(double xMm, double yMm); + int SetRelativeMm(double dxMm, double dyMm); + + double stepSizeXUm_; + double stepSizeYUm_; + double originXUm_; + double originYUm_; + + mutable double lastXUm_; + mutable double lastYUm_; +}; + +class EnderscopeZStage : public CStageBase, public EnderscopeBase +{ +public: + EnderscopeZStage(); + ~EnderscopeZStage() override; + + int Initialize() override; + int Shutdown() override; + void GetName(char* name) const override; + bool Busy() override; + + int SetPositionUm(double pos) override; + int SetRelativePositionUm(double d) override; + int GetPositionUm(double& pos) override; + int SetPositionSteps(long steps) override; + int GetPositionSteps(long& steps) override; + int Stop() override; + int Home() override; + int SetOrigin() override; + int SetAdapterOriginUm(double d) override; + int GetLimits(double& min, double& max) override; + + int IsStageSequenceable(bool& isSequenceable) const override + { + isSequenceable = false; + return DEVICE_OK; + } + + bool IsContinuousFocusDrive() const override { return false; } + + int OnPort(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnBaudRate(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnReadTimeout(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnStepSize(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + int SetAbsoluteMm(double zMm); + int SetRelativeMm(double dzMm); + + double stepSizeUm_; + double originZUm_; + + mutable double lastZUm_; +}; + +#endif // _ENDERSCOPE_STAGE_H_ diff --git a/DeviceAdapters/EnderscopeStage/Makefile.am b/DeviceAdapters/EnderscopeStage/Makefile.am new file mode 100644 index 000000000..400c2bae5 --- /dev/null +++ b/DeviceAdapters/EnderscopeStage/Makefile.am @@ -0,0 +1,10 @@ +AM_CXXFLAGS = $(MMDEVAPI_CXXFLAGS) +AM_LDFLAGS = $(MMDEVAPI_LDFLAGS) + +deviceadapter_LTLIBRARIES = libmmgr_dal_EnderscopeStage.la + +libmmgr_dal_EnderscopeStage_la_SOURCES = \ + EnderscopeStage.cpp \ + EnderscopeStage.h + +libmmgr_dal_EnderscopeStage_la_LIBADD = $(MMDEVAPI_LIBADD) diff --git a/DeviceAdapters/EnderscopeStage/README.md b/DeviceAdapters/EnderscopeStage/README.md new file mode 100644 index 000000000..5da61808c --- /dev/null +++ b/DeviceAdapters/EnderscopeStage/README.md @@ -0,0 +1,46 @@ +# EnderscopeStage Micro-Manager Adapter + +| Summary: | Interfaces with Enderscope/Marlin-compatible motion controllers over serial G-code | +| --- | --- | +| Author: | Jerome Mutterer and Erwan Grandgirard | +| License: | Same as the hosting Micro-Manager build/distribution | +| Platforms: | Should work on all platforms (serial interface) | +| Devices: | EnderscopeXYStage, EnderscopeZStage | +| Since version: | Local prototype (2026-05) | +| Serial port settings: | Typical: `115200 8N1`, handshaking off | + +This directory contains a new Micro-Manager device adapter that follows the structure of `Marzhauser-LStep/LStep.cpp` while targeting the `Stage` behavior in `enderscope.py` (Marlin-like G-code transport). + +## Devices Exported + +- `EnderscopeXYStage` (`MM::XYStageDevice`) +- `EnderscopeZStage` (`MM::StageDevice`) + +## Command Mapping to `enderscope.Stage` + +- `SetPositionUm(...)` -> `G90` then `G0 ...` then `M400` +- `SetRelativePositionUm(...)` -> `G91` then `G0 ...` then `M400` +- `GetPosition...` -> `M114` (parses `X:.. Y:.. Z:..`) +- `Home()` -> `G28 X Y` / `G28 Z` (fallback `G28`) then `M400` +- `Stop()` -> `M410` + +All stage coordinates are converted between Micro-Manager um and Enderscope mm. + +## Pre-initialization Properties + +- `Port` (serial port) +- `BaudRate` (default `115200`) +- `ReadTimeoutMs` (default `1000`) + +## Notes + +- Adapter currently reports unbounded limits. +- Adapter uses synchronous motion (`M400`) and returns `Busy() == false`. +- To compile in `mmCoreAndDevices`, place this folder under `DeviceAdapters/` and add it to that repository's build configuration (CMake or VS project lists, depending on platform/build system). + +## References + +1. **EnderScope: a low-cost 3D printer-based scanning microscope for microplastic detection.** *Philosophical Transactions of the Royal Society A*, 2024. + +2. **Enderscope.py: A library for computational imaging using the EnderScope automated microscope.** *SoftwareX*, 2025. + From 389450062748f8b1c1d812cdc015bc96e6ac90d0 Mon Sep 17 00:00:00 2001 From: Nico Stuurman Date: Thu, 4 Jun 2026 12:48:48 -0700 Subject: [PATCH 2/5] Enderscope: added to the windows build. --- .../EnderscopeStage/EnderscopeStage.cpp | 3 + .../EnderscopeStage/EnderscopeStage.h | 3 + .../EnderscopeStage/EnderscopeStage.vcxproj | 106 ++++++++++++++++++ .../EnderscopeStage.vcxproj.filters | 21 ++++ micromanager.sln | 12 ++ 5 files changed, 145 insertions(+) create mode 100644 DeviceAdapters/EnderscopeStage/EnderscopeStage.vcxproj create mode 100644 DeviceAdapters/EnderscopeStage/EnderscopeStage.vcxproj.filters diff --git a/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp b/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp index c07d30428..59f2568b0 100644 --- a/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp +++ b/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp @@ -8,6 +8,9 @@ /////////////////////////////////////////////////////////////////////////////// #ifdef WIN32 +// Prevent windows.h (pulled in transitively) from defining min/max macros, +// which break std::min/std::max. +#define NOMINMAX #pragma warning(disable : 4355) #endif diff --git a/DeviceAdapters/EnderscopeStage/EnderscopeStage.h b/DeviceAdapters/EnderscopeStage/EnderscopeStage.h index b59e05cba..813f69ed3 100644 --- a/DeviceAdapters/EnderscopeStage/EnderscopeStage.h +++ b/DeviceAdapters/EnderscopeStage/EnderscopeStage.h @@ -17,6 +17,9 @@ extern const char* g_EnderscopeXYStageDeviceName; extern const char* g_EnderscopeZStageDeviceName; +// Device adapter custom error codes (>= 10000 by MM convention) +constexpr int ERR_PORT_CHANGE_FORBIDDEN = 10004; + class EnderscopeBase { public: diff --git a/DeviceAdapters/EnderscopeStage/EnderscopeStage.vcxproj b/DeviceAdapters/EnderscopeStage/EnderscopeStage.vcxproj new file mode 100644 index 000000000..9d887451a --- /dev/null +++ b/DeviceAdapters/EnderscopeStage/EnderscopeStage.vcxproj @@ -0,0 +1,106 @@ + + + + + Debug + x64 + + + Release + x64 + + + + EnderscopeStage + {2e5b103b-0d6f-4d73-9309-0ecb807a5154} + EnderScopeStage + Win32Proj + 10.0 + + + + DynamicLibrary + MultiByte + v143 + false + + + DynamicLibrary + MultiByte + v143 + true + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + true + false + + + + X64 + + + Disabled + WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) + EnableFastChecks + true + + + 4290;%(DisableSpecificWarnings) + + + %(AdditionalLibraryDirectories) + Windows + + + + + + + X64 + + + WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + + + 4290;%(DisableSpecificWarnings) + + + %(AdditionalLibraryDirectories) + Windows + true + true + + + + + + + + + + + + + {b8c95f39-54bf-40a9-807b-598df2821d55} + + + + + + diff --git a/DeviceAdapters/EnderscopeStage/EnderscopeStage.vcxproj.filters b/DeviceAdapters/EnderscopeStage/EnderscopeStage.vcxproj.filters new file mode 100644 index 000000000..f2c637359 --- /dev/null +++ b/DeviceAdapters/EnderscopeStage/EnderscopeStage.vcxproj.filters @@ -0,0 +1,21 @@ + + + + + {bebef9e4-ef97-4571-863f-1f8b32d7bb19} + + + {9db17189-12bd-42a7-9a94-596b1827edd9} + + + + + Header Files + + + + + Source Files + + + diff --git a/micromanager.sln b/micromanager.sln index 1c9487827..438d7edb4 100644 --- a/micromanager.sln +++ b/micromanager.sln @@ -542,6 +542,10 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ThorlabsTSP01", "DeviceAdap EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SpinnakerC", "DeviceAdapters\SpinnakerC\SpinnakerC.vcxproj", "{4DEE8237-EF6F-426A-9DED-1909B6F3B18D}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NikonAZ100SDK", "SecretDeviceAdapters\NikonAZ100SDK\NikonAZ100SDK.vcxproj", "{F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EndersccopeStage", "DeviceAdapters\EnderscopeStage\EnderscopeStage.vcxproj", "{2e5b103b-0d6f-4d73-9309-0ecb807a5154}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -1628,6 +1632,14 @@ Global {4DEE8237-EF6F-426A-9DED-1909B6F3B18D}.Debug|x64.Build.0 = Debug|x64 {4DEE8237-EF6F-426A-9DED-1909B6F3B18D}.Release|x64.ActiveCfg = Release|x64 {4DEE8237-EF6F-426A-9DED-1909B6F3B18D}.Release|x64.Build.0 = Release|x64 + {F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}.Debug|x64.ActiveCfg = Debug|x64 + {F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}.Debug|x64.Build.0 = Debug|x64 + {F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}.Release|x64.ActiveCfg = Release|x64 + {F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}.Release|x64.Build.0 = Release|x64 + {2e5b103b-0d6f-4d73-9309-0ecb807a5154}.Debug|x64.ActiveCfg = Debug|x64 + {2e5b103b-0d6f-4d73-9309-0ecb807a5154}.Debug|x64.Build.0 = Debug|x64 + {2e5b103b-0d6f-4d73-9309-0ecb807a5154}.Release|x64.ActiveCfg = Release|x64 + {2e5b103b-0d6f-4d73-9309-0ecb807a5154}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From f32ed96f9ff9977105ff3c2f887810b2236c54c1 Mon Sep 17 00:00:00 2001 From: Nico Stuurman Date: Thu, 4 Jun 2026 12:49:58 -0700 Subject: [PATCH 3/5] EnderscopeSTage: removed unneeded CMake file. --- DeviceAdapters/EnderscopeStage/CMakeLists.txt | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 DeviceAdapters/EnderscopeStage/CMakeLists.txt diff --git a/DeviceAdapters/EnderscopeStage/CMakeLists.txt b/DeviceAdapters/EnderscopeStage/CMakeLists.txt deleted file mode 100644 index c427b6b66..000000000 --- a/DeviceAdapters/EnderscopeStage/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -set(module_name EnderscopeStage) - -set(module_srcs - EnderscopeStage.cpp -) - -set(module_hdrs - EnderscopeStage.h -) - -# In mmCoreAndDevices, this target is usually created by helper macros in parent CMake. -# If your parent build uses a different pattern, adapt this block accordingly. -add_library(${module_name} MODULE ${module_srcs} ${module_hdrs}) - -target_include_directories(${module_name} - PRIVATE - ${CMAKE_SOURCE_DIR}/MMDevice -) From 45902890928dcc96fbf64b36e6ca88be83e51b43 Mon Sep 17 00:00:00 2001 From: Nico Stuurman Date: Thu, 4 Jun 2026 12:57:53 -0700 Subject: [PATCH 4/5] Enderscope: Remove unused baudrate, claude-assisted code review and cleanup. --- .../EnderscopeStage/EnderscopeStage.cpp | 116 +++++++----------- .../EnderscopeStage/EnderscopeStage.h | 5 +- DeviceAdapters/EnderscopeStage/README.md | 9 +- 3 files changed, 51 insertions(+), 79 deletions(-) diff --git a/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp b/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp index 59f2568b0..8f782b481 100644 --- a/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp +++ b/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp @@ -28,20 +28,16 @@ #include #include -using namespace std; - const char* g_EnderscopeXYStageDeviceName = "EnderscopeXYStage"; const char* g_EnderscopeZStageDeviceName = "EnderscopeZStage"; namespace { -const long kDefaultBaudRate = 115200; const long kDefaultReadTimeoutMs = 1000; const double kDefaultStepSizeUm = 1.0; const char* kGCodeAbsolute = "G90"; const char* kGCodeRelative = "G91"; -const char* kGCodeHomeAll = "G28"; const char* kGCodeHomeXY = "G28 X Y"; const char* kGCodeHomeZ = "G28 Z"; const char* kGCodeFinish = "M400"; @@ -88,7 +84,6 @@ MODULE_API void DeleteDevice(MM::Device* pDevice) EnderscopeBase::EnderscopeBase(MM::Device* device) : initialized_(false), port_("Undefined"), - baudRate_(kDefaultBaudRate), readTimeoutMs_(kDefaultReadTimeoutMs), device_(device), core_(0) @@ -119,7 +114,8 @@ int EnderscopeBase::CheckDeviceStatus() return ret; } - initialized_ = true; + // initialized_ is set only at the end of Initialize(), not here, so a + // failure during property creation does not leave the device half-initialized. return DEVICE_OK; } @@ -134,8 +130,11 @@ int EnderscopeBase::ClearPort() unsigned char clear[bufSize]; unsigned long read = bufSize; + // Cap the number of drain iterations so a device that streams continuously + // (e.g. Marlin auto-reporting from M155) cannot trap us in an unbounded loop. + const int maxIterations = 100; int ret = DEVICE_OK; - while (static_cast(read) == bufSize) + for (int i = 0; static_cast(read) == bufSize && i < maxIterations; ++i) { ret = core_->ReadFromSerial(device_, port_.c_str(), clear, bufSize, read); if (ret != DEVICE_OK) @@ -188,6 +187,11 @@ int EnderscopeBase::CommandExpectOk(const std::string& command) const return ret; } + // The number of read attempts is a coarse bound derived from ReadTimeoutMs, + // assuming each GetSerialAnswer call returns in roughly 10 ms. The real + // per-read timeout is enforced by the serial port's own AnswerTimeout + // setting; this loop only limits how many lines we are willing to skip + // (e.g. blank lines or Marlin auto-reports) while waiting for "ok". const long maxReads = std::max(1L, readTimeoutMs_ / 10L); for (long i = 0; i < maxReads; ++i) { @@ -212,6 +216,12 @@ int EnderscopeBase::CommandExpectOk(const std::string& command) const return DEVICE_SERIAL_TIMEOUT; } +// Queries the current position with M114. The expected Marlin reply is a single +// data line of the form: +// X:0.00 Y:0.00 Z:0.00 E:0.00 Count X:0 Y:0 Z:0 +// followed by an "ok". Only the leading work-coordinate fields (the first "X:", +// "Y:", "Z:") are parsed; the trailing "Count ..." stepper section is ignored +// because ParseAxisValue matches the first occurrence of each axis key. int EnderscopeBase::QueryPositionMm(double& x, double& y, double& z) const { int ret = SendCommand(kGCodePosition); @@ -261,6 +271,14 @@ int EnderscopeBase::QueryPositionMm(double& x, double& y, double& z) const return DEVICE_SERIAL_INVALID_RESPONSE; } + // Drop the trailing "Count ..." stepper section so its per-axis fields can + // never be mistaken for the work coordinates we want. + const size_t countPos = dataLine.find("Count"); + if (countPos != std::string::npos) + { + dataLine = dataLine.substr(0, countPos); + } + if (!ParseAxisValue(dataLine, 'X', x) || !ParseAxisValue(dataLine, 'Y', y) || !ParseAxisValue(dataLine, 'Z', z)) { return DEVICE_SERIAL_INVALID_RESPONSE; @@ -334,13 +352,8 @@ EnderscopeXYStage::EnderscopeXYStage() CPropertyAction* pAct = new CPropertyAction(this, &EnderscopeXYStage::OnPort); CreateProperty(MM::g_Keyword_Port, "Undefined", MM::String, false, pAct, true); - pAct = new CPropertyAction(this, &EnderscopeXYStage::OnBaudRate); - CreateProperty("BaudRate", CDeviceUtils::ConvertToString(baudRate_), MM::Integer, false, pAct, true); - - AddAllowedValue("BaudRate", "9600"); - AddAllowedValue("BaudRate", "57600"); - AddAllowedValue("BaudRate", "115200"); - AddAllowedValue("BaudRate", "250000"); + // Note: the serial baud rate is configured on the COM port device itself + // (the port's own BaudRate property), not here. pAct = new CPropertyAction(this, &EnderscopeXYStage::OnReadTimeout); CreateProperty("ReadTimeoutMs", CDeviceUtils::ConvertToString(readTimeoutMs_), MM::Integer, false, pAct, true); @@ -399,6 +412,8 @@ int EnderscopeXYStage::Shutdown() bool EnderscopeXYStage::Busy() { + // Motion is synchronous: SetPosition... blocks on M400 ("ok") until the move + // completes, so the device is never busy by the time control returns. return false; } @@ -410,7 +425,7 @@ int EnderscopeXYStage::SetAbsoluteMm(double xMm, double yMm) return ret; } - ostringstream cmd; + std::ostringstream cmd; cmd << "G0 X " << xMm << " Y " << yMm; ret = CommandExpectOk(cmd.str()); if (ret != DEVICE_OK) @@ -429,7 +444,7 @@ int EnderscopeXYStage::SetRelativeMm(double dxMm, double dyMm) return ret; } - ostringstream cmd; + std::ostringstream cmd; cmd << "G0 X " << dxMm << " Y " << dyMm; ret = CommandExpectOk(cmd.str()); if (ret != DEVICE_OK) @@ -522,14 +537,12 @@ int EnderscopeXYStage::SetRelativePositionSteps(long x, long y) int EnderscopeXYStage::Home() { + // Home only X and Y here. We deliberately do not fall back to "home all" + // (G28) on failure: this is the XY device and must never move the Z axis. int ret = CommandExpectOk(kGCodeHomeXY); if (ret != DEVICE_OK) { - ret = CommandExpectOk(kGCodeHomeAll); - if (ret != DEVICE_OK) - { - return ret; - } + return ret; } ret = CommandExpectOk(kGCodeFinish); @@ -622,25 +635,6 @@ int EnderscopeXYStage::OnPort(MM::PropertyBase* pProp, MM::ActionType eAct) return DEVICE_OK; } -int EnderscopeXYStage::OnBaudRate(MM::PropertyBase* pProp, MM::ActionType eAct) -{ - if (eAct == MM::BeforeGet) - { - pProp->Set(baudRate_); - } - else if (eAct == MM::AfterSet) - { - if (initialized_) - { - pProp->Set(baudRate_); - return DEVICE_CAN_NOT_SET_PROPERTY; - } - pProp->Get(baudRate_); - } - - return DEVICE_OK; -} - int EnderscopeXYStage::OnReadTimeout(MM::PropertyBase* pProp, MM::ActionType eAct) { if (eAct == MM::BeforeGet) @@ -718,13 +712,8 @@ EnderscopeZStage::EnderscopeZStage() CPropertyAction* pAct = new CPropertyAction(this, &EnderscopeZStage::OnPort); CreateProperty(MM::g_Keyword_Port, "Undefined", MM::String, false, pAct, true); - pAct = new CPropertyAction(this, &EnderscopeZStage::OnBaudRate); - CreateProperty("BaudRate", CDeviceUtils::ConvertToString(baudRate_), MM::Integer, false, pAct, true); - - AddAllowedValue("BaudRate", "9600"); - AddAllowedValue("BaudRate", "57600"); - AddAllowedValue("BaudRate", "115200"); - AddAllowedValue("BaudRate", "250000"); + // Note: the serial baud rate is configured on the COM port device itself + // (the port's own BaudRate property), not here. pAct = new CPropertyAction(this, &EnderscopeZStage::OnReadTimeout); CreateProperty("ReadTimeoutMs", CDeviceUtils::ConvertToString(readTimeoutMs_), MM::Integer, false, pAct, true); @@ -776,6 +765,8 @@ int EnderscopeZStage::Shutdown() bool EnderscopeZStage::Busy() { + // Motion is synchronous: SetPosition... blocks on M400 ("ok") until the move + // completes, so the device is never busy by the time control returns. return false; } @@ -787,7 +778,7 @@ int EnderscopeZStage::SetAbsoluteMm(double zMm) return ret; } - ostringstream cmd; + std::ostringstream cmd; cmd << "G0 Z " << zMm; ret = CommandExpectOk(cmd.str()); if (ret != DEVICE_OK) @@ -806,7 +797,7 @@ int EnderscopeZStage::SetRelativeMm(double dzMm) return ret; } - ostringstream cmd; + std::ostringstream cmd; cmd << "G0 Z " << dzMm; ret = CommandExpectOk(cmd.str()); if (ret != DEVICE_OK) @@ -886,14 +877,12 @@ int EnderscopeZStage::Stop() int EnderscopeZStage::Home() { + // Home only Z here. We deliberately do not fall back to "home all" (G28) + // on failure: this is the Z device and must never move the X/Y axes. int ret = CommandExpectOk(kGCodeHomeZ); if (ret != DEVICE_OK) { - ret = CommandExpectOk(kGCodeHomeAll); - if (ret != DEVICE_OK) - { - return ret; - } + return ret; } ret = CommandExpectOk(kGCodeFinish); @@ -963,25 +952,6 @@ int EnderscopeZStage::OnPort(MM::PropertyBase* pProp, MM::ActionType eAct) return DEVICE_OK; } -int EnderscopeZStage::OnBaudRate(MM::PropertyBase* pProp, MM::ActionType eAct) -{ - if (eAct == MM::BeforeGet) - { - pProp->Set(baudRate_); - } - else if (eAct == MM::AfterSet) - { - if (initialized_) - { - pProp->Set(baudRate_); - return DEVICE_CAN_NOT_SET_PROPERTY; - } - pProp->Get(baudRate_); - } - - return DEVICE_OK; -} - int EnderscopeZStage::OnReadTimeout(MM::PropertyBase* pProp, MM::ActionType eAct) { if (eAct == MM::BeforeGet) diff --git a/DeviceAdapters/EnderscopeStage/EnderscopeStage.h b/DeviceAdapters/EnderscopeStage/EnderscopeStage.h index 813f69ed3..c27b469e4 100644 --- a/DeviceAdapters/EnderscopeStage/EnderscopeStage.h +++ b/DeviceAdapters/EnderscopeStage/EnderscopeStage.h @@ -4,7 +4,7 @@ // SUBSYSTEM: DeviceAdapters // // DESCRIPTION: Enderscope Stage adapter (Marlin/Enderscope-compatible) -// Inspired by adapter structure used in Marzhauser-LStep. +// Adapted in spirit from the Marzhauser-LStep adapter shape. /////////////////////////////////////////////////////////////////////////////// #ifndef _ENDERSCOPE_STAGE_H_ @@ -40,7 +40,6 @@ class EnderscopeBase protected: bool initialized_; std::string port_; - long baudRate_; long readTimeoutMs_; MM::Device* device_; @@ -81,7 +80,6 @@ class EnderscopeXYStage : public CXYStageBase, public Endersc double GetStepSizeYUm() override { return stepSizeYUm_; } int OnPort(MM::PropertyBase* pProp, MM::ActionType eAct); - int OnBaudRate(MM::PropertyBase* pProp, MM::ActionType eAct); int OnReadTimeout(MM::PropertyBase* pProp, MM::ActionType eAct); int OnStepSizeX(MM::PropertyBase* pProp, MM::ActionType eAct); int OnStepSizeY(MM::PropertyBase* pProp, MM::ActionType eAct); @@ -130,7 +128,6 @@ class EnderscopeZStage : public CStageBase, public EnderscopeB bool IsContinuousFocusDrive() const override { return false; } int OnPort(MM::PropertyBase* pProp, MM::ActionType eAct); - int OnBaudRate(MM::PropertyBase* pProp, MM::ActionType eAct); int OnReadTimeout(MM::PropertyBase* pProp, MM::ActionType eAct); int OnStepSize(MM::PropertyBase* pProp, MM::ActionType eAct); diff --git a/DeviceAdapters/EnderscopeStage/README.md b/DeviceAdapters/EnderscopeStage/README.md index 5da61808c..c04a983e4 100644 --- a/DeviceAdapters/EnderscopeStage/README.md +++ b/DeviceAdapters/EnderscopeStage/README.md @@ -21,7 +21,7 @@ This directory contains a new Micro-Manager device adapter that follows the stru - `SetPositionUm(...)` -> `G90` then `G0 ...` then `M400` - `SetRelativePositionUm(...)` -> `G91` then `G0 ...` then `M400` - `GetPosition...` -> `M114` (parses `X:.. Y:.. Z:..`) -- `Home()` -> `G28 X Y` / `G28 Z` (fallback `G28`) then `M400` +- `Home()` -> `G28 X Y` (XY stage) / `G28 Z` (Z stage) then `M400` - `Stop()` -> `M410` All stage coordinates are converted between Micro-Manager um and Enderscope mm. @@ -29,13 +29,18 @@ All stage coordinates are converted between Micro-Manager um and Enderscope mm. ## Pre-initialization Properties - `Port` (serial port) -- `BaudRate` (default `115200`) - `ReadTimeoutMs` (default `1000`) +The serial baud rate is configured on the COM port device itself (its own +`BaudRate` property), not on the stage device. Set the port to match the +controller (typically `115200`). + ## Notes - Adapter currently reports unbounded limits. - Adapter uses synchronous motion (`M400`) and returns `Busy() == false`. +- `Home()` homes only the axes owned by each device; it never falls back to a + full `G28` home-all, so the XY device never moves Z and vice versa. - To compile in `mmCoreAndDevices`, place this folder under `DeviceAdapters/` and add it to that repository's build configuration (CMake or VS project lists, depending on platform/build system). ## References From 37fedb900537c6bfe16f976c5f3542d0d5ef3559 Mon Sep 17 00:00:00 2001 From: Nico Stuurman Date: Thu, 4 Jun 2026 15:46:30 -0700 Subject: [PATCH 5/5] Build: fix erronously added NikonAZ100SDK and fix typos in Enderscope. --- micromanager.sln | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/micromanager.sln b/micromanager.sln index 438d7edb4..7a0d38c7a 100644 --- a/micromanager.sln +++ b/micromanager.sln @@ -542,9 +542,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ThorlabsTSP01", "DeviceAdap EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SpinnakerC", "DeviceAdapters\SpinnakerC\SpinnakerC.vcxproj", "{4DEE8237-EF6F-426A-9DED-1909B6F3B18D}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NikonAZ100SDK", "SecretDeviceAdapters\NikonAZ100SDK\NikonAZ100SDK.vcxproj", "{F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EndersccopeStage", "DeviceAdapters\EnderscopeStage\EnderscopeStage.vcxproj", "{2e5b103b-0d6f-4d73-9309-0ecb807a5154}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EnderscopeStage", "DeviceAdapters\EnderscopeStage\EnderscopeStage.vcxproj", "{2e5b103b-0d6f-4d73-9309-0ecb807a5154}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1632,10 +1630,6 @@ Global {4DEE8237-EF6F-426A-9DED-1909B6F3B18D}.Debug|x64.Build.0 = Debug|x64 {4DEE8237-EF6F-426A-9DED-1909B6F3B18D}.Release|x64.ActiveCfg = Release|x64 {4DEE8237-EF6F-426A-9DED-1909B6F3B18D}.Release|x64.Build.0 = Release|x64 - {F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}.Debug|x64.ActiveCfg = Debug|x64 - {F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}.Debug|x64.Build.0 = Debug|x64 - {F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}.Release|x64.ActiveCfg = Release|x64 - {F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}.Release|x64.Build.0 = Release|x64 {2e5b103b-0d6f-4d73-9309-0ecb807a5154}.Debug|x64.ActiveCfg = Debug|x64 {2e5b103b-0d6f-4d73-9309-0ecb807a5154}.Debug|x64.Build.0 = Debug|x64 {2e5b103b-0d6f-4d73-9309-0ecb807a5154}.Release|x64.ActiveCfg = Release|x64