From c53285e63ee26478e345e966a5dc800a17a0e235 Mon Sep 17 00:00:00 2001 From: "Mark A. Tsuchida" Date: Wed, 10 Jan 2024 17:42:49 -0600 Subject: [PATCH] NotificationTester: Add XY stage simulation --- .../NotificationTester/NotificationTester.cpp | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) diff --git a/DeviceAdapters/NotificationTester/NotificationTester.cpp b/DeviceAdapters/NotificationTester/NotificationTester.cpp index 548f7c978..9683ac9c1 100644 --- a/DeviceAdapters/NotificationTester/NotificationTester.cpp +++ b/DeviceAdapters/NotificationTester/NotificationTester.cpp @@ -30,8 +30,34 @@ constexpr char DEVNAME_SYNC_PROPERTY[] = "NTSyncProperty"; constexpr char DEVNAME_ASYNC_PROPERTY[] = "NTAsyncProperty"; constexpr char DEVNAME_SYNC_STAGE[] = "NTSyncStage"; constexpr char DEVNAME_ASYNC_STAGE[] = "NTAsyncStage"; +constexpr char DEVNAME_SYNC_XY_STAGE[] = "NTSyncXYStage"; +constexpr char DEVNAME_ASYNC_XY_STAGE[] = "NTAsyncXYStage"; constexpr char PROPNAME_TEST_PROPERTY[] = "TestProperty"; +std::pair> ParseIntegerPair(const std::string& s) { + // Strings like " 100 ; -200 ", all spaces optional. + // (Not using ',' as delimiter as it is not allowed in property values.) + static constexpr char delimiter = ';'; + static constexpr auto invalid = std::make_pair(false, + std::array{}); + try { + std::size_t index{}; + const long x = std::stol(s, &index); + const auto delimPos = s.find_first_not_of(' ', index); + if (delimPos >= s.size() || s[delimPos] != delimiter) { + return invalid; + } + const std::string afterDelim = s.substr(delimPos + 1); + const long y = std::stol(afterDelim, &index); + if (afterDelim.find_first_not_of(' ', index) != std::string::npos) { + return invalid; + } + return {true, {x, y}}; + } catch (const std::exception&) { + return invalid; + } +} + template class NTestProp : public CGenericBase> { @@ -371,6 +397,221 @@ class NTestStage : public CStageBase> } }; +template +class NTestXYStage : public CXYStageBase> +{ + // The process model operates in steps (only set to integers; round upon + // readout). X and Y use the same step size. + static constexpr double umPerStep_ = 0.1; + std::string name_; + ProcModel model_; + DelayedNotifier delayer_; + + std::mutex notificationMut_; + bool notificationsEnabled_ = false; + +public: + explicit NTestXYStage(std::string name) : + name_(std::move(name)), + model_([this](std::array pv) { + const long ix = std::lround(pv[0]); + const long iy = std::lround(pv[1]); + this->LogMessage(("PV = " + std::to_string(ix) + ", " + + std::to_string(iy)).c_str(), true); + { + std::lock_guard lock(notificationMut_); + if (!notificationsEnabled_) { + return; + } + } + delayer_.Schedule([this, ix, iy] { + this->LogMessage(("Notifying: PV = " + std::to_string(ix) + ", " + + std::to_string(iy)).c_str(), true); + this->OnXYStagePositionChanged(umPerStep_* ix, umPerStep_* iy); + }); + }) + { + // Adjust default for stage-like velocity (100 um/s). + model_.ReciprocalSlewRateSeconds(0.01 * umPerStep_); + } + + int Initialize() final { + this->CreateFloatProperty("UmPerStep", umPerStep_, true); + + this->CreateStringProperty("NotificationsEnabled", + notificationsEnabled_ ? "Yes" : "No", false, + new MM::ActionLambda([this](MM::PropertyBase* pProp, + MM::ActionType eAct) { + if (eAct == MM::BeforeGet) { + pProp->Set(notificationsEnabled_ ? "Yes" : "No"); + } else if (eAct == MM::AfterSet) { + std::string value; + pProp->Get(value); + notificationsEnabled_ = (value == "Yes"); + } + return DEVICE_OK; + })); + this->AddAllowedValue("NotificationsEnabled", "No"); + this->AddAllowedValue("NotificationsEnabled", "Yes"); + + this->CreateStringProperty("ExternallySetSteps", "0; 0", false, + new MM::ActionLambda([this](MM::PropertyBase* pProp, + MM::ActionType eAct) { + if (eAct == MM::BeforeGet) { + // Keep last-set value + } else if (eAct == MM::AfterSet) { + std::string commaSeparated; + pProp->Get(commaSeparated); + const auto okAndPair = ParseIntegerPair(commaSeparated); + if (!okAndPair.first) { + // Snap to current setpoint for next get + const auto sp = model_.Setpoint(); + pProp->Set((std::to_string(std::lround(sp[0])) + "; " + + std::to_string(std::lround(sp[1]))).c_str()); + return DEVICE_INVALID_PROPERTY_VALUE; + } + const auto sp = okAndPair.second; + this->LogMessage(("sp = " + std::to_string(sp[0]) + ", " + + std::to_string(sp[1])).c_str(), true); + model_.Setpoint({double(sp[0]), double(sp[1])}); + } + return DEVICE_OK; + })); + + if (ProcModel::isAsync) { + this->CreateFloatProperty("SlewTimePerStep_s", + model_.ReciprocalSlewRateSeconds(), false, + new MM::ActionLambda([this](MM::PropertyBase* pProp, + MM::ActionType eAct) { + if (eAct == MM::BeforeGet) { + pProp->Set(model_.ReciprocalSlewRateSeconds()); + } else if (eAct == MM::AfterSet) { + double seconds{}; + pProp->Get(seconds); + model_.ReciprocalSlewRateSeconds(seconds); + } + return DEVICE_OK; + })); + this->SetPropertyLimits("SlewTimePerStep_s", 0.0001, 10.0); + + this->CreateFloatProperty("UpdateInterval_s", + model_.UpdateIntervalSeconds(), false, + new MM::ActionLambda([this](MM::PropertyBase* pProp, + MM::ActionType eAct) { + if (eAct == MM::BeforeGet) { + pProp->Set(model_.UpdateIntervalSeconds()); + } else if (eAct == MM::AfterSet) { + double seconds{}; + pProp->Get(seconds); + model_.UpdateIntervalSeconds(seconds); + } + return DEVICE_OK; + })); + this->SetPropertyLimits("UpdateInterval_s", 0.0001, 10.0); + + using std::chrono::duration_cast; + using std::chrono::microseconds; + using FPSeconds = std::chrono::duration; + this->CreateFloatProperty("NotificationDelay_s", + duration_cast(delayer_.Delay()).count(), false, + new MM::ActionLambda([this](MM::PropertyBase* pProp, + MM::ActionType eAct) { + if (eAct == MM::BeforeGet) { + pProp->Set(duration_cast(delayer_.Delay()).count()); + } else if (eAct == MM::AfterSet) { + double seconds{}; + pProp->Get(seconds); + const auto delay = FPSeconds{seconds}; + delayer_.Delay(duration_cast(delay)); + } + return DEVICE_OK; + })); + this->SetPropertyLimits("NotificationDelay_s", 0.0, 1.0); + } + + return DEVICE_OK; + } + + int Shutdown() final { + model_.Halt(); + delayer_.CancelAll(); + return DEVICE_OK; + } + + bool Busy() final { + return model_.IsSlewing(); + } + + void GetName(char* name) const final { + CDeviceUtils::CopyLimitedString(name, name_.c_str()); + } + + int GetLimitsUm(double& xMin, double& xMax, double& yMin, double& yMax) final { + long xLo, xHi, yLo, yHi; + int ret = GetStepLimits(xLo, xHi, yLo, yHi); + if (ret != DEVICE_OK) { + return ret; + } + xMin = xLo * umPerStep_; + xMax = xHi * umPerStep_; + yMin = yLo * umPerStep_; + yMax = yHi * umPerStep_; + return DEVICE_OK; + } + + int SetPositionSteps(long x, long y) final { + this->LogMessage(("sp = " + std::to_string(x) + ", " + + std::to_string(y)).c_str(), true); + model_.Setpoint({double(x), double(y)}); + return DEVICE_OK; + } + + int GetPositionSteps(long& x, long& y) final { + const auto pv = model_.ProcessVariable(); + x = std::lround(pv[0]); + y = std::lround(pv[1]); + return DEVICE_OK; + } + + int Home() final { + // We could simulate homing, but it is not currently clear what + // notifications mean during a home (application code should explicitly + // read the position after a home). For now, pretend that the stage + // doesn't support homing. + return DEVICE_UNSUPPORTED_COMMAND; + } + + int Stop() final { + model_.Halt(); + return DEVICE_OK; + } + + int SetOrigin() final { + return DEVICE_UNSUPPORTED_COMMAND; + } + + int GetStepLimits(long& xMin, long& xMax, long& yMin, long& yMax) final { + // Conservatively keep steps within 32-bit range (we want exact integer + // steps to be preserved by the double-based process model). + xMin = yMin = -2147483648; + xMax = yMax = +2147483647; + return DEVICE_OK; + } + + double GetStepSizeXUm() final { + return umPerStep_; + } + + double GetStepSizeYUm() final { + return umPerStep_; + } + + int IsXYStageSequenceable(bool& flag) const final { + flag = false; + return DEVICE_OK; + } +}; + MODULE_API void InitializeModuleData() { RegisterDevice(DEVNAME_SYNC_PROPERTY, MM::GenericDevice, @@ -381,6 +622,10 @@ MODULE_API void InitializeModuleData() "Test synchronous stage position notifications"); RegisterDevice(DEVNAME_ASYNC_STAGE, MM::StageDevice, "Test asynchronous stage busy state and position notifications"); + RegisterDevice(DEVNAME_SYNC_XY_STAGE, MM::XYStageDevice, + "Test synchronous XY stage position notifications"); + RegisterDevice(DEVNAME_ASYNC_XY_STAGE, MM::XYStageDevice, + "Test asynchronous XY stage busy state and position notifications"); } MODULE_API MM::Device *CreateDevice(const char *deviceName) @@ -394,6 +639,10 @@ MODULE_API MM::Device *CreateDevice(const char *deviceName) return new NTestStage>(name); if (name == DEVNAME_ASYNC_STAGE) return new NTestStage>(name); + if (name == DEVNAME_SYNC_XY_STAGE) + return new NTestXYStage>(name); + if (name == DEVNAME_ASYNC_XY_STAGE) + return new NTestXYStage>(name); return nullptr; }