diff --git a/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp b/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp new file mode 100644 index 000000000..8f782b481 --- /dev/null +++ b/DeviceAdapters/EnderscopeStage/EnderscopeStage.cpp @@ -0,0 +1,993 @@ +/////////////////////////////////////////////////////////////////////////////// +// 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 +// Prevent windows.h (pulled in transitively) from defining min/max macros, +// which break std::min/std::max. +#define NOMINMAX +#pragma warning(disable : 4355) +#endif + +#include "EnderscopeStage.h" + +#include "MMDevice.h" +#include "ModuleInterface.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +const char* g_EnderscopeXYStageDeviceName = "EnderscopeXYStage"; +const char* g_EnderscopeZStageDeviceName = "EnderscopeZStage"; + +namespace +{ +const long kDefaultReadTimeoutMs = 1000; +const double kDefaultStepSizeUm = 1.0; + +const char* kGCodeAbsolute = "G90"; +const char* kGCodeRelative = "G91"; +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"), + 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_ 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; +} + +int EnderscopeBase::ClearPort() +{ + if (core_ == 0) + { + return DEVICE_NOT_CONNECTED; + } + + const int bufSize = 255; + 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; + 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) + { + 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; + } + + // 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) + { + 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; +} + +// 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); + 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; + } + + // 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; + } + + 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); + + // 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); + 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() +{ + // 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; +} + +int EnderscopeXYStage::SetAbsoluteMm(double xMm, double yMm) +{ + int ret = CommandExpectOk(kGCodeAbsolute); + if (ret != DEVICE_OK) + { + return ret; + } + + std::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; + } + + std::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() +{ + // 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) + { + 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::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); + + // 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); + 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() +{ + // 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; +} + +int EnderscopeZStage::SetAbsoluteMm(double zMm) +{ + int ret = CommandExpectOk(kGCodeAbsolute); + if (ret != DEVICE_OK) + { + return ret; + } + + std::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; + } + + std::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() +{ + // 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) + { + 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::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..c27b469e4 --- /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) +// Adapted in spirit from the Marzhauser-LStep adapter shape. +/////////////////////////////////////////////////////////////////////////////// + +#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; + +// Device adapter custom error codes (>= 10000 by MM convention) +constexpr int ERR_PORT_CHANGE_FORBIDDEN = 10004; + +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 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 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 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/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/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..c04a983e4 --- /dev/null +++ b/DeviceAdapters/EnderscopeStage/README.md @@ -0,0 +1,51 @@ +# 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` (XY stage) / `G28 Z` (Z stage) then `M400` +- `Stop()` -> `M410` + +All stage coordinates are converted between Micro-Manager um and Enderscope mm. + +## Pre-initialization Properties + +- `Port` (serial port) +- `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 + +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. + diff --git a/micromanager.sln b/micromanager.sln index 1c9487827..7a0d38c7a 100644 --- a/micromanager.sln +++ b/micromanager.sln @@ -542,6 +542,8 @@ 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}") = "EnderscopeStage", "DeviceAdapters\EnderscopeStage\EnderscopeStage.vcxproj", "{2e5b103b-0d6f-4d73-9309-0ecb807a5154}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -1628,6 +1630,10 @@ 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 + {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