diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..1630d6788 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,173 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Overview + +Micro-Manager is Open Source software for microscope control. This repository contains the C++ core components: + +- **MMCore**: Device abstraction layer providing the top-level API for applications to control microscope hardware (see `MMCore/MMCore.h`) +- **MMDevice**: Device API defining the interface that device adapters must implement (see `MMDevice/MMDevice.h` and `MMDevice/DeviceBase.h`) +- **MMCoreJ_wrap**: SWIG-generated Java wrapper for MMCore +- **DeviceAdapters/**: Publicly accessible device adapter source code +- **SecretDeviceAdapters/**: Non-public device adapters (separate private repository referenced via git submodule) + +### Architecture + +MMCore implements two APIs: +- **Top API** (`MMCore/MMCore.h`): Used by applications to control abstracted microscope hardware. Public methods use lowercase naming (e.g., `getConfiguration()`) to be language-agnostic for SWIG wrapping. +- **Bottom API** (`MMDevice/MMDevice.h`): Implemented by device adapters to communicate with physical devices. + +Both APIs maintain a pure C-interface for cross-platform compatibility and dynamic loading. + +Device adapters are compiled into dynamically loadable libraries (`.dll`/`.so`/`.dylib`) that MMCore loads at runtime. + +## Build Commands + +### Windows (Visual Studio) + +Primary solution file: `micromanager.sln` + +Build using MSBuild or Visual Studio. MSBuild properties can be overridden via `/property:name=value`: +- `MM_3RDPARTYPUBLIC`: Path to public 3rd party dependencies (default: `../../3rdpartypublic`) +- `MM_3RDPARTYPRIVATE`: Path to private 3rd party dependencies (default: `../../3rdparty`) +- `MM_BOOST_INCLUDEDIR`: Boost include directory +- `MM_BOOST_LIBDIR`: Boost lib directory +- `MM_BUILDDIR`: Build artifacts directory + +See `buildscripts/VisualStudio/MMCommon.props` for default values. + +### macOS and Linux (Autotools) + +Standard GNU Autotools workflow: + +```bash +# From repository root +./configure # Configure build (detects available SDKs) +make # Build all components +make install # Install (may require sudo) +``` + +Build only MMCore and device adapters (skip Java components): +```bash +./configure --without-java +make +``` + +The build system auto-detects which device adapters can be built based on available vendor SDKs and headers. + +### Experimental Meson Build (justfile) + +Requires: `just`, `meson`, `ninja` + +```bash +just # List available commands +just build-mmdevice # Build MMDevice +just build-mmcore # Build MMCore (depends on MMDevice) +just test-mmdevice # Test MMDevice +just test-mmcore # Test MMCore +just test # Run all tests +just clean # Clean build artifacts +``` + +Quick one-liner with uv: +```bash +uvx --from rust-just --with meson --with ninja just test +``` + +## Testing + +Tests use Catch2 framework and are located in `unittest/` subdirectories: +- `MMCore/unittest/` - Core tests +- `MMDevice/unittest/` - Device API tests +- Some device adapters have their own tests (e.g., `DeviceAdapters/HamiltonMVP/unittest/`) + +Run tests via meson (see justfile commands above) or through IDE test runners. + +## Device Adapter Development + +### Base Classes + +Device adapters inherit from specialized base classes in `MMDevice/DeviceBase.h`: +- `CCameraBase` - Cameras +- `CShutterBase` - Shutters +- `CStageBase` - Z-stages +- `CXYStageBase` - XY stages +- `CStateDeviceBase` - Filter wheels, turrets +- `CGenericBase` - Generic devices +- `CAutoFocusBase` - Autofocus devices +- `CImageProcessorBase` - Image processors +- `CHubBase` - Device hubs (multi-device controllers) + +All base classes use the CRTP pattern: `class MyDevice : public CCameraBase` + +### Property System + +Device configuration uses a property-based system. Properties are created in `Initialize()`: +```cpp +// Create action handler +CPropertyAction* pAct = new CPropertyAction(this, &MyDevice::OnProperty); +// Create property with allowed values +CreateProperty("PropertyName", "DefaultValue", MM::String, false, pAct); +AddAllowedValue("PropertyName", "Value1"); +AddAllowedValue("PropertyName", "Value2"); +``` + +### Vendor SDK Integration + +Many device adapters depend on proprietary vendor SDKs: +- SDK headers/libraries are in `../../3rdpartypublic/` (public) or `../../3rdparty/` (private) +- Build system automatically disables adapters when SDKs are unavailable +- Each adapter's `configure.ac` or `.vcxproj` specifies SDK requirements + +Example adapters to study: +- `DemoCamera` - Full-featured reference implementation without hardware dependencies +- `Arduino` - Simple serial communication pattern +- `PVCAM` - Camera with vendor SDK integration +- `ASITiger` - Multi-device hub pattern + +### Threading Considerations + +Camera adapters typically use separate threads for image acquisition: +- Implement proper locking around shared state +- Use MMDevice threading primitives (`MMThreadLock` in `DeviceThreads.h`) +- Circular buffers handle asynchronous image streaming (see `MMCore/CircularBuffer.h`) + +### Error Handling + +Use standard error codes from `MMDevice/MMDeviceConstants.h`: +- `DEVICE_OK` - Success +- `DEVICE_ERR` - Generic error +- Define adapter-specific error codes starting at 100+ + +Common error message constants are defined in `MMDevice/DeviceBase.h` (e.g., `g_Msg_SERIAL_TIMEOUT`). + +## Key Files for Understanding the System + +- `MMCore/MMCore.h` - Main public API +- `MMDevice/MMDevice.h` - Device interface definitions +- `MMDevice/DeviceBase.h` - Base classes and utility templates +- `DeviceAdapters/DemoCamera/DemoCamera.cpp` - Reference implementation +- `buildscripts/VisualStudio/MMCommon.props` - Windows build configuration defaults + +## Coding style +- All indents are 3 spaces (no tab characters). +- Curly braces open in the same line in Java and in a new line in C++ (see examples). +- Class names begin with uppercase, and with each word capitalized, e.g. MyFirstClass. +- Function names use the same convention except that in Java they begin with lowercase and in C++ with uppercase, e.g. MyFunc() in C++ and myFunc() in Java. +- All variables begin with lower case, e.g. myVar. +- Class member variables begin with lowercase and end with underscore, e.g. memberVar_. +- Do not use this->memberVar_ idiom unless it is absolutely necessary to avoid confusion. +- Static constants in C++: const char* const g_Keyword_Description. +- Static constants in Java: static final String METADATA_FILE_NAME. +- if/else, for, and while statements should include curly braces, even if they are only followed by a single line. + +## CRITICAL: File Editing on Windows +MANDATORY: Always use Backslashes on Windows for File Paths +When using Edit or MultiEdit on WIndows, you MUST use backslashes (\) in the file paths, NOT forward slashes (/). +WRONG: +Edit(file_path: "D:/repos/project/file.tsx", ...) +MultiEdit(file_path: "D:/repos/project/file.tsx", ...) +CORRECT: +Edit(file_path: "D:\repos\project\file.tsx", ...) +MultiEdit(file_path: "D:\repos\project\file.tsx", ...) diff --git a/DeviceAdapters/EvidentIX85/EvidentHub.cpp b/DeviceAdapters/EvidentIX85/EvidentHub.cpp new file mode 100644 index 000000000..cea636c10 --- /dev/null +++ b/DeviceAdapters/EvidentIX85/EvidentHub.cpp @@ -0,0 +1,2170 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentHub.cpp +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 microscope hub implementation +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#include "EvidentHub.h" +#include "ModuleInterface.h" +#include +#include +#include +#include + +using namespace EvidentIX85; + +const char* g_HubDeviceName = "EvidentIX85-Hub"; + +// Device names +extern const char* g_FocusDeviceName; +extern const char* g_NosepieceDeviceName; +extern const char* g_MagnificationDeviceName; +extern const char* g_LightPathDeviceName; +extern const char* g_CondenserTurretDeviceName; +extern const char* g_DIAShutterDeviceName; +extern const char* g_EPIShutter1DeviceName; +extern const char* g_EPIShutter2DeviceName; +extern const char* g_MirrorUnit1DeviceName; +extern const char* g_MirrorUnit2DeviceName; +extern const char* g_PolarizerDeviceName; +extern const char* g_DICPrismDeviceName; +extern const char* g_EPINDDeviceName; +extern const char* g_CorrectionCollarDeviceName; + +// Property names +const char* g_PropPort = "Port"; +const char* g_PropAnswerTimeout = "AnswerTimeout"; +extern const char* g_Keyword_Magnification; + +EvidentHub::EvidentHub() : + initialized_(false), + port_(""), + answerTimeoutMs_(ANSWER_TIMEOUT_MS), + stopMonitoring_(false), + responseReady_(false), + rememberedDIABrightness_(255) +{ + InitializeDefaultErrorMessages(); + + // Custom error messages + SetErrorText(ERR_COMMAND_TIMEOUT, "Command timeout - no response from microscope"); + SetErrorText(ERR_NEGATIVE_ACK, "Microscope returned error (negative acknowledgement)"); + SetErrorText(ERR_INVALID_RESPONSE, "Invalid response from microscope"); + SetErrorText(ERR_NOT_IN_REMOTE_MODE, "Microscope not in remote mode"); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Device not available on this microscope"); + SetErrorText(ERR_POSITION_UNKNOWN, "Device position is unknown"); + SetErrorText(ERR_MONITOR_THREAD_FAILED, "Failed to start monitoring thread"); + SetErrorText(ERR_PORT_NOT_SET, "Serial port not set"); + SetErrorText(ERR_PORT_CHANGE_FORBIDDEN, "Cannot change serial port after initialization"); + + // Pre-initialization properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentHub::OnPort); + CreateProperty(g_PropPort, "Undefined", MM::String, false, pAct, true); + + pAct = new CPropertyAction(this, &EvidentHub::OnAnswerTimeout); + CreateProperty(g_PropAnswerTimeout, "2000", MM::Integer, false, pAct, true); +} + +EvidentHub::~EvidentHub() +{ + Shutdown(); +} + +void EvidentHub::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, g_HubDeviceName); +} + +bool EvidentHub::Busy() +{ + return false; // Hub itself is never busy +} + +int EvidentHub::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + usedDevices_.clear(); + + // Verify port is set + if (port_.empty() || port_ == "Undefined") + return ERR_PORT_NOT_SET; + + // Clear port buffers + int ret = ClearPort(); + if (ret != DEVICE_OK) + return ret; + + // Start monitoring thread + StartMonitoring(); + + // Switch to remote mode + ret = SetRemoteMode(); + if (ret != DEVICE_OK) + return ret; + + // Get version and unit info + ret = GetVersion(version_); + if (ret != DEVICE_OK) + return ret; + + ret = GetUnit(unit_); + if (ret != DEVICE_OK) + return ret; + + LogMessage(("Microscope Version: " + version_).c_str(), false); + LogMessage(("Microscope Unit: " + unit_).c_str(), false); + + // Detect available devices + ret = DoDeviceDetection(); + if (ret != DEVICE_OK) + return ret; + + // Initialize MCU indicators if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + // Initialize nosepiece indicator (I1) + if (model_.IsDevicePresent(DeviceType_Nosepiece)) + { + long pos = model_.GetPosition(DeviceType_Nosepiece); + // Position will be 0 if unknown (not yet queried), display as unknown + UpdateNosepieceIndicator(pos == 0 ? -1 : static_cast(pos)); + } + else + { + // No nosepiece, display "---" + UpdateNosepieceIndicator(-1); + } + + // Initialize mirror unit indicator (I2) + if (model_.IsDevicePresent(DeviceType_MirrorUnit1)) + { + long pos = model_.GetPosition(DeviceType_MirrorUnit1); + // Position will be 0 if unknown (not yet queried), display as unknown + UpdateMirrorUnitIndicator(pos == 0 ? -1 : static_cast(pos)); + } + else + { + // No mirror unit, display "---" + UpdateMirrorUnitIndicator(-1); + } + + // Enable encoder E1 for nosepiece control if nosepiece is present + if (model_.IsDevicePresent(DeviceType_Nosepiece)) + { + std::string cmd = BuildCommand(CMD_ENCODER1, 1); // Enable encoder + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + // Verify response is "E1 0" + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] == "0") + { + LogMessage("Encoder E1 enabled for nosepiece control", false); + } + else + { + LogMessage(("Unexpected response to E1 enable: " + response).c_str(), false); + } + } + else + { + LogMessage("Failed to enable encoder E1", false); + } + } + + // Enable encoder E2 for mirror unit control if mirror unit is present + if (model_.IsDevicePresent(DeviceType_MirrorUnit1)) + { + std::string cmd = BuildCommand(CMD_ENCODER2, 1); // Enable encoder + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + // Verify response is "E2 0" + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] == "0") + { + LogMessage("Encoder E2 enabled for mirror unit control", false); + } + else + { + LogMessage(("Unexpected response to E2 enable: " + response).c_str(), false); + } + } + else + { + LogMessage("Failed to enable encoder E2", false); + } + } + + // Enable encoder E3 for DIA brightness control if DIA shutter is present + if (model_.IsDevicePresent(DeviceType_DIAShutter)) + { + std::string cmd = BuildCommand(CMD_ENCODER3, 1); // Enable encoder + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + // Verify response is "E3 0" + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] == "0") + { + LogMessage("Encoder E3 enabled for DIA brightness control", false); + } + else + { + LogMessage(("Unexpected response to E3 enable: " + response).c_str(), false); + } + } + else + { + LogMessage("Failed to enable encoder E3", false); + } + } + + // Enable jog (focus) control if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_JOG, 1); // Enable jog control + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Jog (focus) control enabled on MCU", false); + } + else + { + LogMessage("Failed to enable jog control", false); + } + } + + // Enable MCU switches (S2) if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_MCZ_SWITCH, 1); // Enable switches + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("MCU switches (S2) enabled", false); + } + else + { + LogMessage("Failed to enable MCU switches", false); + } + } + + // Initialize light path indicator (I4) + if (model_.IsDevicePresent(DeviceType_LightPath)) + { + long pos = model_.GetPosition(DeviceType_LightPath); + // Position will be 0 if unknown (not yet queried), display as unknown (all off) + UpdateLightPathIndicator(pos == 0 ? -1 : static_cast(pos)); + } + else + { + // No light path, display all off + UpdateLightPathIndicator(-1); + } + + // Initialize EPI shutter 1 indicator (I5) + if (model_.IsDevicePresent(DeviceType_EPIShutter1)) + { + long state = model_.GetPosition(DeviceType_EPIShutter1); + // Position will be 0 if unknown (not yet queried), display as closed (I5 1) + UpdateEPIShutter1Indicator(state == 0 ? 0 : static_cast(state)); + } + else + { + // No EPI shutter 1, display as closed + UpdateEPIShutter1Indicator(0); + } + } + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentHub::Shutdown() +{ + if (initialized_) + { + // Disable all active notifications BEFORE stopping monitoring thread + for (auto deviceType : availableDevices_) + { + // Disable notifications for devices that support them + switch (deviceType) + { + case DeviceType_Focus: + EnableNotification(CMD_FOCUS_NOTIFY, false); + break; + case DeviceType_Nosepiece: + EnableNotification(CMD_NOSEPIECE_NOTIFY, false); + break; + case DeviceType_Magnification: + EnableNotification(CMD_MAGNIFICATION_NOTIFY, false); + break; + // Add more as needed + default: + break; + } + } + + // Disable encoder E1 if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_ENCODER1, 0); // Disable encoder + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Encoder E1 disabled", true); + } + + else + { + LogMessage("Failed to disable encoder E1", true); + } + } + + // Disable encoder E2 if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_ENCODER2, 0); // Disable encoder + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Encoder E2 disabled", true); + } + else + { + LogMessage("Failed to disable encoder E2", true); + } + } + + // Disable encoder E3 if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_ENCODER3, 0); // Disable encoder + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Encoder E3 disabled", true); + } + else + { + LogMessage("Failed to disable encoder E3", true); + } + } + + // Disable jog control if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_JOG, 0); // Disable jog control + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Jog (focus) control disabled", true); + } + else + { + LogMessage("Failed to disable jog control", true); + } + } + + // Disable MCU switches (S2) if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_MCZ_SWITCH, 0); // Disable switches + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("MCU switches (S2) disabled", true); + } + else + { + LogMessage("Failed to disable MCU switches", true); + } + } + + // Switch back to local mode + std::string cmd = BuildCommand(CMD_LOGIN, 0); // 0 = Local mode + std::string response; + ExecuteCommand(cmd, response); + + // Now stop the monitoring thread (after all commands sent) + StopMonitoring(); + + initialized_ = false; + } + return DEVICE_OK; +} + +int EvidentHub::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 EvidentHub::OnAnswerTimeout(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(answerTimeoutMs_); + } + else if (eAct == MM::AfterSet) + { + pProp->Get(answerTimeoutMs_); + } + return DEVICE_OK; +} + +int EvidentHub::ClearPort() +{ + const unsigned int bufSize = 255; + unsigned char clear[bufSize]; + unsigned long read = bufSize; + int ret; + + while (read == bufSize) + { + ret = ReadFromComPort(port_.c_str(), clear, bufSize, read); + if (ret != DEVICE_OK) + return ret; + } + return DEVICE_OK; +} + + +int EvidentHub::SetRemoteMode() +{ + std::string cmd = BuildCommand(CMD_LOGIN, 1); // 1 = Remote mode + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_LOGIN)) + return ERR_NOT_IN_REMOTE_MODE; + + return DEVICE_OK; +} + +int EvidentHub::GetVersion(std::string& version) +{ + std::string cmd = BuildCommand(CMD_VERSION, 1); // 1 = Firmware version + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsValidAnswer(response, CMD_VERSION)) + { + // Response format: "V +" - version command doesn't return version number + // Version is embedded in the response or needs separate query + version = response.substr(2); + return DEVICE_OK; + } + + return ERR_INVALID_RESPONSE; +} + +int EvidentHub::GetUnit(std::string& unit) +{ + std::string cmd = BuildQuery(CMD_UNIT); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Parse response: "U IX5,..." + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + unit = params[0]; + return DEVICE_OK; + } + + return ERR_INVALID_RESPONSE; +} + +int EvidentHub::GetUnitDirect(std::string& unit) +{ + std::string cmd = BuildQuery(CMD_UNIT); + std::string response; + int ret = SendCommand(cmd); + if (ret != DEVICE_OK) + return ret; + ret = GetSerialAnswer(port_.c_str(), TERMINATOR, response); + if (ret != DEVICE_OK) + return ret; + + // Parse response: "U IX5,..." + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + unit = params[0]; + return DEVICE_OK; + } + + return ERR_INVALID_RESPONSE; +} + +int EvidentHub::ExecuteCommand(const std::string& command, std::string& response) +{ + std::lock_guard lock(commandMutex_); + + int ret = SendCommand(command); + if (ret != DEVICE_OK) + return ret; + + // Extract expected response tag from command + // First strip any trailing whitespace/terminators from the command + std::string cleanCommand = command; + size_t end = cleanCommand.find_last_not_of(" \t\r\n"); + if (end != std::string::npos) + cleanCommand = cleanCommand.substr(0, end + 1); + std::string expectedTag = ExtractTag(cleanCommand); + + ret = GetResponse(response, answerTimeoutMs_); + if (ret != DEVICE_OK) + return ret; + + // Verify response tag matches command tag + std::string responseTag = ExtractTag(response); + if (responseTag != expectedTag) + { + // Received wrong response - this can happen if responses arrive out of order + LogMessage(("Warning: Expected response for '" + expectedTag + + "' but received '" + responseTag + "' (" + response + + "). Discarding and waiting for correct response.").c_str(), false); + + // Wait for the correct response (with remaining timeout) + ret = GetResponse(response, answerTimeoutMs_); + if (ret != DEVICE_OK) + return ret; + + // Check again + responseTag = ExtractTag(response); + if (responseTag != expectedTag) + { + LogMessage(("Error: Still did not receive expected response for '" + + expectedTag + "', got '" + responseTag + "' instead.").c_str(), false); + return ERR_INVALID_RESPONSE; + } + } + + return DEVICE_OK; +} + +int EvidentHub::SendCommand(const std::string& command) +{ + LogMessage(("Sending: " + command).c_str(), true); + int ret = SendSerialCommand(port_.c_str(), command.c_str(), TERMINATOR); + if (ret != DEVICE_OK) + return ret; + + return DEVICE_OK; +} + +int EvidentHub::GetResponse(std::string& response, long timeoutMs) +{ + if (timeoutMs < 0) + timeoutMs = answerTimeoutMs_; + + // Wait for the monitoring thread to provide a response + std::unique_lock lock(responseMutex_); + responseReady_ = false; + + if (responseCV_.wait_for(lock, std::chrono::milliseconds(timeoutMs), + [this] { return responseReady_; })) + { + response = pendingResponse_; + LogMessage(("Received: " + response).c_str(), true); + return DEVICE_OK; + } + + return ERR_COMMAND_TIMEOUT; +} + +int EvidentHub::DoDeviceDetection() +{ + availableDevices_.clear(); + detectedDevicesByName_.clear(); + + // Use V command to detect device presence + // This avoids firmware bugs with individual device queries + std::string version; + + // V2 - Nosepiece + if (QueryDevicePresenceByVersion(V_NOSEPIECE, version) == DEVICE_OK) + { + LogMessage(("Detected Nosepiece (V2): " + version).c_str()); + model_.SetDevicePresent(DeviceType_Nosepiece, true); + model_.SetDeviceVersion(DeviceType_Nosepiece, version); + availableDevices_.push_back(DeviceType_Nosepiece); + detectedDevicesByName_.push_back(g_NosepieceDeviceName); + // Query actual position/numPositions + QueryNosepiece(); + } + + // V5 - Focus + if (QueryDevicePresenceByVersion(V_FOCUS, version) == DEVICE_OK) + { + LogMessage(("Detected Focus (V5): " + version).c_str()); + model_.SetDevicePresent(DeviceType_Focus, true); + model_.SetDeviceVersion(DeviceType_Focus, version); + availableDevices_.push_back(DeviceType_Focus); + detectedDevicesByName_.push_back(g_FocusDeviceName); + // Query actual position/limits + QueryFocus(); + } + + // V6 - Light Path + if (QueryDevicePresenceByVersion(V_LIGHTPATH, version) == DEVICE_OK) + { + LogMessage(("Detected LightPath (V6): " + version).c_str()); + model_.SetDevicePresent(DeviceType_LightPath, true); + model_.SetDeviceVersion(DeviceType_LightPath, version); + availableDevices_.push_back(DeviceType_LightPath); + detectedDevicesByName_.push_back(g_LightPathDeviceName); + // Query actual position + QueryLightPath(); + } + + // V7 - Condenser Unit (IX3-LWUCDA): Contains Polarizer, CondenserTurret, DIAShutter, DIAAperture + if (QueryDevicePresenceByVersion(V_CONDENSER_UNIT, version) == DEVICE_OK) + { + LogMessage(("Detected Condenser Unit (V7): " + version).c_str()); + + // Polarizer + model_.SetDevicePresent(DeviceType_Polarizer, true); + model_.SetDeviceVersion(DeviceType_Polarizer, version); + availableDevices_.push_back(DeviceType_Polarizer); + detectedDevicesByName_.push_back(g_PolarizerDeviceName); + QueryPolarizer(); + + // Condenser Turret + model_.SetDevicePresent(DeviceType_CondenserTurret, true); + model_.SetDeviceVersion(DeviceType_CondenserTurret, version); + availableDevices_.push_back(DeviceType_CondenserTurret); + detectedDevicesByName_.push_back(g_CondenserTurretDeviceName); + QueryCondenserTurret(); + + // DIA Shutter + model_.SetDevicePresent(DeviceType_DIAShutter, true); + model_.SetDeviceVersion(DeviceType_DIAShutter, version); + availableDevices_.push_back(DeviceType_DIAShutter); + detectedDevicesByName_.push_back(g_DIAShutterDeviceName); + QueryDIAShutter(); + } + + // V8 - DIC Unit (IX5-DICTA): Contains DICPrism, DICRetardation + if (QueryDevicePresenceByVersion(V_DIC_UNIT, version) == DEVICE_OK) + { + LogMessage(("Detected DIC Unit (V8): " + version).c_str()); + + // DIC Prism + model_.SetDevicePresent(DeviceType_DICPrism, true); + model_.SetDeviceVersion(DeviceType_DICPrism, version); + availableDevices_.push_back(DeviceType_DICPrism); + detectedDevicesByName_.push_back(g_DICPrismDeviceName); + QueryDICPrism(); + } + + // V9 - Mirror Unit 1 + if (QueryDevicePresenceByVersion(V_MIRROR_UNIT1, version) == DEVICE_OK) + { + LogMessage(("Detected MirrorUnit1 (V9): " + version).c_str()); + model_.SetDevicePresent(DeviceType_MirrorUnit1, true); + model_.SetDeviceVersion(DeviceType_MirrorUnit1, version); + availableDevices_.push_back(DeviceType_MirrorUnit1); + detectedDevicesByName_.push_back(g_MirrorUnit1DeviceName); + QueryMirrorUnit1(); + } + + // V10 - EPI Shutter 1 + if (QueryDevicePresenceByVersion(V_EPI_SHUTTER1, version) == DEVICE_OK) + { + LogMessage(("Detected EPIShutter1 (V10): " + version).c_str()); + model_.SetDevicePresent(DeviceType_EPIShutter1, true); + model_.SetDeviceVersion(DeviceType_EPIShutter1, version); + availableDevices_.push_back(DeviceType_EPIShutter1); + detectedDevicesByName_.push_back(g_EPIShutter1DeviceName); + QueryEPIShutter1(); + } + + // V11 - Mirror Unit 2 + if (QueryDevicePresenceByVersion(V_MIRROR_UNIT2, version) == DEVICE_OK) + { + LogMessage(("Detected MirrorUnit2 (V11): " + version).c_str()); + model_.SetDevicePresent(DeviceType_MirrorUnit2, true); + model_.SetDeviceVersion(DeviceType_MirrorUnit2, version); + availableDevices_.push_back(DeviceType_MirrorUnit2); + detectedDevicesByName_.push_back(g_MirrorUnit2DeviceName); + QueryMirrorUnit2(); + } + + // V12 - EPI Shutter 2 + if (QueryDevicePresenceByVersion(V_EPI_SHUTTER2, version) == DEVICE_OK) + { + LogMessage(("Detected EPIShutter2 (V12): " + version).c_str()); + model_.SetDevicePresent(DeviceType_EPIShutter2, true); + model_.SetDeviceVersion(DeviceType_EPIShutter2, version); + availableDevices_.push_back(DeviceType_EPIShutter2); + detectedDevicesByName_.push_back(g_EPIShutter2DeviceName); + QueryEPIShutter2(); + } + + // V14 - EPI ND Filter + if (QueryDevicePresenceByVersion(V_EPIND, version) == DEVICE_OK) + { + LogMessage(("Detected EPIND (V14): " + version).c_str()); + model_.SetDevicePresent(DeviceType_EPIND, true); + model_.SetDeviceVersion(DeviceType_EPIND, version); + availableDevices_.push_back(DeviceType_EPIND); + detectedDevicesByName_.push_back(g_EPINDDeviceName); + QueryEPIND(); + } + + // V13 - Manual Control Unit (MCU) + if (QueryDevicePresenceByVersion(V_MANUAL_CONTROL, version) == DEVICE_OK) + { + LogMessage(("Detected Manual Control Unit (V13): " + version).c_str()); + model_.SetDevicePresent(DeviceType_ManualControl, true); + model_.SetDeviceVersion(DeviceType_ManualControl, version); + // Note: MCU is not added to availableDevices/detectedDevicesByName + // as it's not a standalone MM device but provides indicator feedback + } + + // Keep legacy query methods for devices without clear V mapping + + // Magnification (CA command) - V mapping unclear, keep existing query + if (QueryMagnification() == DEVICE_OK) + { + LogMessage("Detected Magnification (CA)"); + availableDevices_.push_back(DeviceType_Magnification); + detectedDevicesByName_.push_back(g_MagnificationDeviceName); + model_.SetDevicePresent(DeviceType_Magnification, true); + } + + // Correction Collar - Present whenever Focus drive is present + // Note: "CC?" returns "X" when not linked, so we can't use QueryCorrectionCollar() + if (model_.IsDevicePresent(DeviceType_Focus)) + { + LogMessage("Detected CorrectionCollar (present with Focus drive)"); + availableDevices_.push_back(DeviceType_CorrectionCollar); + detectedDevicesByName_.push_back(g_CorrectionCollarDeviceName); + model_.SetDevicePresent(DeviceType_CorrectionCollar, true); + // Position will be 0 until device is linked + model_.SetPosition(DeviceType_CorrectionCollar, 0); + } + + std::ostringstream msg; + msg << "Discovered " << availableDevices_.size() << " devices"; + LogMessage(msg.str().c_str(), false); + + return DEVICE_OK; +} + +MM::DeviceDetectionStatus EvidentHub::DetectDevice(void) +{ + + if (initialized_) + return MM::CanCommunicate; + + // our property port_ should have been set to one of the valid ports + + + // all conditions must be satisfied... + MM::DeviceDetectionStatus result = MM::Misconfigured; + char answerTO[MM::MaxStrLength]; + + try + { + std::string portLowerCase = port_; + for( std::string::iterator its = portLowerCase.begin(); its != portLowerCase.end(); ++its) + { + *its = (char)tolower(*its); + } + if( 0< portLowerCase.length() && 0 != portLowerCase.compare("undefined") && 0 != portLowerCase.compare("unknown") ) + { + result = MM::CanNotCommunicate; + // record current port settings + GetCoreCallback()->GetDeviceProperty(port_.c_str(), "AnswerTimeout", answerTO); + + // device specific default communication parameters + GetCoreCallback()->SetDeviceProperty(port_.c_str(), MM::g_Keyword_BaudRate, "115200" ); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), MM::g_Keyword_StopBits, "1"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), MM::g_Keyword_Parity, "Even"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "Verbose", "0"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "AnswerTimeout", "5000.0"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "DelayBetweenCharsMs", "0"); + MM::Device* pS = GetCoreCallback()->GetDevice(this, port_.c_str()); + pS->Initialize(); + std::string unit; + int ret = GetUnitDirect(unit); + if (ret != DEVICE_OK || unit != "IX5") + { + pS->Shutdown(); + // always restore the AnswerTimeout to the default + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "AnswerTimeout", answerTO); + return result; + } + result = MM::CanCommunicate; + pS->Shutdown(); + // always restore the AnswerTimeout to the default + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "AnswerTimeout", answerTO); + + } + } + catch(...) + { + LogMessage("Exception in DetectDevice!",false); + } + return result; + +} + +int EvidentHub::DetectInstalledDevices() +{ + for (size_t i=0; i < detectedDevicesByName_.size(); i++) + { + MM::Device* pDev = ::CreateDevice(detectedDevicesByName_[i].c_str()); + if (pDev) + AddInstalledDevice(pDev); + } + + return DEVICE_OK; +} + +int EvidentHub::QueryDevicePresenceByVersion(int unitNumber, std::string& version) +{ + std::string cmd = BuildCommand(CMD_VERSION, unitNumber); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Check for error response indicating device not present + if (IsNegativeAck(response, CMD_VERSION)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Parse version string from response + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + version = params[0]; + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +bool EvidentHub::IsDevicePresent(EvidentIX85::DeviceType type) const +{ + return model_.IsDevicePresent(type); +} + +std::string EvidentHub::GetDeviceVersion(EvidentIX85::DeviceType type) const +{ + return model_.GetDeviceVersion(type); +} + +int EvidentHub::EnableNotification(const char* cmd, bool enable) +{ + std::string command = BuildCommand(cmd, enable ? 1 : 0); + std::string response; + + int ret = ExecuteCommand(command, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, cmd)) + return ERR_NEGATIVE_ACK; + + return DEVICE_OK; +} + +// Device query implementations +int EvidentHub::QueryFocus() +{ + std::string cmd = BuildQuery(CMD_FOCUS_POSITION); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + long pos = ParseLongParameter(params[0]); + model_.SetPosition(DeviceType_Focus, pos); + model_.SetLimits(DeviceType_Focus, FOCUS_MIN_POS, FOCUS_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryNosepiece() +{ + std::string cmd = BuildQuery(CMD_NOSEPIECE); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_Nosepiece, pos); + model_.SetNumPositions(DeviceType_Nosepiece, NOSEPIECE_MAX_POS); + model_.SetLimits(DeviceType_Nosepiece, NOSEPIECE_MIN_POS, NOSEPIECE_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryMagnification() +{ + std::string cmd = BuildQuery(CMD_MAGNIFICATION); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_Magnification, pos); + model_.SetNumPositions(DeviceType_Magnification, MAGNIFICATION_MAX_POS); + model_.SetLimits(DeviceType_Magnification, MAGNIFICATION_MIN_POS, MAGNIFICATION_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryLightPath() +{ + std::string cmd = BuildQuery(CMD_LIGHT_PATH); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_LightPath, pos); + model_.SetNumPositions(DeviceType_LightPath, 4); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryCondenserTurret() +{ + std::string cmd = BuildQuery(CMD_CONDENSER_TURRET); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_CondenserTurret, pos); + model_.SetNumPositions(DeviceType_CondenserTurret, CONDENSER_TURRET_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryDIAAperture() +{ + std::string cmd = BuildQuery(CMD_DIA_APERTURE); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_DIAAperture, pos); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryDIAShutter() +{ + std::string cmd = BuildQuery(CMD_DIA_SHUTTER); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int state = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_DIAShutter, state); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryPolarizer() +{ + std::string cmd = BuildQuery(CMD_POLARIZER); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Note: This function is now only called after V7 confirms condenser unit is present + // May still return "X" on first query due to firmware bug, but that's okay - + // we'll just set position to 0 (Out) and device will work correctly + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 0) + { + model_.SetPosition(DeviceType_Polarizer, pos); + } + else + { + // Firmware returned "X", set default position (0 = Out) + model_.SetPosition(DeviceType_Polarizer, 0); + } + model_.SetNumPositions(DeviceType_Polarizer, POLARIZER_MAX_POS); + return DEVICE_OK; + } + + return DEVICE_OK; // Device present (confirmed by V7), just couldn't get position +} + +int EvidentHub::QueryDICPrism() +{ + std::string cmd = BuildQuery(CMD_DIC_PRISM); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Note: This function is now only called after V8 confirms DIC unit is present + // May still return "X" on first query due to firmware bug, but that's okay - + // we'll just set position to 0 and device will work correctly + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 0) + { + model_.SetPosition(DeviceType_DICPrism, pos); + } + else + { + // Firmware returned "X", set default position (0) + model_.SetPosition(DeviceType_DICPrism, 0); + } + model_.SetNumPositions(DeviceType_DICPrism, DIC_PRISM_MAX_POS); + return DEVICE_OK; + } + + return DEVICE_OK; // Device present (confirmed by V8), just couldn't get position +} + +int EvidentHub::QueryDICRetardation() +{ + std::string cmd = BuildQuery(CMD_DIC_RETARDATION); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_DICRetardation, pos); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryEPIShutter1() +{ + std::string cmd = BuildQuery(CMD_EPI_SHUTTER1); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int state = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_EPIShutter1, state); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryEPIShutter2() +{ + std::string cmd = BuildQuery(CMD_EPI_SHUTTER2); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int state = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_EPIShutter2, state); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryMirrorUnit1() +{ + std::string cmd = BuildQuery(CMD_MIRROR_UNIT1); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_MirrorUnit1, pos); + model_.SetNumPositions(DeviceType_MirrorUnit1, MIRROR_UNIT_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryMirrorUnit2() +{ + std::string cmd = BuildQuery(CMD_MIRROR_UNIT2); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_MirrorUnit2, pos); + model_.SetNumPositions(DeviceType_MirrorUnit2, MIRROR_UNIT_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryEPIND() +{ + std::string cmd = BuildQuery(CMD_EPI_ND); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_EPIND, pos); + model_.SetNumPositions(DeviceType_EPIND, EPIND_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryRightPort() +{ + std::string cmd = BuildQuery(CMD_RIGHT_PORT); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_RightPort, pos); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::QueryCorrectionCollar() +{ + std::string cmd = BuildQuery(CMD_CORRECTION_COLLAR); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_CorrectionCollar, pos); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHub::UpdateNosepieceIndicator(int position) +{ + // Check if MCU is present + if (!model_.IsDevicePresent(DeviceType_ManualControl)) + return DEVICE_OK; // Not an error, MCU just not present + + std::string cmd; + + // Position -1 means unknown, display "---" (three dashes) + if (position == -1 || position < 1 || position > 9) + { + // Three dashes: 0x01,0x01,0x01 + cmd = "I1 010101"; + } + else + { + // Single digit display - get 7-segment code + int code = Get7SegmentCode(position); + + // Format as hex string (2 digits) + std::ostringstream oss; + oss << "I1 " << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << code; + cmd = oss.str(); + } + + // Send command without waiting for response (to avoid deadlock when called from monitoring thread) + // The "I1 +" response will be consumed by the monitoring thread as a pseudo-notification + int ret = SendCommand(cmd); + if (ret != DEVICE_OK) + { + LogMessage(("Failed to send nosepiece indicator command: " + cmd).c_str()); + return ret; + } + + LogMessage(("Sent nosepiece indicator command: " + cmd).c_str(), true); + return DEVICE_OK; +} + +int EvidentHub::UpdateMirrorUnitIndicator(int position) +{ + // Check if MCU is present + if (!model_.IsDevicePresent(DeviceType_ManualControl)) + return DEVICE_OK; // Not an error, MCU just not present + + std::string cmd; + + // Position -1 means unknown, display "---" (three dashes) + if (position == -1 || position < 1 || position > 9) + { + // Three dashes: 0x01,0x01,0x01 + cmd = "I2 010101"; + } + else + { + // Single digit display - get 7-segment code + int code = Get7SegmentCode(position); + + // Format as hex string (2 digits) + std::ostringstream oss; + oss << "I2 " << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << code; + cmd = oss.str(); + } + + // Send command without waiting for response (to avoid deadlock when called from monitoring thread) + // The "I2 +" response will be consumed by the monitoring thread as a pseudo-notification + int ret = SendCommand(cmd); + if (ret != DEVICE_OK) + { + LogMessage(("Failed to send mirror unit indicator command: " + cmd).c_str()); + return ret; + } + + LogMessage(("Sent mirror unit indicator command: " + cmd).c_str(), true); + return DEVICE_OK; +} + +int EvidentHub::UpdateLightPathIndicator(int position) +{ + // Check if MCU is present + if (!model_.IsDevicePresent(DeviceType_ManualControl)) + return DEVICE_OK; // Not an error, MCU just not present + + std::string cmd; + + // Map LightPath position (1-4) to I4 indicator value + // Position 1 (Left Port) -> I4 1 (camera) + // Position 2 (Binocular 50/50) -> I4 2 (50:50) + // Position 3 (Binocular 100%) -> I4 4 (eyepiece) + // Position 4 (Right Port) -> I4 0 (all off) + // Unknown -> I4 0 (all off) + + int i4Value; + if (position == 1) + i4Value = 1; // Left Port -> camera + else if (position == 2) + i4Value = 2; // 50:50 + else if (position == 3) + i4Value = 4; // Binocular 100% -> eyepiece + else + i4Value = 0; // Right Port or unknown -> all off + + std::ostringstream oss; + oss << "I4 " << i4Value; + cmd = oss.str(); + + // Send command without waiting for response (to avoid deadlock when called from monitoring thread) + // The "I4 +" response will be consumed by the monitoring thread as a pseudo-notification + int ret = SendCommand(cmd); + if (ret != DEVICE_OK) + { + LogMessage(("Failed to send light path indicator command: " + cmd).c_str()); + return ret; + } + + LogMessage(("Sent light path indicator command: " + cmd).c_str(), true); + return DEVICE_OK; +} + +int EvidentHub::UpdateEPIShutter1Indicator(int state) +{ + // Check if MCU is present + if (!model_.IsDevicePresent(DeviceType_ManualControl)) + return DEVICE_OK; // Not an error, MCU just not present + + std::string cmd; + + // Map EPI Shutter state to I5 indicator value + // State 0 (Closed) -> I5 1 + // State 1 (Open) -> I5 2 + // Unknown or other -> I5 1 (default to closed) + + int i5Value; + if (state == 1) + i5Value = 2; // Open -> 2 + else + i5Value = 1; // Closed or unknown -> 1 + + std::ostringstream oss; + oss << "I5 " << i5Value; + cmd = oss.str(); + + // Send command without waiting for response (to avoid deadlock when called from monitoring thread) + // The "I5 +" response will be consumed by the monitoring thread as a pseudo-notification + int ret = SendCommand(cmd); + if (ret != DEVICE_OK) + { + LogMessage(("Failed to send EPI shutter indicator command: " + cmd).c_str()); + return ret; + } + + LogMessage(("Sent EPI shutter indicator command: " + cmd).c_str(), true); + return DEVICE_OK; +} + +int EvidentHub::UpdateDIABrightnessIndicator(int brightness) +{ + // Check if MCU is present + if (!model_.IsDevicePresent(DeviceType_ManualControl)) + return DEVICE_OK; // Not an error, MCU just not present + + // Map brightness (0-255) to I3 indicator LED pattern (hex values) + // I3 accepts hex bitmask values: 1, 3, 7, F, 1F + // 0 brightness -> no LEDs (I3 0) + // 1-51 -> 1 LED (I3 1) + // 52-102 -> 2 LEDs (I3 3) + // 103-153 -> 3 LEDs (I3 7) + // 154-204 -> 4 LEDs (I3 F) + // 205-255 -> 5 LEDs (I3 1F) + + std::string i3Value; + if (brightness == 0) + i3Value = "0"; + else if (brightness <= 51) + i3Value = "1"; + else if (brightness <= 102) + i3Value = "3"; + else if (brightness <= 153) + i3Value = "7"; + else if (brightness <= 204) + i3Value = "F"; + else + i3Value = "1F"; + + std::string cmd = "I3 " + i3Value; + + // Send command without waiting for response + int ret = SendCommand(cmd); + if (ret != DEVICE_OK) + { + LogMessage(("Failed to send DIA brightness indicator command: " + cmd).c_str()); + return ret; + } + + LogMessage(("Sent DIA brightness indicator command: " + cmd).c_str(), true); + return DEVICE_OK; +} + +// Monitoring thread +void EvidentHub::StartMonitoring() +{ + if (monitorThread_.joinable()) + return; // Already running + + stopMonitoring_ = false; + monitorThread_ = std::thread(&EvidentHub::MonitorThreadFunc, this); + + LogMessage("Monitoring thread started", true); +} + +void EvidentHub::StopMonitoring() +{ + if (!monitorThread_.joinable()) + return; // Not running + + stopMonitoring_ = true; + + // Wake up any waiting command threads + { + std::lock_guard lock(responseMutex_); + responseReady_ = false; + } + responseCV_.notify_all(); + + monitorThread_.join(); + + LogMessage("Monitoring thread stopped", true); +} + +void EvidentHub::MonitorThreadFunc() +{ + // This thread is the SOLE reader from the serial port + // It routes messages to either: + // - Command thread (via condition variable) for responses + // - Processes directly for notifications + + LogMessage("Monitor thread function started", true); + + std::string buffer; + + while (!stopMonitoring_.load()) + { + // Read one byte at a time from serial port + unsigned char byte; + unsigned long read; + int ret = ReadFromComPort(port_.c_str(), &byte, 1, read); + + if (ret != DEVICE_OK || read == 0) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + + // Build message byte by byte + if (byte == '\n') + { + // End of line - remove trailing \r if present + if (!buffer.empty() && buffer.back() == '\r') + buffer.pop_back(); + + if (!buffer.empty()) + { + // Determine if this is a notification or command response + if (IsNotificationTag(buffer)) + { + // This is a notification - process it + LogMessage(("Notification: " + buffer).c_str(), true); + ProcessNotification(buffer); + } + else + { + // This is a command response - pass to waiting command thread + LogMessage(("Response (from monitor): " + buffer).c_str(), true); + { + std::lock_guard lock(responseMutex_); + pendingResponse_ = buffer; + responseReady_ = true; + } + responseCV_.notify_one(); + } + + buffer.clear(); + } + } + else + { + buffer += static_cast(byte); + } + } + + LogMessage("Monitor thread function exiting", true); +} + +bool EvidentHub::IsNotificationTag(const std::string& message) const +{ + // Extract tag from the message + std::string tag = ExtractTag(message); + + // Special case: I1, I2, I3, I4, and I5 (indicator) responses must always be consumed by monitoring thread + // This prevents them from being sent to command threads and causing sync issues + if (tag == CMD_INDICATOR1 || tag == CMD_INDICATOR2 || tag == CMD_INDICATOR3 || + tag == CMD_INDICATOR4 || tag == CMD_INDICATOR5) + return true; + + // Special case: E1 (encoder) messages with delta values (-1, 1) are notifications + // but "E1 0" and acknowledgments "E1 +" are command responses + if (tag == CMD_ENCODER1) + { + std::vector params = ParseParameters(message); + if (params.size() > 0) + { + // Reject acknowledgments first (before ParseIntParameter which returns -1 for "+") + if (params[0] == "+" || params[0] == "!") + return false; + + int value = ParseIntParameter(params[0]); + // -1 and 1 are encoder turn notifications + if (value == -1 || value == 1) + return true; + } + // "E1 0" or other values are command responses + return false; + } + + // Special case: E2 (encoder) messages with delta values (-1, 1) are notifications + // but "E2 0" and acknowledgments "E2 +" are command responses + if (tag == CMD_ENCODER2) + { + std::vector params = ParseParameters(message); + if (params.size() > 0) + { + // Reject acknowledgments first (before ParseIntParameter which returns -1 for "+") + if (params[0] == "+" || params[0] == "!") + return false; + + int value = ParseIntParameter(params[0]); + // -1 and 1 are encoder turn notifications + if (value == -1 || value == 1) + return true; + } + // "E2 0" or other values are command responses + return false; + } + + // Special case: E3 (encoder) messages with delta values (-9 to 9) are notifications + // but "E3 0" and acknowledgments "E3 +" are command responses + if (tag == CMD_ENCODER3) + { + std::vector params = ParseParameters(message); + if (params.size() > 0) + { + // Reject acknowledgments first (before ParseIntParameter which returns -1 for "+") + if (params[0] == "+" || params[0] == "!") + return false; + + int value = ParseIntParameter(params[0]); + // Fast turning can produce delta values from -9 to 9 (excluding 0) + if (value >= -9 && value <= 9 && value != 0) + return true; + } + // "E3 0" or other values are command responses + return false; + } + + // Special case: S2 (MCU switches) messages with hex bitmask are notifications + // but acknowledgments "S2 +" are command responses + if (tag == CMD_MCZ_SWITCH) + { + std::vector params = ParseParameters(message); + if (params.size() > 0) + { + // Only reject acknowledgments + if (params[0] == "+" || params[0] == "!") + return false; + + // All hex bitmask values (0-7F) are notifications + return true; + } + return false; + } + + // Check if it's a known notification tag + bool isNotifyTag = (tag == CMD_FOCUS_NOTIFY || + tag == CMD_NOSEPIECE_NOTIFY || + tag == CMD_MAGNIFICATION_NOTIFY || + tag == CMD_CONDENSER_TURRET_NOTIFY || + tag == CMD_DIA_APERTURE_NOTIFY || + tag == CMD_DIA_ILLUMINATION_NOTIFY || + tag == CMD_POLARIZER_NOTIFY || + tag == CMD_DIC_RETARDATION_NOTIFY || + tag == CMD_DIC_LOCALIZED_NOTIFY || + tag == CMD_MIRROR_UNIT_NOTIFY1 || + tag == CMD_MIRROR_UNIT_NOTIFY2 || + tag == CMD_RIGHT_PORT_NOTIFY || + tag == CMD_OFFSET_LENS_NOTIFY); + + if (!isNotifyTag) + return false; + + // It's a notify tag, but is it an acknowledgment or actual notification? + // Acknowledgments: "NCA +", "NFP !" + // Notifications: "NCA 1", "NFP 3110" + // Only treat as notification if it's NOT an acknowledgment + return !IsPositiveAck(message, tag.c_str()) && !IsNegativeAck(message, tag.c_str()); +} + +void EvidentHub::ProcessNotification(const std::string& message) +{ + std::string tag = ExtractTag(message); + std::vector params = ParseParameters(message); + + // Update model based on notification + if (tag == CMD_FOCUS_NOTIFY && params.size() > 0) + { + long pos = ParseLongParameter(params[0]); + if (pos >= 0) + { + model_.SetPosition(DeviceType_Focus, pos); + + // Notify core callback of stage position change + auto it = usedDevices_.find(DeviceType_Focus); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Convert from 10nm units to micrometers + double positionUm = pos * FOCUS_STEP_SIZE_UM; + GetCoreCallback()->OnStagePositionChanged(it->second, positionUm); + } + + // Check if we've reached the target position (with tolerance for mechanical settling) + long targetPos = model_.GetTargetPosition(DeviceType_Focus); + if (IsAtTargetPosition(pos, targetPos, FOCUS_POSITION_TOLERANCE)) + { + model_.SetBusy(DeviceType_Focus, false); + LogMessage("Focus reached target position", true); + } + } + } + else if (tag == CMD_NOSEPIECE_NOTIFY && params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 0) + { + model_.SetPosition(DeviceType_Nosepiece, pos); + + // Update MCU indicator I1 with new nosepiece position + UpdateNosepieceIndicator(pos); + + // Check if we've reached the target position + long targetPos = model_.GetTargetPosition(DeviceType_Nosepiece); + bool isExpectedChange = (targetPos >= 0 && pos == targetPos); + + std::ostringstream msg; + msg << "Nosepiece notification: pos=" << pos << ", targetPos=" << targetPos << ", isExpectedChange=" << isExpectedChange; + LogMessage(msg.str().c_str(), true); + + if (isExpectedChange) + { + LogMessage("Nosepiece reached target position, clearing busy flag", true); + model_.SetBusy(DeviceType_Nosepiece, false); + } + + // Only notify core of property changes for UNSOLICITED changes (manual controls) + // For commanded changes, the core already knows about the change + if (!isExpectedChange) + { + LogMessage("Unsolicited nosepiece change detected, notifying core", true); + auto it = usedDevices_.find(DeviceType_Nosepiece); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Convert from 1-based position to 0-based state value + int stateValue = pos - 1; + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_State, + CDeviceUtils::ConvertToString(stateValue)); + + // This should works since the nosepiece is a state device + // it would be safer to test the type first + char label[MM::MaxStrLength]; + int ret = ((MM::State*) it->second)->GetPositionLabel(stateValue, label); + if (ret == DEVICE_OK) + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_Label, label); + } + } + + } + } + else if (tag == CMD_MAGNIFICATION_NOTIFY && params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 0) + { + model_.SetPosition(DeviceType_Magnification, pos); + auto it = usedDevices_.find(DeviceType_Magnification); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Map position (1-based) to magnification value + const double magnifications[3] = {1.0, 1.6, 2.0}; + if (pos >= 1 && pos <= 3) + { + GetCoreCallback()->OnPropertyChanged(it->second, g_Keyword_Magnification, + CDeviceUtils::ConvertToString(magnifications[pos - 1])); + // Notify core that magnification has changed + GetCoreCallback()->OnMagnifierChanged(it->second); + } + } + } + } + else if (tag == CMD_ENCODER1 && params.size() > 0) + { + // Encoder 1 controls nosepiece position + int delta = ParseIntParameter(params[0]); + if (delta == -1 || delta == 1) + { + // Get current nosepiece position + long currentPos = model_.GetPosition(DeviceType_Nosepiece); + + // If position is unknown (0), we can't handle encoder input + if (currentPos == 0) + { + LogMessage("E1 encoder input ignored - nosepiece position unknown", true); + return; + } + + // Calculate new position with wrapping + int newPos = static_cast(currentPos) + delta; + int maxPos = NOSEPIECE_MAX_POS; // 6 + + if (newPos < 1) + newPos = maxPos; // Wrap from 1 to max + else if (newPos > maxPos) + newPos = 1; // Wrap from max to 1 + + // Send OB command to change nosepiece position + std::string cmd = BuildCommand(CMD_NOSEPIECE, newPos); + std::ostringstream msg; + msg << "E1 encoder: moving nosepiece from " << currentPos << " to " << newPos; + LogMessage(msg.str().c_str(), true); + + // Send the command (fire-and-forget, the NOB notification will update the model) + int ret = SendCommand(cmd); + if (ret != DEVICE_OK) + { + LogMessage("Failed to send nosepiece command from encoder", false); + } + } + } + else if (tag == CMD_ENCODER2 && params.size() > 0) + { + // Encoder 2 controls MirrorUnit1 position + int delta = ParseIntParameter(params[0]); + if (delta == -1 || delta == 1) + { + // Get current mirror unit position + long currentPos = model_.GetPosition(DeviceType_MirrorUnit1); + + // If position is unknown (0), we can't handle encoder input + if (currentPos == 0) + { + LogMessage("E2 encoder input ignored - mirror unit position unknown", true); + return; + } + + // Calculate new position with wrapping + int newPos = static_cast(currentPos) + delta; + int maxPos = MIRROR_UNIT_MAX_POS; // 6 + + if (newPos < 1) + newPos = maxPos; // Wrap from 1 to max + else if (newPos > maxPos) + newPos = 1; // Wrap from max to 1 + + // Send MU1 command to change mirror unit position + std::string cmd = BuildCommand(CMD_MIRROR_UNIT1, newPos); + std::ostringstream msg; + msg << "E2 encoder: moving mirror unit from " << currentPos << " to " << newPos; + LogMessage(msg.str().c_str(), true); + + // Use SendCommand (not ExecuteCommand) to avoid deadlock when called from monitoring thread + // The "MU1 +" response will be consumed by monitoring thread as a command response + int ret = SendCommand(cmd); + if (ret == DEVICE_OK) + { + // Optimistically update model and indicator (mirror unit has no position change notifications) + model_.SetPosition(DeviceType_MirrorUnit1, newPos); + UpdateMirrorUnitIndicator(newPos); + + // Notify core callback of State property change + auto it = usedDevices_.find(DeviceType_MirrorUnit1); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Convert from 1-based position to 0-based state value + int stateValue = newPos - 1; + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_State, + CDeviceUtils::ConvertToString(stateValue)); + + // This should works since the MirrorUnit is a state device + // it would be safer to test the type first + char label[MM::MaxStrLength]; + ret = ((MM::State*) it->second)->GetPositionLabel(stateValue, label); + if (ret == DEVICE_OK) + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_Label, label); + } + } + else + { + LogMessage("Failed to send mirror unit command from encoder", false); + } + } + } + else if (tag == CMD_ENCODER3 && params.size() > 0) + { + // Encoder 3 controls DIA brightness + int delta = ParseIntParameter(params[0]); + // Fast turning can produce delta values from -9 to 9 (excluding 0) + if (delta >= -9 && delta <= 9 && delta != 0) + { + // Get current remembered brightness (not actual brightness) + int currentRemembered = rememberedDIABrightness_; + + // Calculate new remembered brightness (each encoder step changes brightness by 3) + int newRemembered = currentRemembered + (delta * 3); + + // Clamp to valid range 0-255 + if (newRemembered < 0) + newRemembered = 0; + else if (newRemembered > 255) + newRemembered = 255; + + // Always update remembered brightness + rememberedDIABrightness_ = newRemembered; + + std::ostringstream msg; + msg << "E3 encoder: changing DIA remembered brightness from " << currentRemembered << " to " << newRemembered; + LogMessage(msg.str().c_str(), true); + + // Always update I3 indicator to match remembered brightness + UpdateDIABrightnessIndicator(newRemembered); + + // Only send DIL command if logical shutter is open (actual brightness > 0) + long currentActual = model_.GetPosition(DeviceType_DIABrightness); + if (currentActual > 0) + { + // Shutter is open: update actual lamp brightness + std::string cmd = BuildCommand(CMD_DIA_ILLUMINATION, newRemembered); + int ret = SendCommand(cmd); + if (ret == DEVICE_OK) + { + // Update model with new brightness + model_.SetPosition(DeviceType_DIABrightness, newRemembered); + } + else + { + LogMessage("Failed to send DIA brightness command from encoder", false); + } + } + // If shutter is closed, don't send DIL, don't update model + + // Always update Brightness property callback with remembered value + auto it = usedDevices_.find(DeviceType_DIAShutter); + if (it != usedDevices_.end() && it->second != nullptr) + { + GetCoreCallback()->OnPropertyChanged(it->second, "Brightness", + CDeviceUtils::ConvertToString(newRemembered)); + } + } + } + else if (tag == CMD_DIA_ILLUMINATION_NOTIFY && params.size() > 0) + { + // Handle DIA illumination (brightness) change notification + int brightness = ParseIntParameter(params[0]); + if (brightness >= 0 && brightness <= 255) + { + model_.SetPosition(DeviceType_DIABrightness, brightness); + + // Only update I3 indicator and Brightness property if shutter is open (brightness > 0) + // When closed (brightness = 0), user wants to continue seeing remembered brightness + if (brightness > 0) + { + // Shutter is open: update remembered brightness, I3 indicator, and property + rememberedDIABrightness_ = brightness; + UpdateDIABrightnessIndicator(brightness); + + // Notify core callback of Brightness property change + auto it = usedDevices_.find(DeviceType_DIAShutter); + if (it != usedDevices_.end() && it->second != nullptr) + { + GetCoreCallback()->OnPropertyChanged(it->second, "Brightness", + CDeviceUtils::ConvertToString(brightness)); + } + } + // If brightness = 0 (closed), don't update I3 or Brightness property + // They should continue showing the remembered brightness value + } + } + else if (tag == CMD_MCZ_SWITCH && params.size() > 0) + { + // Handle MCU switch press notifications + // Parameter is a hex bitmask (0-7F) indicating which switch(es) were pressed + // Bit 0 (0x01): Switch 1 - Light Path cycle + // Bit 1 (0x02): Switch 2 - EPI Shutter toggle + // Bit 2 (0x04): Switch 3 - DIA on/off toggle + // Bits 3-6: Reserved for future use + + // Parse hex parameter + int switchMask = 0; + std::istringstream iss(params[0]); + iss >> std::hex >> switchMask; + + if (switchMask < 0 || switchMask > 0x7F) + { + LogMessage(("Invalid S2 switch mask: " + params[0]).c_str(), false); + return; + } + + std::ostringstream msg; + msg << "MCU switch pressed: 0x" << std::hex << std::uppercase << switchMask; + LogMessage(msg.str().c_str(), true); + + // Switch 1 (Bit 0): Light Path cycling + if (switchMask & 0x01) + { + if (model_.IsDevicePresent(DeviceType_LightPath)) + { + long currentPos = model_.GetPosition(DeviceType_LightPath); + // Cycle: 3 (Eyepiece) → 2 (50/50) → 1 (Left Port) → 4 (Right Port) → 3 (Eyepiece) + int newPos; + if (currentPos == 3) // Eyepiece → 50/50 + newPos = 2; + else if (currentPos == 2) // 50/50 → Left Port + newPos = 1; + else if (currentPos == 1) // Left Port → Right Port + newPos = 4; + else // Right Port or unknown → Eyepiece + newPos = 3; + + std::string cmd = BuildCommand(CMD_LIGHT_PATH, newPos); + int ret = SendCommand(cmd); + if (ret == DEVICE_OK) + { + model_.SetPosition(DeviceType_LightPath, newPos); + UpdateLightPathIndicator(newPos); + + auto it = usedDevices_.find(DeviceType_LightPath); + if (it != usedDevices_.end() && it->second != nullptr) + { + int stateValue = newPos - 1; + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_State, + CDeviceUtils::ConvertToString(stateValue)); + + // This should works since the LightPath is a state device + // it would be safer to test the type first + char label[MM::MaxStrLength]; + ret = ((MM::State*) it->second)->GetPositionLabel(stateValue, label); + if (ret == DEVICE_OK) + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_Label, label); + } + LogMessage(("Switch 1: Light path changed to position " + std::to_string(newPos)).c_str(), true); + } + else + { + LogMessage("Failed to change light path from switch", false); + } + } + } + + // Switch 2 (Bit 1): EPI Shutter toggle + if (switchMask & 0x02) + { + auto it = usedDevices_.find(DeviceType_EPIShutter1); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Get current state from model + long currentState = model_.GetPosition(DeviceType_EPIShutter1); + int newState = (currentState == 0) ? 1 : 0; + + // Send toggle command using SendCommand (not ExecuteCommand) to avoid deadlock + std::string cmd = BuildCommand(CMD_EPI_SHUTTER1, newState); + int ret = SendCommand(cmd); + if (ret == DEVICE_OK) + { + model_.SetPosition(DeviceType_EPIShutter1, newState); + UpdateEPIShutter1Indicator(newState); + + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_State, + CDeviceUtils::ConvertToString(static_cast(newState))); + GetCoreCallback()->OnShutterOpenChanged(it->second, newState == 1); + + LogMessage(("Switch 2: EPI shutter toggled to " + std::string(newState ? "open" : "closed")).c_str(), true); + } + else + { + LogMessage("Failed to toggle EPI shutter from switch", false); + } + } + } + + // Switch 3 (Bit 2): DIA on/off toggle (with brightness memory) + if (switchMask & 0x04) + { + auto it = usedDevices_.find(DeviceType_DIAShutter); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Get current brightness from model to determine if logical shutter is open + long currentBrightness = model_.GetPosition(DeviceType_DIABrightness); + int newBrightness; + + if (currentBrightness > 0) + { + // Currently on: remember brightness and turn off + rememberedDIABrightness_ = static_cast(currentBrightness); + newBrightness = 0; + } + else + { + // Currently off: restore remembered brightness + newBrightness = rememberedDIABrightness_; + } + + // Send command using SendCommand (not ExecuteCommand) to avoid deadlock + std::string cmd = BuildCommand(CMD_DIA_ILLUMINATION, newBrightness); + int ret = SendCommand(cmd); + if (ret == DEVICE_OK) + { + model_.SetPosition(DeviceType_DIABrightness, newBrightness); + + // Always update I3 indicator with remembered brightness (not 0 when closing) + // User wants to see the remembered brightness value, not that lamp is off + UpdateDIABrightnessIndicator(rememberedDIABrightness_); + + // Only update State property (logical shutter), NOT Brightness property + // This keeps the Brightness property at its remembered value + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_State, + CDeviceUtils::ConvertToString(newBrightness > 0 ? 1L : 0L)); + GetCoreCallback()->OnShutterOpenChanged(it->second, newBrightness > 0); + + LogMessage(("Switch 3: DIA toggled to " + std::string(newBrightness > 0 ? "on" : "off")).c_str(), true); + } + else + { + LogMessage("Failed to toggle DIA from switch", false); + } + } + } + } + // Add more notification handlers as needed +} diff --git a/DeviceAdapters/EvidentIX85/EvidentHub.h b/DeviceAdapters/EvidentIX85/EvidentHub.h new file mode 100644 index 000000000..e8f682949 --- /dev/null +++ b/DeviceAdapters/EvidentIX85/EvidentHub.h @@ -0,0 +1,152 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentHub.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 microscope hub +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include "DeviceBase.h" +#include "EvidentModel.h" +#include "EvidentProtocol.h" +#include "EvidentIX85.h" +#include +#include +#include +#include +#include +#include + +class EvidentHub : public HubBase +{ +public: + EvidentHub(); + ~EvidentHub(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + bool SupportsDeviceDetection(void) { return true; } + MM::DeviceDetectionStatus DetectDevice(void); + // Hub API + int DetectInstalledDevices(); + + // Action handlers + int OnPort(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnAnswerTimeout(MM::PropertyBase* pProp, MM::ActionType eAct); + + // Hub interface for devices to access state + EvidentIX85::MicroscopeModel* GetModel() { return &model_; } + + // Command execution (thread-safe) + int ExecuteCommand(const std::string& command, std::string& response); + int SendCommand(const std::string& command); + int GetResponse(std::string& response, long timeoutMs = -1); + + // Device discovery + bool IsDevicePresent(EvidentIX85::DeviceType type) const; + std::string GetDeviceVersion(EvidentIX85::DeviceType type) const; + + // Notification control + int EnableNotification(const char* cmd, bool enable); + + void RegisterDeviceAsUsed(EvidentIX85::DeviceType type, MM::Device* device) { usedDevices_[type] = device;}; + void UnRegisterDeviceAsUsed(EvidentIX85::DeviceType type) { usedDevices_.erase(type); }; + + int UpdateMirrorUnitIndicator(int position); + int UpdateLightPathIndicator(int position); + int UpdateEPIShutter1Indicator(int state); + int UpdateDIABrightnessIndicator(int brightness); + + // DIA brightness memory for logical shutter + int GetRememberedDIABrightness() const { return rememberedDIABrightness_; } + void SetRememberedDIABrightness(int brightness) { rememberedDIABrightness_ = brightness; } + +private: + // Initialization helpers + int SetRemoteMode(); + int GetVersion(std::string& version); + int GetUnit(std::string& unit); + int GetUnitDirect(std::string& unit); + int ClearPort(); + int DoDeviceDetection(); + int QueryDevicePresenceByVersion(int unitNumber, std::string& version); + + // Device query helpers + int QueryFocus(); + int QueryNosepiece(); + int QueryMagnification(); + int QueryLightPath(); + int QueryCondenserTurret(); + int QueryDIAAperture(); + int QueryDIAShutter(); + int QueryPolarizer(); + int QueryDICPrism(); + int QueryDICRetardation(); + int QueryEPIShutter1(); + int QueryEPIShutter2(); + int QueryMirrorUnit1(); + int QueryMirrorUnit2(); + int QueryEPIND(); + int QueryRightPort(); + int QueryCorrectionCollar(); + int QueryManualControl(); + + // Manual Control Unit (MCU) helpers + int UpdateNosepieceIndicator(int position); + + // Monitoring thread + void StartMonitoring(); + void StopMonitoring(); + void MonitorThreadFunc(); + void ProcessNotification(const std::string& message); + bool IsNotificationTag(const std::string& message) const; + + // Member variables + bool initialized_; + std::string port_; + long answerTimeoutMs_; + EvidentIX85::MicroscopeModel model_; + + // Threading + std::thread monitorThread_; + std::atomic stopMonitoring_; + mutable std::mutex commandMutex_; // Protects command sending only + + // Response handling (monitoring thread passes responses to command thread) + std::mutex responseMutex_; + std::condition_variable responseCV_; + std::string pendingResponse_; + bool responseReady_; + + // State + std::string version_; + std::string unit_; + std::vector availableDevices_; + std::vector detectedDevicesByName_; + + // MCU switch state + int rememberedDIABrightness_; // Remembered brightness for DIA switch toggle + + // Child devices + std::map usedDevices_; +}; diff --git a/DeviceAdapters/EvidentIX85/EvidentIX85.cpp b/DeviceAdapters/EvidentIX85/EvidentIX85.cpp new file mode 100644 index 000000000..b7208c458 --- /dev/null +++ b/DeviceAdapters/EvidentIX85/EvidentIX85.cpp @@ -0,0 +1,3199 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentIX85.cpp +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 microscope device implementations +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#include "EvidentIX85.h" +#include "ModuleInterface.h" +#include + +using namespace EvidentIX85; + +// External hub name +extern const char* g_HubDeviceName; + +// Device names +const char* g_FocusDeviceName = "IX85-Focus"; +const char* g_NosepieceDeviceName = "IX85-Nosepiece"; +const char* g_MagnificationDeviceName = "IX85-Magnification"; +const char* g_LightPathDeviceName = "IX85-LightPath"; +const char* g_CondenserTurretDeviceName = "IX85-CondenserTurret"; +const char* g_DIAShutterDeviceName = "IX85-DIAShutter"; +const char* g_EPIShutter1DeviceName = "IX85-EPIShutter1"; +const char* g_EPIShutter2DeviceName = "IX85-EPIShutter2"; +const char* g_MirrorUnit1DeviceName = "IX85-MirrorUnit1"; +const char* g_MirrorUnit2DeviceName = "IX85-MirrorUnit2"; +const char* g_PolarizerDeviceName = "IX85-Polarizer"; +const char* g_DICPrismDeviceName = "IX85-DICPrism"; +const char* g_EPINDDeviceName = "IX85-EPIND"; +const char* g_CorrectionCollarDeviceName = "IX85-CorrectionCollar"; + +// Property Names +const char* g_Keyword_Magnification = "Magnification"; + +/////////////////////////////////////////////////////////////////////////////// +// MODULE_API - Exported MMDevice interface +/////////////////////////////////////////////////////////////////////////////// + +MODULE_API void InitializeModuleData() +{ + RegisterDevice(g_HubDeviceName, MM::HubDevice, "Evident IX85 Hub"); + RegisterDevice(g_FocusDeviceName, MM::StageDevice, "Evident IX85 Focus Drive"); + RegisterDevice(g_NosepieceDeviceName, MM::StateDevice, "Evident IX85 Nosepiece"); + RegisterDevice(g_MagnificationDeviceName, MM::MagnifierDevice, "Evident IX85 Magnification Changer"); + RegisterDevice(g_LightPathDeviceName, MM::StateDevice, "Evident IX85 Light Path"); + RegisterDevice(g_CondenserTurretDeviceName, MM::StateDevice, "Evident IX85 Condenser Turret"); + RegisterDevice(g_DIAShutterDeviceName, MM::ShutterDevice, "Evident IX85 DIA Shutter"); + RegisterDevice(g_EPIShutter1DeviceName, MM::ShutterDevice, "Evident IX85 EPI Shutter 1"); + RegisterDevice(g_EPIShutter2DeviceName, MM::ShutterDevice, "Evident IX85 EPI Shutter 2"); + RegisterDevice(g_MirrorUnit1DeviceName, MM::StateDevice, "Evident IX85 Mirror Unit 1"); + RegisterDevice(g_MirrorUnit2DeviceName, MM::StateDevice, "Evident IX85 Mirror Unit 2"); + RegisterDevice(g_PolarizerDeviceName, MM::StateDevice, "Evident IX85 Polarizer"); + RegisterDevice(g_DICPrismDeviceName, MM::StateDevice, "Evident IX85 DIC Prism"); + RegisterDevice(g_EPINDDeviceName, MM::StateDevice, "Evident IX85 EPI ND Filter"); + RegisterDevice(g_CorrectionCollarDeviceName, MM::StageDevice, "Evident IX85 Correction Collar"); +} + +MODULE_API MM::Device* CreateDevice(const char* deviceName) +{ + if (deviceName == nullptr) + return nullptr; + + if (strcmp(deviceName, g_HubDeviceName) == 0) + return new EvidentHub(); + else if (strcmp(deviceName, g_FocusDeviceName) == 0) + return new EvidentFocus(); + else if (strcmp(deviceName, g_NosepieceDeviceName) == 0) + return new EvidentNosepiece(); + else if (strcmp(deviceName, g_MagnificationDeviceName) == 0) + return new EvidentMagnification(); + else if (strcmp(deviceName, g_LightPathDeviceName) == 0) + return new EvidentLightPath(); + else if (strcmp(deviceName, g_CondenserTurretDeviceName) == 0) + return new EvidentCondenserTurret(); + else if (strcmp(deviceName, g_DIAShutterDeviceName) == 0) + return new EvidentDIAShutter(); + else if (strcmp(deviceName, g_EPIShutter1DeviceName) == 0) + return new EvidentEPIShutter1(); + else if (strcmp(deviceName, g_EPIShutter2DeviceName) == 0) + return new EvidentEPIShutter2(); + else if (strcmp(deviceName, g_MirrorUnit1DeviceName) == 0) + return new EvidentMirrorUnit1(); + else if (strcmp(deviceName, g_MirrorUnit2DeviceName) == 0) + return new EvidentMirrorUnit2(); + else if (strcmp(deviceName, g_PolarizerDeviceName) == 0) + return new EvidentPolarizer(); + else if (strcmp(deviceName, g_DICPrismDeviceName) == 0) + return new EvidentDICPrism(); + else if (strcmp(deviceName, g_EPINDDeviceName) == 0) + return new EvidentEPIND(); + else if (strcmp(deviceName, g_CorrectionCollarDeviceName) == 0) + return new EvidentCorrectionCollar(); + + return nullptr; +} + +MODULE_API void DeleteDevice(MM::Device* pDevice) +{ + delete pDevice; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentFocus - Focus Drive Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentFocus::EvidentFocus() : + initialized_(false), + name_(g_FocusDeviceName), + stepSizeUm_(FOCUS_STEP_SIZE_UM) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Focus drive not available on this microscope"); + + // Parent ID for hub + CreateHubIDProperty(); +} + +EvidentFocus::~EvidentFocus() +{ + Shutdown(); +} + +void EvidentFocus::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentFocus::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_Focus)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentFocus::OnPosition); + int ret = CreateProperty(MM::g_Keyword_Position, "0.0", MM::Float, false, pAct); + if (ret != DEVICE_OK) + return ret; + + pAct = new CPropertyAction(this, &EvidentFocus::OnSpeed); + ret = CreateProperty("Speed (um/s)", "30.0", MM::Float, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Jog Direction property + pAct = new CPropertyAction(this, &EvidentFocus::OnJogDirection); + ret = CreateProperty("Jog Direction", "Default", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + std::vector directionValues; + directionValues.push_back("Default"); + directionValues.push_back("Reverse"); + ret = SetAllowedValues("Jog Direction", directionValues); + if (ret != DEVICE_OK) + return ret; + + // Jog Fine Sensitivity property + pAct = new CPropertyAction(this, &EvidentFocus::OnJogFineSensitivity); + ret = CreateProperty("Jog Fine Sensitivity", "10", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + ret = SetPropertyLimits("Jog Fine Sensitivity", 1, 100); + if (ret != DEVICE_OK) + return ret; + + // Jog Coarse Sensitivity property + pAct = new CPropertyAction(this, &EvidentFocus::OnJogCoarseSensitivity); + ret = CreateProperty("Jog Coarse Sensitivity", "10", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + ret = SetPropertyLimits("Jog Coarse Sensitivity", 1, 100); + if (ret != DEVICE_OK) + return ret; + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_Focus); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Enable active notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + return ret; + + // Register with hub so notification handler can call OnStagePositionChanged + hub->RegisterDeviceAsUsed(DeviceType_Focus, this); + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentFocus::Shutdown() +{ + if (initialized_) + { + EnableNotifications(false); + + // Unregister from hub + EvidentHub* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_Focus); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentFocus::Busy() +{ + EvidentHub* hub = GetHub(); + if (!hub) + return false; + + return hub->GetModel()->IsBusy(DeviceType_Focus); +} + +int EvidentFocus::SetPositionUm(double pos) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert μm to 10nm units + long steps = static_cast(pos / stepSizeUm_); + + // Clamp to limits + if (steps < FOCUS_MIN_POS) steps = FOCUS_MIN_POS; + if (steps > FOCUS_MAX_POS) steps = FOCUS_MAX_POS; + + // Check if we're already at the target position + long currentPos = hub->GetModel()->GetPosition(DeviceType_Focus); + bool alreadyAtTarget = IsAtTargetPosition(currentPos, steps, FOCUS_POSITION_TOLERANCE); + + // Set target position BEFORE sending command so notifications can check against it + hub->GetModel()->SetTargetPosition(DeviceType_Focus, steps); + hub->GetModel()->SetBusy(DeviceType_Focus, true); + + std::string cmd = BuildCommand(CMD_FOCUS_GOTO, static_cast(steps)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + // Command failed - clear busy state + hub->GetModel()->SetBusy(DeviceType_Focus, false); + return ret; + } + + if (!IsPositiveAck(response, CMD_FOCUS_GOTO)) + { + // Command rejected - clear busy state + hub->GetModel()->SetBusy(DeviceType_Focus, false); + return ERR_NEGATIVE_ACK; + } + + // If we're already at the target, firmware won't send notifications, so clear busy immediately + if (alreadyAtTarget) + { + hub->GetModel()->SetBusy(DeviceType_Focus, false); + } + + // Command accepted - if not already at target, busy will be cleared by notification when target reached + return DEVICE_OK; +} + +int EvidentFocus::GetPositionUm(double& pos) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + long steps = hub->GetModel()->GetPosition(DeviceType_Focus); + if (steps < 0) // Unknown + return ERR_POSITION_UNKNOWN; + + pos = steps * stepSizeUm_; + return DEVICE_OK; +} + +int EvidentFocus::SetPositionSteps(long steps) +{ + return SetPositionUm(steps * stepSizeUm_); +} + +int EvidentFocus::GetPositionSteps(long& steps) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + steps = hub->GetModel()->GetPosition(DeviceType_Focus); + if (steps < 0) + return ERR_POSITION_UNKNOWN; + + return DEVICE_OK; +} + +int EvidentFocus::SetOrigin() +{ + // Not supported by IX85 + return DEVICE_UNSUPPORTED_COMMAND; +} + +int EvidentFocus::GetLimits(double& lower, double& upper) +{ + lower = FOCUS_MIN_POS * stepSizeUm_; + upper = FOCUS_MAX_POS * stepSizeUm_; + return DEVICE_OK; +} + +int EvidentFocus::OnPosition(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + double pos; + int ret = GetPositionUm(pos); + if (ret != DEVICE_OK) + return ret; + pProp->Set(pos); + } + else if (eAct == MM::AfterSet) + { + double pos; + pProp->Get(pos); + int ret = SetPositionUm(pos); + if (ret != DEVICE_OK) + return ret; + } + return DEVICE_OK; +} + +int EvidentFocus::OnSpeed(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + // Query current speed + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string cmd = BuildQuery(CMD_FOCUS_SPEED); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() >= 2) + { + long highSpeed = ParseLongParameter(params[1]); // High speed in 10nm/s + double speedUmPerSec = (highSpeed * stepSizeUm_); + pProp->Set(speedUmPerSec); + } + } + else if (eAct == MM::AfterSet) + { + double speedUmPerSec; + pProp->Get(speedUmPerSec); + + // Convert to 10nm/s + long highSpeed = static_cast(speedUmPerSec / stepSizeUm_); + long initialSpeed = highSpeed / 10; // Default: 10% of high speed + long acceleration = highSpeed * 5; // Default acceleration + + std::string cmd = BuildCommand(CMD_FOCUS_SPEED, + static_cast(initialSpeed), + static_cast(highSpeed), + static_cast(acceleration)); + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_FOCUS_SPEED)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +int EvidentFocus::OnJogDirection(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + // Query current direction + std::string cmd = BuildQuery(CMD_JOG_DIRECTION); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() >= 1) + { + int direction = ParseIntParameter(params[0]); + if (direction == 0) + pProp->Set("Reverse"); + else if (direction == 1) + pProp->Set("Default"); + } + } + else if (eAct == MM::AfterSet) + { + std::string directionStr; + pProp->Get(directionStr); + + int direction = (directionStr == "Reverse") ? 0 : 1; + + std::string cmd = BuildCommand(CMD_JOG_DIRECTION, direction); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_JOG_DIRECTION)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +int EvidentFocus::OnJogFineSensitivity(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + // Query current fine sensitivity + std::string cmd = BuildQuery(CMD_JOG_SENSITIVITY_FINE); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() >= 1) + { + long sensitivity = ParseLongParameter(params[0]); + if (sensitivity >= 1 && sensitivity <= 100) + pProp->Set(sensitivity); + } + } + else if (eAct == MM::AfterSet) + { + long sensitivity; + pProp->Get(sensitivity); + + // Clamp to valid range + if (sensitivity < 1) sensitivity = 1; + if (sensitivity > 100) sensitivity = 100; + + std::string cmd = BuildCommand(CMD_JOG_SENSITIVITY_FINE, static_cast(sensitivity)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_JOG_SENSITIVITY_FINE)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +int EvidentFocus::OnJogCoarseSensitivity(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + // Query current coarse sensitivity + std::string cmd = BuildQuery(CMD_JOG_SENSITIVITY_COARSE); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() >= 1) + { + long sensitivity = ParseLongParameter(params[0]); + if (sensitivity >= 1 && sensitivity <= 100) + pProp->Set(sensitivity); + } + } + else if (eAct == MM::AfterSet) + { + long sensitivity; + pProp->Get(sensitivity); + + // Clamp to valid range + if (sensitivity < 1) sensitivity = 1; + if (sensitivity > 100) sensitivity = 100; + + std::string cmd = BuildCommand(CMD_JOG_SENSITIVITY_COARSE, static_cast(sensitivity)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_JOG_SENSITIVITY_COARSE)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +EvidentHub* EvidentFocus::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentFocus::EnableNotifications(bool enable) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + return hub->EnableNotification(CMD_FOCUS_NOTIFY, enable); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentNosepiece - Nosepiece Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentNosepiece::EvidentNosepiece() : + initialized_(false), + name_(g_NosepieceDeviceName), + numPos_(NOSEPIECE_MAX_POS), + safeNosepieceChange_(true) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Nosepiece not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentNosepiece::~EvidentNosepiece() +{ + Shutdown(); +} + +void EvidentNosepiece::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentNosepiece::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_Nosepiece)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_Nosepiece); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentNosepiece::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream label; + label << "Position-" << (i + 1); + SetPositionLabel(i, label.str().c_str()); + } + + // Create SafeNosepieceChange property + pAct = new CPropertyAction(this, &EvidentNosepiece::OnSafeChange); + ret = CreateProperty("SafeNosepieceChange", "Enabled", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue("SafeNosepieceChange", "Disabled"); + AddAllowedValue("SafeNosepieceChange", "Enabled"); + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_Nosepiece); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Enable notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + return ret; + + // Register with hub so notification handler can notify property changes + hub->RegisterDeviceAsUsed(DeviceType_Nosepiece, this); + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentNosepiece::Shutdown() +{ + if (initialized_) + { + EnableNotifications(false); + + // Unregister from hub + EvidentHub* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_Nosepiece); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentNosepiece::Busy() +{ + EvidentHub* hub = GetHub(); + if (!hub) + return false; + + return hub->GetModel()->IsBusy(DeviceType_Nosepiece); +} + +unsigned long EvidentNosepiece::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentNosepiece::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + long pos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + if (pos < 0) + return ERR_POSITION_UNKNOWN; + + // Convert from 1-based to 0-based + pProp->Set(pos - 1); + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Use safe nosepiece change if enabled + if (safeNosepieceChange_) + { + return SafeNosepieceChange(pos + 1); // Convert 0-based to 1-based + } + + // Direct nosepiece change (original behavior) + // Set target position BEFORE sending command so notifications can check against it + // Convert from 0-based to 1-based for the microscope + long targetPos = pos + 1; + hub->GetModel()->SetTargetPosition(DeviceType_Nosepiece, targetPos); + + // Check if already at target position + long currentPos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + if (currentPos == targetPos) + { + // Already at target, no need to move + hub->GetModel()->SetBusy(DeviceType_Nosepiece, false); + return DEVICE_OK; + } + + hub->GetModel()->SetBusy(DeviceType_Nosepiece, true); + + std::string cmd = BuildCommand(CMD_NOSEPIECE, static_cast(targetPos)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + // Command failed - clear busy state + hub->GetModel()->SetBusy(DeviceType_Nosepiece, false); + return ret; + } + + if (!IsPositiveAck(response, CMD_NOSEPIECE)) + { + // Command rejected - clear busy state + hub->GetModel()->SetBusy(DeviceType_Nosepiece, false); + return ERR_NEGATIVE_ACK; + } + + // Command accepted - busy state already set, will be cleared by notification when target reached + } + return DEVICE_OK; +} + +EvidentHub* EvidentNosepiece::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentNosepiece::EnableNotifications(bool enable) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + return hub->EnableNotification(CMD_NOSEPIECE_NOTIFY, enable); +} + +int EvidentNosepiece::OnSafeChange(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(safeNosepieceChange_ ? "Enabled" : "Disabled"); + } + else if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + safeNosepieceChange_ = (value == "Enabled"); + } + return DEVICE_OK; +} + +int EvidentNosepiece::SafeNosepieceChange(long targetPosition) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Check if Focus device is available + if (!hub->IsDevicePresent(DeviceType_Focus)) + { + // No focus device - just do a regular nosepiece change + LogMessage("Focus device not available, skipping safe nosepiece change"); + hub->GetModel()->SetTargetPosition(DeviceType_Nosepiece, targetPosition); + + // Check if already at target position + long currentPos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + if (currentPos == targetPosition) + { + // Already at target, no need to move + hub->GetModel()->SetBusy(DeviceType_Nosepiece, false); + return DEVICE_OK; + } + + hub->GetModel()->SetBusy(DeviceType_Nosepiece, true); + + std::string cmd = BuildCommand(CMD_NOSEPIECE, static_cast(targetPosition)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + hub->GetModel()->SetBusy(DeviceType_Nosepiece, false); + return ret; + } + if (!IsPositiveAck(response, CMD_NOSEPIECE)) + { + hub->GetModel()->SetBusy(DeviceType_Nosepiece, false); + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; + } + + // Get current focus position + long originalFocusPos = hub->GetModel()->GetPosition(DeviceType_Focus); + if (originalFocusPos < 0) + { + LogMessage("Focus position unknown, cannot perform safe nosepiece change"); + return ERR_POSITION_UNKNOWN; + } + + // Timeout settings for wait loops + const int maxWaitIterations = 100; // 10 seconds max + + LogMessage("Safe nosepiece change: Moving focus to zero"); + + // Check if focus is already at zero + bool alreadyAtZero = IsAtTargetPosition(originalFocusPos, 0, FOCUS_POSITION_TOLERANCE); + + if (!alreadyAtZero) + { + // Move focus to zero + hub->GetModel()->SetTargetPosition(DeviceType_Focus, 0); + hub->GetModel()->SetBusy(DeviceType_Focus, true); + + std::string cmd = BuildCommand(CMD_FOCUS_GOTO, 0); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + hub->GetModel()->SetBusy(DeviceType_Focus, false); + return ret; + } + if (!IsPositiveAck(response, CMD_FOCUS_GOTO)) + { + hub->GetModel()->SetBusy(DeviceType_Focus, false); + return ERR_NEGATIVE_ACK; + } + + // Wait for focus to reach zero (with timeout) + int focusWaitCount = 0; + while (hub->GetModel()->IsBusy(DeviceType_Focus) && focusWaitCount < maxWaitIterations) + { + CDeviceUtils::SleepMs(100); + focusWaitCount++; + } + + if (focusWaitCount >= maxWaitIterations) + { + LogMessage("Timeout waiting for focus to reach zero"); + return ERR_COMMAND_TIMEOUT; + } + } + else + { + LogMessage("Focus already at zero, skipping focus move"); + hub->GetModel()->SetBusy(DeviceType_Focus, false); + } + + LogMessage("Safe nosepiece change: Changing nosepiece position"); + + // Change nosepiece position + hub->GetModel()->SetTargetPosition(DeviceType_Nosepiece, targetPosition); + + std::ostringstream msg; + msg << "Set nosepiece target position to: " << targetPosition; + LogMessage(msg.str().c_str()); + + // Check if already at target position + long currentNosepiecePos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + bool alreadyAtTargetNosepiece = (currentNosepiecePos == targetPosition); + + std::ostringstream msg2; + msg2 << "Current nosepiece position: " << currentNosepiecePos << ", target: " << targetPosition << ", alreadyAtTarget: " << alreadyAtTargetNosepiece; + LogMessage(msg2.str().c_str()); + + if (!alreadyAtTargetNosepiece) + { + hub->GetModel()->SetBusy(DeviceType_Nosepiece, true); + + std::string cmd = BuildCommand(CMD_NOSEPIECE, static_cast(targetPosition)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + hub->GetModel()->SetBusy(DeviceType_Nosepiece, false); + // Try to restore focus position even if nosepiece change failed + hub->GetModel()->SetTargetPosition(DeviceType_Focus, originalFocusPos); + hub->GetModel()->SetBusy(DeviceType_Focus, true); + std::string focusCmd = BuildCommand(CMD_FOCUS_GOTO, static_cast(originalFocusPos)); + std::string focusResponse; + hub->ExecuteCommand(focusCmd, focusResponse); + return ret; + } + + if (!IsPositiveAck(response, CMD_NOSEPIECE)) + { + hub->GetModel()->SetBusy(DeviceType_Nosepiece, false); + // Try to restore focus position even if nosepiece change failed + hub->GetModel()->SetTargetPosition(DeviceType_Focus, originalFocusPos); + hub->GetModel()->SetBusy(DeviceType_Focus, true); + std::string focusCmd = BuildCommand(CMD_FOCUS_GOTO, static_cast(originalFocusPos)); + std::string focusResponse; + hub->ExecuteCommand(focusCmd, focusResponse); + return ERR_NEGATIVE_ACK; + } + } + else + { + // Already at target, no need to move + hub->GetModel()->SetBusy(DeviceType_Nosepiece, false); + } + + // Wait for nosepiece to complete (with timeout) + int nosepieceWaitCount = 0; + while (hub->GetModel()->IsBusy(DeviceType_Nosepiece) && nosepieceWaitCount < maxWaitIterations) + { + CDeviceUtils::SleepMs(100); + nosepieceWaitCount++; + } + + if (nosepieceWaitCount >= maxWaitIterations) + { + LogMessage("Timeout waiting for nosepiece to complete"); + return ERR_COMMAND_TIMEOUT; + } + + LogMessage("Safe nosepiece change: Restoring focus position"); + + // Check if we're already at the target position (originalFocusPos) + long currentFocusPos = hub->GetModel()->GetPosition(DeviceType_Focus); + bool alreadyAtTarget = IsAtTargetPosition(currentFocusPos, originalFocusPos, FOCUS_POSITION_TOLERANCE); + + if (!alreadyAtTarget) + { + // Restore original focus position + hub->GetModel()->SetTargetPosition(DeviceType_Focus, originalFocusPos); + hub->GetModel()->SetBusy(DeviceType_Focus, true); + + std::string cmd = BuildCommand(CMD_FOCUS_GOTO, static_cast(originalFocusPos)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + hub->GetModel()->SetBusy(DeviceType_Focus, false); + return ret; + } + + if (!IsPositiveAck(response, CMD_FOCUS_GOTO)) + { + hub->GetModel()->SetBusy(DeviceType_Focus, false); + return ERR_NEGATIVE_ACK; + } + + // Busy will be cleared by notification when target reached + } + else + { + LogMessage("Focus already at target position, skipping focus restore"); + hub->GetModel()->SetBusy(DeviceType_Focus, false); + } + + LogMessage("Safe nosepiece change completed successfully"); + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentMagnification - Magnification Changer Implementation +/////////////////////////////////////////////////////////////////////////////// + +// Static magnification values +const double EvidentMagnification::magnifications_[3] = {1.0, 1.6, 2.0}; + +EvidentMagnification::EvidentMagnification() : + initialized_(false), + name_(g_MagnificationDeviceName), + numPos_(MAGNIFICATION_MAX_POS) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Magnification changer not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentMagnification::~EvidentMagnification() +{ + Shutdown(); +} + +void EvidentMagnification::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentMagnification::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_Magnification)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_Magnification); + + // Create magnification property (read-only) + CPropertyAction* pAct = new CPropertyAction(this, &EvidentMagnification::OnMagnification); + int ret = CreateProperty(g_Keyword_Magnification, "1.0", MM::Float, true, pAct); + if (ret != DEVICE_OK) + return ret; + + // Set allowed values + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream os; + os << magnifications_[i]; + AddAllowedValue(g_Keyword_Magnification, os.str().c_str()); + } + + hub->RegisterDeviceAsUsed(DeviceType_Magnification, this); + + // Enable notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + return ret; + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentMagnification::Shutdown() +{ + if (initialized_) + { + EnableNotifications(false); + GetHub()->UnRegisterDeviceAsUsed(DeviceType_Magnification); + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentMagnification::Busy() +{ + EvidentHub* hub = GetHub(); + if (!hub) + return false; + + return hub->GetModel()->IsBusy(DeviceType_Magnification); +} + +double EvidentMagnification::GetMagnification() +{ + EvidentHub* hub = GetHub(); + if (!hub) + return 1.0; + + long pos = hub->GetModel()->GetPosition(DeviceType_Magnification); + if (pos < 1 || pos > static_cast(numPos_)) + return 1.0; + + // pos is 1-based, array is 0-based + return magnifications_[pos - 1]; +} + +int EvidentMagnification::OnMagnification(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + double mag = GetMagnification(); + pProp->Set(mag); + } + else if (eAct == MM::AfterSet) + { + // Read-only - nothing to do + } + return DEVICE_OK; +} + +EvidentHub* EvidentMagnification::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentMagnification::EnableNotifications(bool enable) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + return hub->EnableNotification(CMD_MAGNIFICATION_NOTIFY, enable); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentLightPath - Light Path Selector Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentLightPath::EvidentLightPath() : + initialized_(false), + name_(g_LightPathDeviceName) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Light path selector not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentLightPath::~EvidentLightPath() +{ + Shutdown(); +} + +void EvidentLightPath::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentLightPath::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_LightPath)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentLightPath::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, 3); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels for light path positions + SetPositionLabel(0, "Left Port"); // LIGHT_PATH_LEFT_PORT = 1 + SetPositionLabel(1, "Binocular 50/50"); // LIGHT_PATH_BI_50_50 = 2 + SetPositionLabel(2, "Binocular 100%"); // LIGHT_PATH_BI_100 = 3 + SetPositionLabel(3, "Right Port"); // LIGHT_PATH_RIGHT_PORT = 4 + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_LightPath); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Register with hub so OnState can notify indicator changes + hub->RegisterDeviceAsUsed(DeviceType_LightPath, this); + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentLightPath::Shutdown() +{ + if (initialized_) + { + // Unregister from hub + EvidentHub* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_LightPath); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentLightPath::Busy() +{ + return false; // Light path changes are instantaneous +} + +unsigned long EvidentLightPath::GetNumberOfPositions() const +{ + return 4; +} + +int EvidentLightPath::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query current position + std::string cmd = BuildQuery(CMD_LIGHT_PATH); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 1 && pos <= 4) + { + // Convert from 1-based to 0-based + pProp->Set(static_cast(pos - 1)); + } + } + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + std::string cmd = BuildCommand(CMD_LIGHT_PATH, static_cast(pos + 1)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_LIGHT_PATH)) + return ERR_NEGATIVE_ACK; + + // Update MCU indicator I4 with new light path position (1-based) + hub->UpdateLightPathIndicator(static_cast(pos + 1)); + } + return DEVICE_OK; +} + +EvidentHub* EvidentLightPath::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentCondenserTurret - Condenser Turret Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentCondenserTurret::EvidentCondenserTurret() : + initialized_(false), + name_(g_CondenserTurretDeviceName), + numPos_(6) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Condenser turret not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentCondenserTurret::~EvidentCondenserTurret() +{ + Shutdown(); +} + +void EvidentCondenserTurret::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentCondenserTurret::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_CondenserTurret)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_CondenserTurret); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentCondenserTurret::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream label; + label << "Position-" << (i + 1); + SetPositionLabel(i, label.str().c_str()); + } + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_CondenserTurret); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Enable notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + return ret; + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentCondenserTurret::Shutdown() +{ + if (initialized_) + { + EnableNotifications(false); + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentCondenserTurret::Busy() +{ + EvidentHub* hub = GetHub(); + if (!hub) + return false; + + return hub->GetModel()->IsBusy(DeviceType_CondenserTurret); +} + +unsigned long EvidentCondenserTurret::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentCondenserTurret::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + long pos = hub->GetModel()->GetPosition(DeviceType_CondenserTurret); + if (pos < 0) + return ERR_POSITION_UNKNOWN; + + // Convert from 1-based to 0-based + pProp->Set(pos - 1); + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + long targetPos = pos + 1; + + // Check if already at target position + long currentPos = hub->GetModel()->GetPosition(DeviceType_CondenserTurret); + if (currentPos == targetPos) + { + // Already at target, no need to move + hub->GetModel()->SetBusy(DeviceType_CondenserTurret, false); + return DEVICE_OK; + } + + // Set busy before sending command + hub->GetModel()->SetBusy(DeviceType_CondenserTurret, true); + + std::string cmd = BuildCommand(CMD_CONDENSER_TURRET, static_cast(targetPos)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + hub->GetModel()->SetBusy(DeviceType_CondenserTurret, false); + return ret; + } + + if (!IsPositiveAck(response, CMD_CONDENSER_TURRET)) + { + hub->GetModel()->SetBusy(DeviceType_CondenserTurret, false); + return ERR_NEGATIVE_ACK; + } + + // CondenserTurret does not send notifications (NTR) when movement completes. + // The positive ack ("TR +") is only returned after movement completes, + // so we can clear busy immediately and update position. + hub->GetModel()->SetPosition(DeviceType_CondenserTurret, targetPos); + hub->GetModel()->SetBusy(DeviceType_CondenserTurret, false); + } + return DEVICE_OK; +} + +EvidentHub* EvidentCondenserTurret::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentCondenserTurret::EnableNotifications(bool enable) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Use TR (command tag) not NTR (notification tag) to enable notifications + return hub->EnableNotification(CMD_CONDENSER_TURRET, enable); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentDIAShutter - DIA (Transmitted Light) Shutter Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentDIAShutter::EvidentDIAShutter() : + initialized_(false), + name_(g_DIAShutterDeviceName) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "DIA shutter not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentDIAShutter::~EvidentDIAShutter() +{ + Shutdown(); +} + +void EvidentDIAShutter::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentDIAShutter::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_DIAShutter)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Create state property + CPropertyAction* pAct = new CPropertyAction(this, &EvidentDIAShutter::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + AddAllowedValue(MM::g_Keyword_State, "0"); // Closed + AddAllowedValue(MM::g_Keyword_State, "1"); // Open + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_DIAShutter); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Create Brightness property (DIA illumination intensity) + pAct = new CPropertyAction(this, &EvidentDIAShutter::OnBrightness); + ret = CreateProperty("Brightness", "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits("Brightness", 0, 255); + + // Query current brightness value + std::string cmd = BuildQuery(CMD_DIA_ILLUMINATION); + std::string response; + ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int brightness = ParseIntParameter(params[0]); + if (brightness >= 0 && brightness <= 255) + { + hub->GetModel()->SetPosition(DeviceType_DIABrightness, brightness); + SetProperty("Brightness", CDeviceUtils::ConvertToString(brightness)); + + // Update I3 indicator on MCU + hub->UpdateDIABrightnessIndicator(brightness); + } + } + } + + // Create Mechanical Shutter property (controls physical shutter, independent of logical shutter) + pAct = new CPropertyAction(this, &EvidentDIAShutter::OnMechanicalShutter); + ret = CreateProperty("Mechanical Shutter", "Closed", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + AddAllowedValue("Mechanical Shutter", "Closed"); + AddAllowedValue("Mechanical Shutter", "Open"); + + // Query current mechanical shutter state + cmd = BuildQuery(CMD_DIA_SHUTTER); + ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int state = ParseIntParameter(params[0]); + // Note: DSH 0 = Open, DSH 1 = Closed (reversed) + SetProperty("Mechanical Shutter", (state == 0) ? "Open" : "Closed"); + } + } + + // Enable brightness change notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + return ret; + + // Register with hub so notification handler can call OnPropertyChanged + hub->RegisterDeviceAsUsed(DeviceType_DIAShutter, this); + + initialized_ = true; + + // Close logical shutter on startup (set brightness to 0) + SetOpen(false); + + return DEVICE_OK; +} + +int EvidentDIAShutter::Shutdown() +{ + if (initialized_) + { + // Disable brightness change notifications + EnableNotifications(false); + + // Unregister from hub + EvidentHub* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_DIAShutter); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentDIAShutter::Busy() +{ + return false; // Shutter changes are instantaneous +} + +int EvidentDIAShutter::SetOpen(bool open) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (open) + { + // Logical open: Set brightness to remembered value + int rememberedBrightness = hub->GetRememberedDIABrightness(); + std::string cmd = BuildCommand(CMD_DIA_ILLUMINATION, rememberedBrightness); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_DIA_ILLUMINATION)) + return ERR_NEGATIVE_ACK; + + // Update model + hub->GetModel()->SetPosition(DeviceType_DIABrightness, rememberedBrightness); + + // Update I3 indicator on MCU + hub->UpdateDIABrightnessIndicator(rememberedBrightness); + } + else + { + // Logical close: Remember current brightness, then set to 0 + std::string cmd = BuildQuery(CMD_DIA_ILLUMINATION); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int brightness = ParseIntParameter(params[0]); + if (brightness > 0) + hub->SetRememberedDIABrightness(brightness); + } + + // Set brightness to 0 + cmd = BuildCommand(CMD_DIA_ILLUMINATION, 0); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_DIA_ILLUMINATION)) + return ERR_NEGATIVE_ACK; + + // Update model + hub->GetModel()->SetPosition(DeviceType_DIABrightness, 0); + + // Update I3 indicator on MCU with remembered brightness (not 0) + // User wants to see the remembered brightness value, not that lamp is off + hub->UpdateDIABrightnessIndicator(hub->GetRememberedDIABrightness()); + } + + return DEVICE_OK; +} + +int EvidentDIAShutter::GetOpen(bool& open) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Logical shutter state is based on brightness: open if brightness > 0 + std::string cmd = BuildQuery(CMD_DIA_ILLUMINATION); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int brightness = ParseIntParameter(params[0]); + open = (brightness > 0); + + // Update model + hub->GetModel()->SetPosition(DeviceType_DIABrightness, brightness); + } + + return DEVICE_OK; +} + +int EvidentDIAShutter::Fire(double /*deltaT*/) +{ + // Not implemented for this shutter + return DEVICE_UNSUPPORTED_COMMAND; +} + +int EvidentDIAShutter::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + bool open; + int ret = GetOpen(open); + if (ret != DEVICE_OK) + return ret; + pProp->Set(open ? 1L : 0L); + } + else if (eAct == MM::AfterSet) + { + long state; + pProp->Get(state); + return SetOpen(state != 0); + } + return DEVICE_OK; +} + +int EvidentDIAShutter::OnBrightness(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + // Return remembered brightness (what brightness will be when shutter opens) + int rememberedBrightness = hub->GetRememberedDIABrightness(); + pProp->Set(static_cast(rememberedBrightness)); + } + else if (eAct == MM::AfterSet) + { + long brightness; + pProp->Get(brightness); + + // Always update remembered brightness + hub->SetRememberedDIABrightness(static_cast(brightness)); + + // Always update I3 indicator to match Brightness property + hub->UpdateDIABrightnessIndicator(static_cast(brightness)); + + // Only send DIL command if logical shutter is open (actual brightness > 0) + long currentBrightness = hub->GetModel()->GetPosition(DeviceType_DIABrightness); + if (currentBrightness > 0) + { + // Shutter is open: update actual lamp brightness + std::string cmd = BuildCommand(CMD_DIA_ILLUMINATION, static_cast(brightness)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_DIA_ILLUMINATION)) + return ERR_NEGATIVE_ACK; + + // Update model + hub->GetModel()->SetPosition(DeviceType_DIABrightness, brightness); + } + // If shutter is closed, don't send DIL command, don't update model + } + + return DEVICE_OK; +} + +int EvidentDIAShutter::OnMechanicalShutter(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + // Query physical shutter state + std::string cmd = BuildQuery(CMD_DIA_SHUTTER); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int state = ParseIntParameter(params[0]); + // Note: DSH 0 = Open, DSH 1 = Closed (reversed) + pProp->Set((state == 0) ? "Open" : "Closed"); + } + } + else if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + + // Convert "Open"/"Closed" to 0/1 (reversed: Open=0, Closed=1) + int state = (value == "Open") ? 0 : 1; + + // Send DSH command to control physical shutter + std::string cmd = BuildCommand(CMD_DIA_SHUTTER, state); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_DIA_SHUTTER)) + return ERR_NEGATIVE_ACK; + } + + return DEVICE_OK; +} + +int EvidentDIAShutter::EnableNotifications(bool enable) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + return hub->EnableNotification(CMD_DIA_ILLUMINATION_NOTIFY, enable); +} + +EvidentHub* EvidentDIAShutter::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentEPIShutter1 - EPI (Reflected Light) Shutter 1 Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentEPIShutter1::EvidentEPIShutter1() : + initialized_(false), + name_(g_EPIShutter1DeviceName) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "EPI shutter 1 not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentEPIShutter1::~EvidentEPIShutter1() +{ + Shutdown(); +} + +void EvidentEPIShutter1::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentEPIShutter1::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_EPIShutter1)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Create state property + CPropertyAction* pAct = new CPropertyAction(this, &EvidentEPIShutter1::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + AddAllowedValue(MM::g_Keyword_State, "0"); // Closed + AddAllowedValue(MM::g_Keyword_State, "1"); // Open + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_EPIShutter1); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Register with hub so SetOpen can notify indicator changes + hub->RegisterDeviceAsUsed(DeviceType_EPIShutter1, this); + + initialized_ = true; + + // Close shutter on startup + SetOpen(false); + + return DEVICE_OK; +} + +int EvidentEPIShutter1::Shutdown() +{ + if (initialized_) + { + // Unregister from hub + EvidentHub* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_EPIShutter1); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentEPIShutter1::Busy() +{ + return false; // Shutter changes are instantaneous +} + +int EvidentEPIShutter1::SetOpen(bool open) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string cmd = BuildCommand(CMD_EPI_SHUTTER1, open ? 1 : 0); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_EPI_SHUTTER1)) + return ERR_NEGATIVE_ACK; + + // Update MCU indicator I5 with new shutter state (0=closed, 1=open) + hub->UpdateEPIShutter1Indicator(open ? 1 : 0); + + return DEVICE_OK; +} + +int EvidentEPIShutter1::GetOpen(bool& open) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string cmd = BuildQuery(CMD_EPI_SHUTTER1); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int state = ParseIntParameter(params[0]); + open = (state == 1); + } + + return DEVICE_OK; +} + +int EvidentEPIShutter1::Fire(double /*deltaT*/) +{ + // Not implemented for this shutter + return DEVICE_UNSUPPORTED_COMMAND; +} + +int EvidentEPIShutter1::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + bool open; + int ret = GetOpen(open); + if (ret != DEVICE_OK) + return ret; + pProp->Set(open ? 1L : 0L); + } + else if (eAct == MM::AfterSet) + { + long state; + pProp->Get(state); + return SetOpen(state != 0); + } + return DEVICE_OK; +} + +EvidentHub* EvidentEPIShutter1::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentMirrorUnit1 - Mirror Unit 1 (Filter Cube Turret) Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentMirrorUnit1::EvidentMirrorUnit1() : + initialized_(false), + name_(g_MirrorUnit1DeviceName), + numPos_(6) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Mirror unit 1 not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentMirrorUnit1::~EvidentMirrorUnit1() +{ + Shutdown(); +} + +void EvidentMirrorUnit1::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentMirrorUnit1::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_MirrorUnit1)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_MirrorUnit1); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentMirrorUnit1::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream label; + label << "Position-" << (i + 1); + SetPositionLabel(i, label.str().c_str()); + } + + // Note: MirrorUnit uses NMUINIT1 which is an initialization notification, + // not a position change notification, so we use query-based position tracking + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_MirrorUnit1); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Register with hub so encoder can notify property changes + hub->RegisterDeviceAsUsed(DeviceType_MirrorUnit1, this); + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentMirrorUnit1::Shutdown() +{ + if (initialized_) + { + // Unregister from hub + EvidentHub* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_MirrorUnit1); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentMirrorUnit1::Busy() +{ + return false; // Mirror unit changes are instantaneous +} + +unsigned long EvidentMirrorUnit1::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentMirrorUnit1::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query current position from hardware + std::string cmd = BuildQuery(CMD_MIRROR_UNIT1); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 1 && pos <= static_cast(numPos_)) + { + // Convert from 1-based to 0-based + pProp->Set(static_cast(pos - 1)); + } + } + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + std::string cmd = BuildCommand(CMD_MIRROR_UNIT1, static_cast(pos + 1)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_MIRROR_UNIT1)) + return ERR_NEGATIVE_ACK; + + // Update MCU indicator I2 with new mirror position (1-based) + hub->UpdateMirrorUnitIndicator(static_cast(pos + 1)); + } + return DEVICE_OK; +} + +EvidentHub* EvidentMirrorUnit1::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + + +//int EvidentMirrorUnit1::EnableNotifications(bool /*enable*/) +//{ + // NMUINIT1 is an initialization notification, not a position change notification + // MirrorUnit1 uses query-based position tracking instead +// return DEVICE_OK; +//} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentPolarizer - Polarizer Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentPolarizer::EvidentPolarizer() : + initialized_(false), + name_(g_PolarizerDeviceName), + numPos_(6) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Polarizer not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentPolarizer::~EvidentPolarizer() +{ + Shutdown(); +} + +void EvidentPolarizer::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentPolarizer::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_Polarizer)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_Polarizer); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentPolarizer::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels - Polarizer has Out (0) and In (1) + SetPositionLabel(0, "Out"); + SetPositionLabel(1, "In"); + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_Polarizer); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Enable notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + return ret; + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentPolarizer::Shutdown() +{ + if (initialized_) + { + EnableNotifications(false); + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentPolarizer::Busy() +{ + EvidentHub* hub = GetHub(); + if (!hub) + return false; + + return hub->GetModel()->IsBusy(DeviceType_Polarizer); +} + +unsigned long EvidentPolarizer::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentPolarizer::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + long pos = hub->GetModel()->GetPosition(DeviceType_Polarizer); + if (pos < 0) + return ERR_POSITION_UNKNOWN; + + // Polarizer uses 0-based indexing (PO 0 = Out, PO 1 = In), no conversion needed + pProp->Set(pos); + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Set target position BEFORE sending command + // Polarizer uses 0-based indexing (PO 0 = Out, PO 1 = In) + hub->GetModel()->SetTargetPosition(DeviceType_Polarizer, pos); + + // Check if already at target position + long currentPos = hub->GetModel()->GetPosition(DeviceType_Polarizer); + if (currentPos == pos) + { + // Already at target, no need to move + hub->GetModel()->SetBusy(DeviceType_Polarizer, false); + return DEVICE_OK; + } + + hub->GetModel()->SetBusy(DeviceType_Polarizer, true); + + std::string cmd = BuildCommand(CMD_POLARIZER, static_cast(pos)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + hub->GetModel()->SetBusy(DeviceType_Polarizer, false); + return ret; + } + + if (!IsPositiveAck(response, CMD_POLARIZER)) + { + hub->GetModel()->SetBusy(DeviceType_Polarizer, false); + return ERR_NEGATIVE_ACK; + } + + // Polarizer does not send notifications (NPO) when movement completes. + // The positive ack ("PO +") is only returned after movement completes, + // so we can clear busy immediately and update position. + hub->GetModel()->SetPosition(DeviceType_Polarizer, pos); + hub->GetModel()->SetBusy(DeviceType_Polarizer, false); + } + return DEVICE_OK; +} + +EvidentHub* EvidentPolarizer::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentPolarizer::EnableNotifications(bool enable) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Use PO (command tag) not NPO (notification tag) to enable notifications + return hub->EnableNotification(CMD_POLARIZER, enable); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentDICPrism - DIC Prism Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentDICPrism::EvidentDICPrism() : + initialized_(false), + name_(g_DICPrismDeviceName), + numPos_(6) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "DIC prism not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentDICPrism::~EvidentDICPrism() +{ + Shutdown(); +} + +void EvidentDICPrism::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentDICPrism::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_DICPrism)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_DICPrism); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentDICPrism::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream label; + label << "Position-" << (i + 1); + SetPositionLabel(i, label.str().c_str()); + } + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_DICPrism); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentDICPrism::Shutdown() +{ + if (initialized_) + { + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentDICPrism::Busy() +{ + return false; // DIC prism changes are instantaneous +} + +unsigned long EvidentDICPrism::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentDICPrism::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query current position + std::string cmd = BuildQuery(CMD_DIC_PRISM); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 1 && pos <= static_cast(numPos_)) + { + // Convert from 1-based to 0-based + pProp->Set(static_cast(pos - 1)); + } + } + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + std::string cmd = BuildCommand(CMD_DIC_PRISM, static_cast(pos + 1)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_DIC_PRISM)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +EvidentHub* EvidentDICPrism::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentEPIND - EPI ND Filter Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentEPIND::EvidentEPIND() : + initialized_(false), + name_(g_EPINDDeviceName), + numPos_(6) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "EPI ND filter not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentEPIND::~EvidentEPIND() +{ + Shutdown(); +} + +void EvidentEPIND::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentEPIND::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_EPIND)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_EPIND); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentEPIND::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream label; + label << "Position-" << (i + 1); + SetPositionLabel(i, label.str().c_str()); + } + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_EPIND); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentEPIND::Shutdown() +{ + if (initialized_) + { + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentEPIND::Busy() +{ + return false; // ND filter changes are instantaneous +} + +unsigned long EvidentEPIND::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentEPIND::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query current position + std::string cmd = BuildQuery(CMD_EPI_ND); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 1 && pos <= static_cast(numPos_)) + { + // Convert from 1-based to 0-based + pProp->Set(static_cast(pos - 1)); + } + } + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + std::string cmd = BuildCommand(CMD_EPI_ND, static_cast(pos + 1)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_EPI_ND)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +EvidentHub* EvidentEPIND::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentCorrectionCollar - Correction Collar Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentCorrectionCollar::EvidentCorrectionCollar() : + initialized_(false), + linked_(false), + name_(g_CorrectionCollarDeviceName), + stepSizeUm_(CORRECTION_COLLAR_STEP_SIZE_UM) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Correction collar not available on this microscope"); + SetErrorText(ERR_CORRECTION_COLLAR_NOT_LINKED, "Correction Collar must be linked before setting position. Set Activate property to 'Linked'."); + SetErrorText(ERR_CORRECTION_COLLAR_LINK_FAILED, "Correction Collar linking failed. Ensure correct objective is installed (typically objective 6)."); + + CreateHubIDProperty(); +} + +EvidentCorrectionCollar::~EvidentCorrectionCollar() +{ + Shutdown(); +} + +void EvidentCorrectionCollar::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentCorrectionCollar::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_CorrectionCollar)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Create Activate property for linking/unlinking + CPropertyAction* pAct = new CPropertyAction(this, &EvidentCorrectionCollar::OnActivate); + int ret = CreateProperty("Activate", "Unlinked", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + AddAllowedValue("Activate", "Linked"); + AddAllowedValue("Activate", "Unlinked"); + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_CorrectionCollar); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentCorrectionCollar::Shutdown() +{ + if (initialized_) + { + // Auto-unlink on shutdown if linked + if (linked_) + { + EvidentHub* hub = GetHub(); + if (hub) + { + std::string cmd = BuildCommand(CMD_CORRECTION_COLLAR_LINK, 0); // 0 = Unlink + std::string response; + hub->ExecuteCommand(cmd, response); + // Don't check response - best effort unlink + } + linked_ = false; + + // Notify core that position changed to 0 + GetCoreCallback()->OnStagePositionChanged(this, 0.0); + } + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentCorrectionCollar::Busy() +{ + return false; // Correction collar changes are instantaneous +} + +int EvidentCorrectionCollar::OnActivate(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + // Return current linked state + pProp->Set(linked_ ? "Linked" : "Unlinked"); + } + else if (eAct == MM::AfterSet) + { + std::string state; + pProp->Get(state); + + if (state == "Linked" && !linked_) + { + // Link the correction collar + std::string cmd = BuildCommand(CMD_CORRECTION_COLLAR_LINK, 1); // 1 = Link + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_CORRECTION_COLLAR_LINK)) + return ERR_CORRECTION_COLLAR_LINK_FAILED; + + // Initialize the correction collar + cmd = BuildCommand(CMD_CORRECTION_COLLAR_INIT); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + // Link succeeded but init failed - try to unlink + cmd = BuildCommand(CMD_CORRECTION_COLLAR_LINK, 0); + hub->ExecuteCommand(cmd, response); + return ERR_CORRECTION_COLLAR_LINK_FAILED; + } + + if (!IsPositiveAck(response, CMD_CORRECTION_COLLAR_INIT)) + { + // Link succeeded but init failed - try to unlink + cmd = BuildCommand(CMD_CORRECTION_COLLAR_LINK, 0); + hub->ExecuteCommand(cmd, response); + return ERR_CORRECTION_COLLAR_LINK_FAILED; + } + + // Successfully linked and initialized + linked_ = true; + } + else if (state == "Unlinked" && linked_) + { + // Unlink the correction collar + std::string cmd = BuildCommand(CMD_CORRECTION_COLLAR_LINK, 0); // 0 = Unlink + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_CORRECTION_COLLAR_LINK)) + return ERR_NEGATIVE_ACK; + + // Successfully unlinked + linked_ = false; + + // Notify core that position changed to 0 + GetCoreCallback()->OnStagePositionChanged(this, 0.0); + } + } + return DEVICE_OK; +} + +EvidentHub* EvidentCorrectionCollar::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentCorrectionCollar::SetPositionUm(double pos) +{ + // Convert μm to steps (1:1 ratio) + long steps = static_cast(pos / stepSizeUm_); + return SetPositionSteps(steps); +} + +int EvidentCorrectionCollar::GetPositionUm(double& pos) +{ + long steps; + int ret = GetPositionSteps(steps); + if (ret != DEVICE_OK) + return ret; + + pos = steps * stepSizeUm_; + return DEVICE_OK; +} + +int EvidentCorrectionCollar::SetPositionSteps(long steps) +{ + // Check if linked + if (!linked_) + return ERR_CORRECTION_COLLAR_NOT_LINKED; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Clamp to limits + if (steps < CORRECTION_COLLAR_MIN_POS) steps = CORRECTION_COLLAR_MIN_POS; + if (steps > CORRECTION_COLLAR_MAX_POS) steps = CORRECTION_COLLAR_MAX_POS; + + // Send CC command with position + std::string cmd = BuildCommand(CMD_CORRECTION_COLLAR, static_cast(steps)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_CORRECTION_COLLAR)) + return ERR_NEGATIVE_ACK; + + return DEVICE_OK; +} + +int EvidentCorrectionCollar::GetPositionSteps(long& steps) +{ + // If not linked, return 0 (no error) + if (!linked_) + { + steps = 0; + return DEVICE_OK; + } + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query current position + std::string cmd = BuildQuery(CMD_CORRECTION_COLLAR); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= CORRECTION_COLLAR_MIN_POS && pos <= CORRECTION_COLLAR_MAX_POS) + { + steps = pos; + return DEVICE_OK; + } + } + + return ERR_INVALID_RESPONSE; +} + +int EvidentCorrectionCollar::SetOrigin() +{ + // Not supported by IX85 correction collar + return DEVICE_UNSUPPORTED_COMMAND; +} + +int EvidentCorrectionCollar::GetLimits(double& lower, double& upper) +{ + lower = CORRECTION_COLLAR_MIN_POS * stepSizeUm_; + upper = CORRECTION_COLLAR_MAX_POS * stepSizeUm_; + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentEPIShutter2 - EPI (Reflected Light) Shutter 2 Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentEPIShutter2::EvidentEPIShutter2() : + initialized_(false), + name_(g_EPIShutter2DeviceName) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "EPI shutter 2 not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentEPIShutter2::~EvidentEPIShutter2() +{ + Shutdown(); +} + +void EvidentEPIShutter2::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentEPIShutter2::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_EPIShutter2)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Create state property + CPropertyAction* pAct = new CPropertyAction(this, &EvidentEPIShutter2::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + AddAllowedValue(MM::g_Keyword_State, "0"); // Closed + AddAllowedValue(MM::g_Keyword_State, "1"); // Open + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_EPIShutter2); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + initialized_ = true; + + // Close shutter on startup + SetOpen(false); + + return DEVICE_OK; +} + +int EvidentEPIShutter2::Shutdown() +{ + if (initialized_) + { + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentEPIShutter2::Busy() +{ + return false; // Shutter changes are instantaneous +} + +int EvidentEPIShutter2::SetOpen(bool open) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string cmd = BuildCommand(CMD_EPI_SHUTTER2, open ? 1 : 0); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_EPI_SHUTTER2)) + return ERR_NEGATIVE_ACK; + + return DEVICE_OK; +} + +int EvidentEPIShutter2::GetOpen(bool& open) +{ + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string cmd = BuildQuery(CMD_EPI_SHUTTER2); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int state = ParseIntParameter(params[0]); + open = (state == 1); + } + + return DEVICE_OK; +} + +int EvidentEPIShutter2::Fire(double /*deltaT*/) +{ + // Not implemented for this shutter + return DEVICE_UNSUPPORTED_COMMAND; +} + +int EvidentEPIShutter2::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + bool open; + int ret = GetOpen(open); + if (ret != DEVICE_OK) + return ret; + pProp->Set(open ? 1L : 0L); + } + else if (eAct == MM::AfterSet) + { + long state; + pProp->Get(state); + return SetOpen(state != 0); + } + return DEVICE_OK; +} + +EvidentHub* EvidentEPIShutter2::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentMirrorUnit2 - Mirror Unit 2 (Filter Cube Turret) Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentMirrorUnit2::EvidentMirrorUnit2() : + initialized_(false), + name_(g_MirrorUnit2DeviceName), + numPos_(6) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Mirror unit 2 not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentMirrorUnit2::~EvidentMirrorUnit2() +{ + Shutdown(); +} + +void EvidentMirrorUnit2::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentMirrorUnit2::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_MirrorUnit2)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_MirrorUnit2); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentMirrorUnit2::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream label; + label << "Position-" << (i + 1); + SetPositionLabel(i, label.str().c_str()); + } + + // Note: MirrorUnit uses NMUINIT2 which is an initialization notification, + // not a position change notification, so we use query-based position tracking + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_MirrorUnit2); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentMirrorUnit2::Shutdown() +{ + if (initialized_) + { + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentMirrorUnit2::Busy() +{ + return false; // Mirror unit changes are instantaneous +} + +unsigned long EvidentMirrorUnit2::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentMirrorUnit2::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query current position from hardware + std::string cmd = BuildQuery(CMD_MIRROR_UNIT2); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 1 && pos <= static_cast(numPos_)) + { + // Convert from 1-based to 0-based + pProp->Set(static_cast(pos - 1)); + } + } + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHub* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + std::string cmd = BuildCommand(CMD_MIRROR_UNIT2, static_cast(pos + 1)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_MIRROR_UNIT2)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +EvidentHub* EvidentMirrorUnit2::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentMirrorUnit2::EnableNotifications(bool /*enable*/) +{ + // NMUINIT2 is an initialization notification, not a position change notification + // MirrorUnit2 uses query-based position tracking instead + return DEVICE_OK; +} diff --git a/DeviceAdapters/EvidentIX85/EvidentIX85.h b/DeviceAdapters/EvidentIX85/EvidentIX85.h new file mode 100644 index 000000000..3d3bb85f9 --- /dev/null +++ b/DeviceAdapters/EvidentIX85/EvidentIX85.h @@ -0,0 +1,475 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentIX85.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 microscope device classes +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include "DeviceBase.h" +#include "EvidentHub.h" +#include "EvidentModel.h" +#include "EvidentProtocol.h" + + +////////////////////////////////////////////////////////////////////////////// +// Focus Drive (Z-Stage) +////////////////////////////////////////////////////////////////////////////// + +class EvidentHub; + +class EvidentFocus : public CStageBase +{ +public: + EvidentFocus(); + ~EvidentFocus(); + + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Stage API + int SetPositionUm(double pos); + int GetPositionUm(double& pos); + int SetPositionSteps(long steps); + int GetPositionSteps(long& steps); + int SetOrigin(); + int GetLimits(double& lower, double& upper); + int IsStageSequenceable(bool& isSequenceable) const { isSequenceable = false; return DEVICE_OK; }; + bool IsContinuousFocusDrive() const { return false; }; + + // Action interface + int OnPosition(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnSpeed(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnJogDirection(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnJogFineSensitivity(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnJogCoarseSensitivity(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + double stepSizeUm_; +}; + +////////////////////////////////////////////////////////////////////////////// +// Nosepiece (Objective Turret) +////////////////////////////////////////////////////////////////////////////// + +class EvidentNosepiece : public CStateDeviceBase +{ +public: + EvidentNosepiece(); + ~EvidentNosepiece(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnSafeChange(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + int EnableNotifications(bool enable); + int SafeNosepieceChange(long targetPosition); + + bool initialized_; + std::string name_; + unsigned int numPos_; + bool safeNosepieceChange_; // When true, lower focus before changing nosepiece +}; + +////////////////////////////////////////////////////////////////////////////// +// Magnification Changer +////////////////////////////////////////////////////////////////////////////// + +class EvidentMagnification : public CMagnifierBase +{ +public: + EvidentMagnification(); + ~EvidentMagnification(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // CMagnifierBase API + double GetMagnification(); + + // Action interface + int OnMagnification(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; + static const double magnifications_[3]; // 1.0x, 1.6x, 2.0x +}; + +////////////////////////////////////////////////////////////////////////////// +// Light Path Selector +////////////////////////////////////////////////////////////////////////////// + +class EvidentLightPath : public CStateDeviceBase +{ +public: + EvidentLightPath(); + ~EvidentLightPath(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + + bool initialized_; + std::string name_; +}; + +////////////////////////////////////////////////////////////////////////////// +// Condenser Turret +////////////////////////////////////////////////////////////////////////////// + +class EvidentCondenserTurret : public CStateDeviceBase +{ +public: + EvidentCondenserTurret(); + ~EvidentCondenserTurret(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; +}; + +////////////////////////////////////////////////////////////////////////////// +// DIA Shutter +////////////////////////////////////////////////////////////////////////////// + +class EvidentDIAShutter : public CShutterBase +{ +public: + EvidentDIAShutter(); + ~EvidentDIAShutter(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Shutter API + int SetOpen(bool open = true); + int GetOpen(bool& open); + int Fire(double deltaT); + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnBrightness(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnMechanicalShutter(MM::PropertyBase* pProp, MM::ActionType eAct); + + // Notification control + int EnableNotifications(bool enable); + +private: + EvidentHub* GetHub(); + + bool initialized_; + std::string name_; +}; + +////////////////////////////////////////////////////////////////////////////// +// EPI Shutter 1 +////////////////////////////////////////////////////////////////////////////// + +class EvidentEPIShutter1 : public CShutterBase +{ +public: + EvidentEPIShutter1(); + ~EvidentEPIShutter1(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Shutter API + int SetOpen(bool open = true); + int GetOpen(bool& open); + int Fire(double deltaT); + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + + bool initialized_; + std::string name_; +}; + +////////////////////////////////////////////////////////////////////////////// +// Mirror Unit 1 (Filter Cube Turret) +////////////////////////////////////////////////////////////////////////////// + +class EvidentMirrorUnit1 : public CStateDeviceBase +{ +public: + EvidentMirrorUnit1(); + ~EvidentMirrorUnit1(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; +}; + +////////////////////////////////////////////////////////////////////////////// +// EPI Shutter 2 +////////////////////////////////////////////////////////////////////////////// + +class EvidentEPIShutter2 : public CShutterBase +{ +public: + EvidentEPIShutter2(); + ~EvidentEPIShutter2(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Shutter API + int SetOpen(bool open = true); + int GetOpen(bool& open); + int Fire(double deltaT); + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + + bool initialized_; + std::string name_; +}; + +////////////////////////////////////////////////////////////////////////////// +// Mirror Unit 2 (Filter Cube Turret) +////////////////////////////////////////////////////////////////////////////// + +class EvidentMirrorUnit2 : public CStateDeviceBase +{ +public: + EvidentMirrorUnit2(); + ~EvidentMirrorUnit2(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; +}; + +////////////////////////////////////////////////////////////////////////////// +// Polarizer +////////////////////////////////////////////////////////////////////////////// + +class EvidentPolarizer : public CStateDeviceBase +{ +public: + EvidentPolarizer(); + ~EvidentPolarizer(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; +}; + +////////////////////////////////////////////////////////////////////////////// +// DIC Prism +////////////////////////////////////////////////////////////////////////////// + +class EvidentDICPrism : public CStateDeviceBase +{ +public: + EvidentDICPrism(); + ~EvidentDICPrism(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; +}; + +////////////////////////////////////////////////////////////////////////////// +// EPI ND Filter +////////////////////////////////////////////////////////////////////////////// + +class EvidentEPIND : public CStateDeviceBase +{ +public: + EvidentEPIND(); + ~EvidentEPIND(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; +}; + +////////////////////////////////////////////////////////////////////////////// +// Correction Collar +////////////////////////////////////////////////////////////////////////////// + +class EvidentCorrectionCollar : public CStageBase +{ +public: + EvidentCorrectionCollar(); + ~EvidentCorrectionCollar(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Stage API + int SetPositionUm(double pos); + int GetPositionUm(double& pos); + int SetPositionSteps(long steps); + int GetPositionSteps(long& steps); + int SetOrigin(); + int GetLimits(double& lower, double& upper); + int IsStageSequenceable(bool& isSequenceable) const { isSequenceable = false; return DEVICE_OK; }; + bool IsContinuousFocusDrive() const { return false; }; + + // Action interface + int OnActivate(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHub* GetHub(); + + bool initialized_; + bool linked_; + std::string name_; + double stepSizeUm_; +}; diff --git a/DeviceAdapters/EvidentIX85/EvidentIX85.vcxproj b/DeviceAdapters/EvidentIX85/EvidentIX85.vcxproj new file mode 100644 index 000000000..6f8d626d4 --- /dev/null +++ b/DeviceAdapters/EvidentIX85/EvidentIX85.vcxproj @@ -0,0 +1,121 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {E8A5D9F1-4B2C-4E8F-9C1D-6F3A8B7E2D4C} + EvidentIX85 + Win32Proj + 10.0 + + + + DynamicLibrary + MultiByte + v143 + false + + + DynamicLibrary + MultiByte + v143 + true + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + true + false + + + + X64 + + + Disabled + true + Speed + NOMINMAX;WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + EnableFastChecks + true + + + 4290;%(DisableSpecificWarnings) + stdcpp17 + %(AdditionalIncludeDirectories) + + + Windows + + + %(AdditionalDependencies) + %(AdditionalLibraryDirectories) + + + + + X64 + + + MaxSpeed + true + Speed + NOMINMAX;WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + + + 4290;%(DisableSpecificWarnings) + Default + %(AdditionalIncludeDirectories) + + + Windows + true + true + + + %(AdditionalDependencies) + %(AdditionalLibraryDirectories) + + + + + + + + + + + + + + + + {b8c95f39-54bf-40a9-807b-598df2821d55} + + + + + + \ No newline at end of file diff --git a/DeviceAdapters/EvidentIX85/EvidentIX85.vcxproj.filters b/DeviceAdapters/EvidentIX85/EvidentIX85.vcxproj.filters new file mode 100644 index 000000000..14aa60e08 --- /dev/null +++ b/DeviceAdapters/EvidentIX85/EvidentIX85.vcxproj.filters @@ -0,0 +1,42 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx + + + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + diff --git a/DeviceAdapters/EvidentIX85/EvidentModel.cpp b/DeviceAdapters/EvidentIX85/EvidentModel.cpp new file mode 100644 index 000000000..c02405ce3 --- /dev/null +++ b/DeviceAdapters/EvidentIX85/EvidentModel.cpp @@ -0,0 +1,206 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentModel.cpp +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 microscope state model implementation +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#include "EvidentModel.h" +#include + +namespace EvidentIX85 { + +MicroscopeModel::MicroscopeModel() +{ +} + +MicroscopeModel::~MicroscopeModel() +{ +} + +long long MicroscopeModel::SteadyMicroseconds() +{ + using namespace std::chrono; + auto now = steady_clock::now().time_since_epoch(); + auto usec = duration_cast(now); + return usec.count(); +} + +void MicroscopeModel::SetDevicePresent(DeviceType type, bool present) +{ + std::lock_guard lock(mutex_); + GetOrCreateState(type).present = present; +} + +bool MicroscopeModel::IsDevicePresent(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).present; +} + +void MicroscopeModel::SetPosition(DeviceType type, long position) +{ + std::lock_guard lock(mutex_); + auto& state = GetOrCreateState(type); + state.currentPos = position; + state.lastUpdateTime = MM::MMTime::fromUs(SteadyMicroseconds()); +} + +long MicroscopeModel::GetPosition(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).currentPos; +} + +bool MicroscopeModel::IsPositionUnknown(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).currentPos == -1; +} + +void MicroscopeModel::SetTargetPosition(DeviceType type, long position) +{ + std::lock_guard lock(mutex_); + auto& state = GetOrCreateState(type); + state.targetPos = position; + state.lastRequestTime = MM::MMTime::fromUs(SteadyMicroseconds()); +} + +long MicroscopeModel::GetTargetPosition(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).targetPos; +} + +void MicroscopeModel::SetBusy(DeviceType type, bool busy) +{ + std::lock_guard lock(mutex_); + GetOrCreateState(type).busy = busy; +} + +bool MicroscopeModel::IsBusy(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).busy; +} + +void MicroscopeModel::SetLimits(DeviceType type, long minPos, long maxPos) +{ + std::lock_guard lock(mutex_); + auto& state = GetOrCreateState(type); + state.minPos = minPos; + state.maxPos = maxPos; +} + +void MicroscopeModel::GetLimits(DeviceType type, long& minPos, long& maxPos) const +{ + std::lock_guard lock(mutex_); + const auto& state = GetStateConst(type); + minPos = state.minPos; + maxPos = state.maxPos; +} + +void MicroscopeModel::SetNumPositions(DeviceType type, int numPos) +{ + std::lock_guard lock(mutex_); + GetOrCreateState(type).numPositions = numPos; +} + +int MicroscopeModel::GetNumPositions(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).numPositions; +} + +void MicroscopeModel::SetLastUpdateTime(DeviceType type, MM::MMTime time) +{ + std::lock_guard lock(mutex_); + GetOrCreateState(type).lastUpdateTime = time; +} + +MM::MMTime MicroscopeModel::GetLastUpdateTime(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).lastUpdateTime; +} + +void MicroscopeModel::SetLastRequestTime(DeviceType type, MM::MMTime time) +{ + std::lock_guard lock(mutex_); + GetOrCreateState(type).lastRequestTime = time; +} + +MM::MMTime MicroscopeModel::GetLastRequestTime(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).lastRequestTime; +} + +DeviceState MicroscopeModel::GetDeviceState(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type); +} + +void MicroscopeModel::SetDeviceState(DeviceType type, const DeviceState& state) +{ + std::lock_guard lock(mutex_); + devices_[type] = state; +} + +void MicroscopeModel::SetDeviceVersion(DeviceType type, const std::string& version) +{ + std::lock_guard lock(mutex_); + GetOrCreateState(type).version = version; +} + +std::string MicroscopeModel::GetDeviceVersion(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).version; +} + +void MicroscopeModel::Clear() +{ + std::lock_guard lock(mutex_); + devices_.clear(); +} + +DeviceState& MicroscopeModel::GetOrCreateState(DeviceType type) +{ + auto it = devices_.find(type); + if (it == devices_.end()) + { + DeviceState newState; + newState.type = type; + devices_[type] = newState; + return devices_[type]; + } + return it->second; +} + +const DeviceState& MicroscopeModel::GetStateConst(DeviceType type) const +{ + static DeviceState emptyState; + auto it = devices_.find(type); + if (it == devices_.end()) + return emptyState; + return it->second; +} + +} // namespace EvidentIX85 diff --git a/DeviceAdapters/EvidentIX85/EvidentModel.h b/DeviceAdapters/EvidentIX85/EvidentModel.h new file mode 100644 index 000000000..702afeeb3 --- /dev/null +++ b/DeviceAdapters/EvidentIX85/EvidentModel.h @@ -0,0 +1,152 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentModel.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 microscope state model +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include "MMDevice.h" +#include +#include +#include + +namespace EvidentIX85 { + +// Device type enumeration +enum DeviceType +{ + DeviceType_Unknown = 0, + DeviceType_Focus, + DeviceType_Nosepiece, + DeviceType_Magnification, + DeviceType_LightPath, + DeviceType_CondenserTurret, + DeviceType_Condenser, + DeviceType_DIAAperture, + DeviceType_DIAShutter, + DeviceType_DIABrightness, + DeviceType_Polarizer, + DeviceType_DICPrism, + DeviceType_DICRetardation, + DeviceType_EPIShutter1, + DeviceType_EPIShutter2, + DeviceType_MirrorUnit1, + DeviceType_MirrorUnit2, + DeviceType_EPIND, + DeviceType_RightPort, + DeviceType_CorrectionCollar, + DeviceType_Autofocus, + DeviceType_OffsetLens, + DeviceType_ManualControl +}; + +// Device state structure +struct DeviceState +{ + DeviceType type; + bool present; + bool busy; + long currentPos; + long targetPos; + long minPos; + long maxPos; + int numPositions; // For state devices + std::string version; // Firmware version from V command + MM::MMTime lastUpdateTime; + MM::MMTime lastRequestTime; + + DeviceState() : + type(DeviceType_Unknown), + present(false), + busy(false), + currentPos(0), + targetPos(0), + minPos(0), + maxPos(0), + numPositions(0), + version(""), + lastUpdateTime(0.0), + lastRequestTime(0.0) + {} +}; + +// Microscope model - centralized state for all devices +class MicroscopeModel +{ +public: + MicroscopeModel(); + ~MicroscopeModel(); + + // Gets current time in microseconds + static long long SteadyMicroseconds(); + + // Device presence + void SetDevicePresent(DeviceType type, bool present); + bool IsDevicePresent(DeviceType type) const; + + // Position access (thread-safe) + void SetPosition(DeviceType type, long position); + long GetPosition(DeviceType type) const; + bool IsPositionUnknown(DeviceType type) const; + + // Target position + void SetTargetPosition(DeviceType type, long position); + long GetTargetPosition(DeviceType type) const; + + // Busy state + void SetBusy(DeviceType type, bool busy); + bool IsBusy(DeviceType type) const; + + // Position limits + void SetLimits(DeviceType type, long minPos, long maxPos); + void GetLimits(DeviceType type, long& minPos, long& maxPos) const; + + // Number of positions (for state devices) + void SetNumPositions(DeviceType type, int numPos); + int GetNumPositions(DeviceType type) const; + + // Firmware version + void SetDeviceVersion(DeviceType type, const std::string& version); + std::string GetDeviceVersion(DeviceType type) const; + + // Timestamps + void SetLastUpdateTime(DeviceType type, MM::MMTime time); + MM::MMTime GetLastUpdateTime(DeviceType type) const; + + void SetLastRequestTime(DeviceType type, MM::MMTime time); + MM::MMTime GetLastRequestTime(DeviceType type) const; + + // Full state access (for initialization) + DeviceState GetDeviceState(DeviceType type) const; + void SetDeviceState(DeviceType type, const DeviceState& state); + + // Clear all state + void Clear(); + +private: + mutable std::mutex mutex_; + std::map devices_; + + // Helper to get or create device state + DeviceState& GetOrCreateState(DeviceType type); + const DeviceState& GetStateConst(DeviceType type) const; +}; + +} // namespace EvidentIX85 diff --git a/DeviceAdapters/EvidentIX85/EvidentProtocol.h b/DeviceAdapters/EvidentIX85/EvidentProtocol.h new file mode 100644 index 000000000..5b75f3826 --- /dev/null +++ b/DeviceAdapters/EvidentIX85/EvidentProtocol.h @@ -0,0 +1,446 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentProtocol.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 microscope protocol definitions +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include +#include +#include +#include + +namespace EvidentIX85 { + +// Protocol constants +const char* const TERMINATOR = "\r\n"; +const char TAG_DELIMITER = ' '; +const char DATA_DELIMITER = ','; +const char POSITIVE_ACK = '+'; +const char NEGATIVE_ACK = '!'; +const char UNKNOWN_RESPONSE = 'X'; + +const int MAX_COMMAND_LENGTH = 128; +const long ANSWER_TIMEOUT_MS = 4000; + +// Serial port settings +const int BAUD_RATE = 115200; +const int DATA_BITS = 8; +const char PARITY = 'E'; // Even +const int STOP_BITS = 2; + +// Command tags - System commands +const char* const CMD_LOGIN = "L"; +const char* const CMD_UNIT = "U"; +const char* const CMD_VERSION = "V"; +const char* const CMD_ERROR = "ER"; + +// V command unit numbers for device detection +const int V_CONTROLLER = 1; +const int V_NOSEPIECE = 2; +const int V_CORRECTION_COLLAR_LINK = 3; +const int V_CORRECTION_COLLAR_ROULETTE = 4; +const int V_FOCUS = 5; +const int V_LIGHTPATH = 6; +const int V_CONDENSER_UNIT = 7; // IX3-LWUCDA: Polarizer, CondenserTurret, DIAShutter, DIAAperture +const int V_DIC_UNIT = 8; // IX5-DICTA: DICPrism, DICRetardation +const int V_MIRROR_UNIT1 = 9; +const int V_EPI_SHUTTER1 = 10; +const int V_MIRROR_UNIT2 = 11; +const int V_EPI_SHUTTER2 = 12; +const int V_MANUAL_CONTROL = 13; +const int V_EPIND = 14; +const int V_SD_MAGNIFICATION = 15; // SDCA magnification (SPIN/SR system) +const int V_AUTOFOCUS = 16; +const int V_OFFSET_LENS = 17; +const int V_FV40_PSU = 18; + +// Command tags - Focus +const char* const CMD_FOCUS_GOTO = "FG"; +const char* const CMD_FOCUS_MOVE = "FM"; +const char* const CMD_FOCUS_STOP = "FSTP"; +const char* const CMD_FOCUS_POSITION = "FP"; +const char* const CMD_FOCUS_NOTIFY = "NFP"; +const char* const CMD_FOCUS_SPEED = "FSPD"; +const char* const CMD_FOCUS_NEAR_LIMIT = "NL"; +const char* const CMD_FOCUS_FAR_LIMIT = "FL"; + +// Command tags - Nosepiece (Objective Turret) +const char* const CMD_NOSEPIECE = "OB"; +const char* const CMD_NOSEPIECE_NOTIFY = "NOB"; + +// Command tags - Magnification Changer +const char* const CMD_MAGNIFICATION = "CA"; +const char* const CMD_MAGNIFICATION_NOTIFY = "NCA"; + +// Command tags - Light Path +const char* const CMD_LIGHT_PATH = "BIL"; + +// Command tags - Condenser +const char* const CMD_CONDENSER_TURRET = "TR"; +const char* const CMD_CONDENSER_TURRET_NOTIFY = "NTR"; +const char* const CMD_CONDENSER_CONTROL = "CD"; +const char* const CMD_CONDENSER_NOTIFY = "NCD"; +const char* const CMD_CONDENSER_SWITCH = "S1"; + +// Command tags - DIA (Transmitted Light) +const char* const CMD_DIA_APERTURE = "DAS"; +const char* const CMD_DIA_APERTURE_NOTIFY = "NDAS"; +const char* const CMD_DIA_APERTURE_STOP = "DASSTP"; +const char* const CMD_DIA_SHUTTER = "DSH"; +const char* const CMD_DIA_ILLUMINATION = "DIL"; +const char* const CMD_DIA_ILLUMINATION_NOTIFY = "NDIL"; + +// Command tags - Polarizer +const char* const CMD_POLARIZER = "PO"; +const char* const CMD_POLARIZER_NOTIFY = "NPO"; + +// Command tags - DIC +const char* const CMD_DIC_PRISM = "DIC"; +const char* const CMD_DIC_RETARDATION = "DICR"; +const char* const CMD_DIC_RETARDATION_NOTIFY = "NDICR"; +const char* const CMD_DIC_LOCALIZED_NOTIFY = "NLC"; +const char* const CMD_DIC_LOCALIZED = "LC"; + +// Command tags - EPI (Reflected Light) +const char* const CMD_EPI_SHUTTER1 = "ESH1"; +const char* const CMD_EPI_SHUTTER2 = "ESH2"; +const char* const CMD_MIRROR_UNIT1 = "MU1"; +const char* const CMD_MIRROR_UNIT2 = "MU2"; +const char* const CMD_MIRROR_UNIT_NOTIFY1 = "NMUINIT1"; +const char* const CMD_MIRROR_UNIT_NOTIFY2 = "NMUINIT2"; +const char* const CMD_COVER_SWITCH1 = "C1"; +const char* const CMD_COVER_SWITCH2 = "C2"; +const char* const CMD_EPI_ND = "END"; + +// Command tags - Right Port +const char* const CMD_RIGHT_PORT = "BIR"; +const char* const CMD_RIGHT_PORT_NOTIFY = "NBIR"; + +// Command tags - Correction Collar +const char* const CMD_CORRECTION_COLLAR = "CC"; +const char* const CMD_CORRECTION_COLLAR_LINK = "CCL"; +const char* const CMD_CORRECTION_COLLAR_INIT = "CCINIT"; + +// Command tags - Autofocus (ZDC) +const char* const CMD_AF_START_STOP = "AF"; +const char* const CMD_AF_STATUS = "AFST"; +const char* const CMD_AF_TABLE = "AFTBL"; +const char* const CMD_AF_PARAMETER = "AFP"; +const char* const CMD_AF_GET_PARAMETER = "GAFP"; +const char* const CMD_AF_NEAR_LIMIT = "AFNL"; +const char* const CMD_AF_FAR_LIMIT = "AFFL"; +const char* const CMD_AF_BUZZER = "AFBZ"; +const char* const CMD_AF_APERTURE = "AFAS"; +const char* const CMD_AF_DICHROIC = "AFDM"; +const char* const CMD_AF_DICHROIC_MOVE = "AFDMG"; + +// Command tags - Offset Lens (part of ZDC) +const char* const CMD_OFFSET_LENS_GOTO = "ABG"; +const char* const CMD_OFFSET_LENS_MOVE = "ABM"; +const char* const CMD_OFFSET_LENS_STOP = "ABSTP"; +const char* const CMD_OFFSET_LENS_POSITION = "ABP"; +const char* const CMD_OFFSET_LENS_NOTIFY = "NABP"; +const char* const CMD_OFFSET_LENS_RANGE = "ABRANGE"; +const char* const CMD_OFFSET_LENS_LIMIT = "ABLMT"; +const char* const CMD_OFFSET_LENS_LOST_MOTION = "ABLM"; + +// Command tags - MCZ (Manual Control Unit) +const char* const CMD_JOG = "JG"; +const char* const CMD_JOG_SENSITIVITY_FINE = "JGSF"; +const char* const CMD_JOG_SENSITIVITY_COARSE = "JGSC"; +const char* const CMD_JOG_DIRECTION = "JGDR"; // Jog direction (0=Reverse, 1=Default) +const char* const CMD_JOG_LIMIT = "JGL"; +const char* const CMD_OFFSET_LENS_SENSITIVITY_FINE = "ABJGSF"; +const char* const CMD_OFFSET_LENS_SENSITIVITY_COARSE = "ABJGSC"; + +// Command tags - SD Magnification Changer +const char* const CMD_SD_MAGNIFICATION = "SDCA"; + +// Command tags - Indicators and Encoders +const char* const CMD_INDICATOR_CONTROL = "I"; +const char* const CMD_INDICATOR1 = "I1"; +const char* const CMD_INDICATOR2 = "I2"; +const char* const CMD_INDICATOR3 = "I3"; +const char* const CMD_INDICATOR4 = "I4"; +const char* const CMD_INDICATOR5 = "I5"; +const char* const CMD_ENCODER1 = "E1"; +const char* const CMD_ENCODER2 = "E2"; +const char* const CMD_ENCODER3 = "E3"; +const char* const CMD_DIL_ENCODER_CONTROL = "DILE"; +const char* const CMD_MCZ_SWITCH = "S2"; + +// Device limits and constants +const long FOCUS_MIN_POS = 0; +const long FOCUS_MAX_POS = 1050000; // 10.5mm in 10nm units +const double FOCUS_STEP_SIZE_UM = 0.01; // 10nm = 0.01um +const long FOCUS_POSITION_TOLERANCE = 10; // 10 steps = 100nm tolerance for "at position" detection + +const int NOSEPIECE_MIN_POS = 1; +const int NOSEPIECE_MAX_POS = 6; + +const int MAGNIFICATION_MIN_POS = 1; +const int MAGNIFICATION_MAX_POS = 3; + +// Most turrets and state devices have up to 6 positions +const int CONDENSER_TURRET_MAX_POS = 6; +const int MIRROR_UNIT_MAX_POS = 8; +const int POLARIZER_MAX_POS = 2; // Out (0) and In (1) +const int DIC_PRISM_MAX_POS = 6; +const int EPIND_MAX_POS = 6; + +const int LIGHT_PATH_LEFT_PORT = 1; +const int LIGHT_PATH_BI_50_50 = 2; +const int LIGHT_PATH_BI_100 = 3; +const int LIGHT_PATH_RIGHT_PORT = 4; + +// Correction Collar +const long CORRECTION_COLLAR_MIN_POS = -3200; +const long CORRECTION_COLLAR_MAX_POS = 3200; +const double CORRECTION_COLLAR_STEP_SIZE_UM = 1.0; // 1 step = 1 µm + +// Manual Control Unit (MCU) 7-segment display codes +// These hex codes drive the 7-segment displays on the MCU indicators +const int SEG7_0 = 0xEE; +const int SEG7_1 = 0x28; +const int SEG7_2 = 0xCD; +const int SEG7_3 = 0x6D; +const int SEG7_4 = 0x2B; +const int SEG7_5 = 0x67; +const int SEG7_6 = 0xE7; +const int SEG7_7 = 0x2E; +const int SEG7_8 = 0xEF; +const int SEG7_9 = 0x6F; +const int SEG7_DASH = 0x01; + +// Helper function to get 7-segment code for a digit +inline int Get7SegmentCode(int digit) +{ + switch (digit) + { + case 0: return SEG7_0; + case 1: return SEG7_1; + case 2: return SEG7_2; + case 3: return SEG7_3; + case 4: return SEG7_4; + case 5: return SEG7_5; + case 6: return SEG7_6; + case 7: return SEG7_7; + case 8: return SEG7_8; + case 9: return SEG7_9; + default: return SEG7_DASH; // Return dash for invalid digits + } +} + +// Error codes (Evident specific, starting at 10100) +const int ERR_EVIDENT_OFFSET = 10100; +const int ERR_COMMAND_TIMEOUT = ERR_EVIDENT_OFFSET + 1; +const int ERR_NEGATIVE_ACK = ERR_EVIDENT_OFFSET + 2; +const int ERR_INVALID_RESPONSE = ERR_EVIDENT_OFFSET + 3; +const int ERR_NOT_IN_REMOTE_MODE = ERR_EVIDENT_OFFSET + 4; +const int ERR_DEVICE_NOT_AVAILABLE = ERR_EVIDENT_OFFSET + 5; +const int ERR_POSITION_UNKNOWN = ERR_EVIDENT_OFFSET + 6; +const int ERR_MONITOR_THREAD_FAILED = ERR_EVIDENT_OFFSET + 7; +const int ERR_PORT_NOT_SET = ERR_EVIDENT_OFFSET + 8; +const int ERR_PORT_CHANGE_FORBIDDEN = ERR_EVIDENT_OFFSET + 9; +const int ERR_CORRECTION_COLLAR_NOT_LINKED = ERR_EVIDENT_OFFSET + 10; +const int ERR_CORRECTION_COLLAR_LINK_FAILED = ERR_EVIDENT_OFFSET + 11; + +// Helper functions +inline std::string BuildCommand(const char* tag) +{ + std::ostringstream cmd; + cmd << tag; // Don't add TERMINATOR - SendSerialCommand adds it + return cmd.str(); +} + +inline std::string BuildCommand(const char* tag, int param1) +{ + std::ostringstream cmd; + cmd << tag << TAG_DELIMITER << param1; + return cmd.str(); +} + +inline std::string BuildCommand(const char* tag, int param1, int param2) +{ + std::ostringstream cmd; + cmd << tag << TAG_DELIMITER << param1 << DATA_DELIMITER << param2; + return cmd.str(); +} + +inline std::string BuildCommand(const char* tag, int param1, int param2, int param3) +{ + std::ostringstream cmd; + cmd << tag << TAG_DELIMITER << param1 << DATA_DELIMITER << param2 << DATA_DELIMITER << param3; + return cmd.str(); +} + +inline std::string BuildQuery(const char* tag) +{ + std::ostringstream cmd; + cmd << tag << "?"; + return cmd.str(); +} + +// Parse response helpers +inline bool IsPositiveAck(const std::string& response, const char* tag) +{ + std::string expected = std::string(tag) + " +"; + return response.find(expected) == 0; +} + +inline bool IsValidAnswer(const std::string& response, const char* tag) +{ + if (response.length() < 3) + return false; + if (response.substr(0, strlen(tag)) != tag) + return false; + if (response.substr(3, 1) == "!") + return false; + return true; +} + +inline bool IsNegativeAck(const std::string& response, const char* tag) +{ + std::string expected = std::string(tag) + " !"; + return response.find(expected) == 0; +} + +inline bool IsUnknown(const std::string& response) +{ + return response.find(" X") != std::string::npos; +} + +inline std::string ExtractTag(const std::string& response) +{ + size_t pos = response.find(TAG_DELIMITER); + if (pos == std::string::npos) + { + // No delimiter, might be tag-only response or tag with '?' + pos = response.find('?'); + if (pos == std::string::npos) + return response; + else + return response.substr(0, pos); + } + return response.substr(0, pos); +} + +inline std::vector ParseParameters(const std::string& response) +{ + std::vector params; + size_t tagEnd = response.find(TAG_DELIMITER); + if (tagEnd == std::string::npos) + return params; + + std::string dataStr = response.substr(tagEnd + 1); + std::istringstream iss(dataStr); + std::string param; + + while (std::getline(iss, param, DATA_DELIMITER)) + { + // Trim whitespace + size_t start = param.find_first_not_of(" \t\r\n"); + size_t end = param.find_last_not_of(" \t\r\n"); + if (start != std::string::npos && end != std::string::npos) + params.push_back(param.substr(start, end - start + 1)); + else if (start != std::string::npos) + params.push_back(param.substr(start)); + } + + return params; +} + +inline int ParseIntParameter(const std::string& param) +{ + // Handle empty string + if (param.empty()) + return -1; + + // Handle unknown/not-present indicators + if (param == "X" || param == "x") + return -1; + + // Handle acknowledgment characters (shouldn't be parsed as numbers) + if (param == "+" || param == "!") + return -1; + + // Try to parse as integer with exception handling + try + { + return std::stoi(param); + } + catch (const std::invalid_argument&) + { + // Not a valid integer + return -1; + } + catch (const std::out_of_range&) + { + // Number too large for int + return -1; + } +} + +inline long ParseLongParameter(const std::string& param) +{ + // Handle empty string + if (param.empty()) + return -1; + + // Handle unknown/not-present indicators + if (param == "X" || param == "x") + return -1; + + // Handle acknowledgment characters (shouldn't be parsed as numbers) + if (param == "+" || param == "!") + return -1; + + // Try to parse as long with exception handling + try + { + return std::stol(param); + } + catch (const std::invalid_argument&) + { + // Not a valid long + return -1; + } + catch (const std::out_of_range&) + { + // Number too large for long + return -1; + } +} + +// Helper function to check if position is within tolerance of target +inline bool IsAtTargetPosition(long currentPos, long targetPos, long tolerance) +{ + if (targetPos < 0) + return false; // No target set + + long diff = currentPos - targetPos; + if (diff < 0) + diff = -diff; // Absolute value + + return diff <= tolerance; +} + +} // namespace EvidentIX85 diff --git a/DeviceAdapters/EvidentIX85/Makefile.am b/DeviceAdapters/EvidentIX85/Makefile.am new file mode 100644 index 000000000..107c7f8b6 --- /dev/null +++ b/DeviceAdapters/EvidentIX85/Makefile.am @@ -0,0 +1,12 @@ + +AM_CXXFLAGS = $(MMDEVAPI_CXXFLAGS) $(BOOST_CPPFLAGS) -std=c++17 +deviceadapter_LTLIBRARIES = libmmgr_dal_EvidentIX85.la +libmmgr_dal_EvidentIX85_la_SOURCES = EvidentHub.cpp EvidentHub.h \ + EvidentIX85.cpp EvidentIX85.h \ + EvidentModel.cpp EvidentModel.h \ + EvidentProtocol.h \ + ../../MMDevice/MMDevice.h +libmmgr_dal_EvidentIX85_la_LDFLAGS = $(MMDEVAPI_LDFLAGS) +libmmgr_dal_EvidentIX85_la_LIBADD = $(MMDEVAPI_LIBADD) + +EXTRA_DIST = EvidentIX85.vcxproj diff --git a/DeviceAdapters/EvidentIX85Win/EvidentHubWin.cpp b/DeviceAdapters/EvidentIX85Win/EvidentHubWin.cpp new file mode 100644 index 000000000..9921a4193 --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentHubWin.cpp @@ -0,0 +1,3024 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentHubWin.cpp +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 microscope hub implementation +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#include "EvidentHubWin.h" +#include "ModuleInterface.h" +#include +#include +#include +#include + +using namespace EvidentIX85Win; + +const char* g_HubDeviceName = "IX85Win-Hub"; + +// Device names +extern const char* g_FocusDeviceName; +extern const char* g_NosepieceDeviceName; +extern const char* g_MagnificationDeviceName; +extern const char* g_LightPathDeviceName; +extern const char* g_CondenserTurretDeviceName; +extern const char* g_DIAShutterDeviceName; +extern const char* g_EPIShutter1DeviceName; +extern const char* g_EPIShutter2DeviceName; +extern const char* g_MirrorUnit1DeviceName; +extern const char* g_MirrorUnit2DeviceName; +extern const char* g_PolarizerDeviceName; +extern const char* g_DICPrismDeviceName; +extern const char* g_EPINDDeviceName; +extern const char* g_CorrectionCollarDeviceName; +extern const char* g_AutofocusDeviceName; +extern const char* g_OffsetLensDeviceName; +extern const char* g_ZDCVirtualOffsetDeviceName; +extern const char* g_ObjectiveSetupDeviceName; + +// Property names +const char* g_PropPort = "SerialPort"; +const char* g_PropAnswerTimeout = "AnswerTimeout"; +const char* g_PropDLLPath = "SDK_DLL_Path"; +extern const char* g_Keyword_Magnification; + +// Hand Switch (MCZ) property names +const char* g_PropHandSwitchJog = "HandSwitch-FocusJog"; +const char* g_PropHandSwitchSwitches = "HandSwitch-Switches"; +const char* g_PropHandSwitchCondenser = "HandSwitch-CondenserSwitch"; +const char* g_PropHandSwitchIndicators = "HandSwitch-Indicators"; + +// Hand Switch property values +const char* g_Disabled = "Disabled"; +const char* g_Enabled = "Enabled"; +const char* g_IndicatorNormal = "Normal"; +const char* g_IndicatorDark = "Dark"; + +EvidentHubWin::EvidentHubWin() : + initialized_(false), + port_(""), + dllPath_("C:\\Program Files\\Micro-Manager-2.0\\IX5_Library\\msl_pm_ix85.dll"), + answerTimeoutMs_(ANSWER_TIMEOUT_MS), + dllHandle_(NULL), + interfaceHandle_(nullptr), + pfnInitialize_(nullptr), + pfnEnumInterface_(nullptr), + pfnGetInterfaceInfo_(nullptr), + pfnGetPortName_(nullptr), + pfnOpenInterface_(nullptr), + pfnCloseInterface_(nullptr), + pfnSendCommand_(nullptr), + pfnRegisterCallback_(nullptr), + responseReady_(false), + rememberedDIABrightness_(255) +{ + InitializeDefaultErrorMessages(); + + // Custom error messages + SetErrorText(ERR_COMMAND_TIMEOUT, "Command timeout - no response from microscope"); + SetErrorText(ERR_NEGATIVE_ACK, "Microscope returned error (negative acknowledgement)"); + SetErrorText(ERR_INVALID_RESPONSE, "Invalid response from microscope"); + SetErrorText(ERR_NOT_IN_REMOTE_MODE, "Microscope not in remote mode"); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Device not available on this microscope"); + SetErrorText(ERR_POSITION_UNKNOWN, "Device position is unknown"); + SetErrorText(ERR_PORT_NOT_SET, "Serial port not set"); + SetErrorText(ERR_PORT_CHANGE_FORBIDDEN, "Cannot change serial port after initialization"); + + // SDK-specific error messages + SetErrorText(EvidentSDK::SDK_ERR_DLL_NOT_FOUND, "Evident SDK DLL not found - check DLL path"); + SetErrorText(EvidentSDK::SDK_ERR_DLL_INIT_FAILED, "SDK initialization failed"); + SetErrorText(EvidentSDK::SDK_ERR_FUNCTION_NOT_FOUND, "SDK function not found in DLL"); + SetErrorText(EvidentSDK::SDK_ERR_NO_INTERFACE, "No SDK interface found for selected port"); + SetErrorText(EvidentSDK::SDK_ERR_OPEN_FAILED, "Failed to open SDK interface"); + SetErrorText(EvidentSDK::SDK_ERR_SEND_FAILED, "SDK command send failed"); + SetErrorText(EvidentSDK::SDK_ERR_CALLBACK_FAILED, "SDK callback registration failed"); + + // Pre-initialization properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentHubWin::OnSerialPort); + CreateProperty(g_PropPort, "Undefined", MM::String, false, pAct, true); + + // Enumerate available COM ports on Windows + // This allows pre-initialization port selection without requiring + // MMCore SerialManager ports to be loaded first. + AddAllowedValue(g_PropPort, "Undefined"); + + for (int i = 1; i <= 64; i++) // Scan COM1-COM64 + { + std::ostringstream oss; + oss << "COM" << i; + std::string portName = oss.str(); + std::string portPath = "\\\\.\\" + portName; + + HANDLE hCom = CreateFile(portPath.c_str(), + GENERIC_READ | GENERIC_WRITE, + 0, // Exclusive access + NULL, // Default security + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL); + + if (hCom != INVALID_HANDLE_VALUE) + { + AddAllowedValue(g_PropPort, portName.c_str()); + CloseHandle(hCom); + } + } + + pAct = new CPropertyAction(this, &EvidentHubWin::OnDLLPath); + CreateProperty(g_PropDLLPath, dllPath_.c_str(), MM::String, false, pAct, true); + + pAct = new CPropertyAction(this, &EvidentHubWin::OnAnswerTimeout); + std::ostringstream os; + os << ANSWER_TIMEOUT_MS; + CreateProperty(g_PropAnswerTimeout, os.str().c_str(), MM::Integer, false, pAct, true); +} + +EvidentHubWin::~EvidentHubWin() +{ + Shutdown(); +} + +void EvidentHubWin::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, g_HubDeviceName); +} + +bool EvidentHubWin::Busy() +{ + return false; // Hub itself is never busy +} + +int EvidentHubWin::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + usedDevices_.clear(); + + // Start worker thread for command queue + workerRunning_ = true; + workerThread_ = std::thread(&EvidentHubWin::CommandWorkerThread, this); + + // Load Evident SDK DLL + int ret = LoadSDK(); + if (ret != DEVICE_OK) + return ret; + + // Enumerate and open SDK interface + ret = EnumerateAndOpenInterface(); + if (ret != DEVICE_OK) + { + UnloadSDK(); + return ret; + } + + // Switch to remote mode + ret = SetRemoteMode(); + if (ret != DEVICE_OK) + return ret; + + // Exit Setting mode (SDK enters it automatically after login) + ret = ExitSettingMode(); + if (ret != DEVICE_OK) + return ret; + + ret = GetUnit(unit_); + if (ret != DEVICE_OK) + return ret; + + LogMessage(("Microscope Version: " + version_).c_str(), false); + LogMessage(("Microscope Unit: " + unit_).c_str(), false); + + // Detect available devices + ret = DoDeviceDetection(); + if (ret != DEVICE_OK) + return ret; + + // Initialize MCU indicators if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + // Initialize nosepiece indicator (I1) + if (model_.IsDevicePresent(DeviceType_Nosepiece)) + { + long pos = model_.GetPosition(DeviceType_Nosepiece); + // Position will be 0 if unknown (not yet queried), display as unknown + UpdateNosepieceIndicator(pos == 0 ? -1 : static_cast(pos)); + } + else + { + // No nosepiece, display "---" + UpdateNosepieceIndicator(-1); + } + + // Initialize mirror unit indicator (I2) + if (model_.IsDevicePresent(DeviceType_MirrorUnit1)) + { + long pos = model_.GetPosition(DeviceType_MirrorUnit1); + // Position will be 0 if unknown (not yet queried), display as unknown + UpdateMirrorUnitIndicator(pos == 0 ? -1 : static_cast(pos), false); + } + else + { + // No mirror unit, display "---" + UpdateMirrorUnitIndicator(-1, false); + } + + // Enable encoder E1 for nosepiece control if nosepiece is present + if (model_.IsDevicePresent(DeviceType_Nosepiece)) + { + std::string cmd = BuildCommand(CMD_ENCODER1, 1); // Enable encoder + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + // Verify response is "E1 0" + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] == "0") + { + LogMessage("Encoder E1 enabled for nosepiece control", false); + } + else + { + LogMessage(("Unexpected response to E1 enable: " + response).c_str(), false); + } + } + else + { + LogMessage("Failed to enable encoder E1", false); + } + } + + // Enable encoder E2 for mirror unit control if mirror unit is present + if (model_.IsDevicePresent(DeviceType_MirrorUnit1)) + { + std::string cmd = BuildCommand(CMD_ENCODER2, 1); // Enable encoder + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + // Verify response is "E2 0" + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] == "0") + { + LogMessage("Encoder E2 enabled for mirror unit control", false); + } + else + { + LogMessage(("Unexpected response to E2 enable: " + response).c_str(), false); + } + } + else + { + LogMessage("Failed to enable encoder E2", false); + } + } + + // Enable encoder E3 for DIA brightness control if DIA shutter is present + if (model_.IsDevicePresent(DeviceType_DIAShutter)) + { + std::string cmd = BuildCommand(CMD_ENCODER3, 1); // Enable encoder + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + // Verify response is "E3 0" + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] == "0") + { + LogMessage("Encoder E3 enabled for DIA brightness control", false); + } + else + { + LogMessage(("Unexpected response to E3 enable: " + response).c_str(), false); + } + } + else + { + LogMessage("Failed to enable encoder E3", false); + } + } + + // Enable jog (focus) control if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_JOG, 1); // Enable jog control + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Jog (focus) control enabled on MCU", false); + } + else + { + LogMessage("Failed to enable jog control", false); + } + } + + // Enable MCU switches (S2) if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_MCZ_SWITCH, 1); // Enable switches + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("MCU switches (S2) enabled", false); + } + else + { + LogMessage("Failed to enable MCU switches", false); + } + } + + // Enable condenser switch (S1) if condenser is present + if (model_.IsDevicePresent(DeviceType_CondenserTurret)) + { + std::string cmd = BuildCommand(CMD_CONDENSER_SWITCH, 1); // Enable switches + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Condenser switches (S1) enabled", false); + } + else + { + LogMessage("Failed to enable condenser switches", false); + } + } + + // Enable indicators (I) with normal intensity + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_INDICATOR_CONTROL, 1); // 1 = Normal intensity + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("MCU indicators enabled (Normal)", false); + } + else + { + LogMessage("Failed to enable MCU indicators", false); + } + } + + // Enable objective dial request notification (NROB) if nosepiece is present + if (model_.IsDevicePresent(DeviceType_Nosepiece)) + { + std::string cmd = BuildCommand(CMD_NOSEPIECE_REQUEST_NOTIFY, 1); + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Objective dial notification (NROB) enabled", false); + } + else + { + LogMessage("Failed to enable objective dial notification", false); + } + } + + // Enable mirror dial request notification (NRMU) if mirror unit is present + if (model_.IsDevicePresent(DeviceType_MirrorUnit1)) + { + std::string cmd = BuildCommand(CMD_MIRROR_REQUEST_NOTIFY, 1); + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Mirror dial notification (NRMU) enabled", false); + } + else + { + LogMessage("Failed to enable mirror dial notification", false); + } + } + + // Initialize light path indicator (I4) + if (model_.IsDevicePresent(DeviceType_LightPath)) + { + long pos = model_.GetPosition(DeviceType_LightPath); + // Position will be 0 if unknown (not yet queried), display as unknown (all off) + UpdateLightPathIndicator(pos == 0 ? -1 : static_cast(pos), false); + } + else + { + // No light path, display all off + UpdateLightPathIndicator(-1, false); + } + + // Initialize EPI shutter 1 indicator (I5) + if (model_.IsDevicePresent(DeviceType_EPIShutter1)) + { + long state = model_.GetPosition(DeviceType_EPIShutter1); + // Position will be 0 if unknown (not yet queried), display as closed (I5 1) + UpdateEPIShutter1Indicator(state == 0 ? 0 : static_cast(state), false); + } + else + { + // No EPI shutter 1, display as closed + UpdateEPIShutter1Indicator(0, false); + } + + // Create Hand Switch control properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentHubWin::OnHandSwitchJog); + CreateProperty(g_PropHandSwitchJog, g_Enabled, MM::String, false, pAct); + AddAllowedValue(g_PropHandSwitchJog, g_Disabled); + AddAllowedValue(g_PropHandSwitchJog, g_Enabled); + + pAct = new CPropertyAction(this, &EvidentHubWin::OnHandSwitchSwitches); + CreateProperty(g_PropHandSwitchSwitches, g_Enabled, MM::String, false, pAct); + AddAllowedValue(g_PropHandSwitchSwitches, g_Disabled); + AddAllowedValue(g_PropHandSwitchSwitches, g_Enabled); + + if (model_.IsDevicePresent(DeviceType_CondenserTurret)) + { + pAct = new CPropertyAction(this, &EvidentHubWin::OnHandSwitchCondenser); + CreateProperty(g_PropHandSwitchCondenser, g_Enabled, MM::String, false, pAct); + AddAllowedValue(g_PropHandSwitchCondenser, g_Disabled); + AddAllowedValue(g_PropHandSwitchCondenser, g_Enabled); + } + + pAct = new CPropertyAction(this, &EvidentHubWin::OnHandSwitchIndicators); + CreateProperty(g_PropHandSwitchIndicators, g_IndicatorNormal, MM::String, false, pAct); + AddAllowedValue(g_PropHandSwitchIndicators, g_Disabled); + AddAllowedValue(g_PropHandSwitchIndicators, g_IndicatorNormal); + AddAllowedValue(g_PropHandSwitchIndicators, g_IndicatorDark); + } + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentHubWin::Shutdown() +{ + if (initialized_) + { + // Disable all active notifications BEFORE stopping monitoring thread + for (auto deviceType : availableDevices_) + { + // Disable notifications for devices that support them + switch (deviceType) + { + case DeviceType_Focus: + EnableNotification(CMD_FOCUS_NOTIFY, false); + break; + case DeviceType_Nosepiece: + EnableNotification(CMD_NOSEPIECE_NOTIFY, false); + break; + case DeviceType_Magnification: + EnableNotification(CMD_MAGNIFICATION_NOTIFY, false); + break; + // Add more as needed + default: + break; + } + } + + // Disable encoder E1 if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_ENCODER1, 0); // Disable encoder + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Encoder E1 disabled", true); + } + + else + { + LogMessage("Failed to disable encoder E1", true); + } + } + + // Disable encoder E2 if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_ENCODER2, 0); // Disable encoder + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Encoder E2 disabled", true); + } + else + { + LogMessage("Failed to disable encoder E2", true); + } + } + + // Disable encoder E3 if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_ENCODER3, 0); // Disable encoder + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Encoder E3 disabled", true); + } + else + { + LogMessage("Failed to disable encoder E3", true); + } + } + + // Disable jog control if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_JOG, 0); // Disable jog control + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Jog (focus) control disabled", true); + } + else + { + LogMessage("Failed to disable jog control", true); + } + } + + // Disable MCU switches (S2) if MCU is present + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_MCZ_SWITCH, 0); // Disable switches + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("MCU switches (S2) disabled", true); + } + else + { + LogMessage("Failed to disable MCU switches", true); + } + } + + // Disable condenser switches (S1) if condenser is present + if (model_.IsDevicePresent(DeviceType_CondenserTurret)) + { + std::string cmd = BuildCommand(CMD_CONDENSER_SWITCH, 0); // Disable switches + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("Condenser switches (S1) disabled", true); + } + else + { + LogMessage("Failed to disable condenser switches", true); + } + } + + // Disable indicators (I) + if (model_.IsDevicePresent(DeviceType_ManualControl)) + { + std::string cmd = BuildCommand(CMD_INDICATOR_CONTROL, 0); // 0 = Disable + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + LogMessage("MCU indicators disabled", true); + } + else + { + LogMessage("Failed to disable MCU indicators", true); + } + } + + // Switch back to local mode + std::string cmd = BuildCommand(CMD_LOGIN, 0); // 0 = Local mode + std::string response; + ExecuteCommand(cmd, response); + + // Stop worker thread + workerRunning_ = false; + queueCV_.notify_one(); + + // Wait for worker thread to finish + if (workerThread_.joinable()) + { + workerThread_.join(); + } + + // Clear any pending commands + { + std::lock_guard lock(queueMutex_); + while (!commandQueue_.empty()) + { + auto& task = commandQueue_.front(); + // Set exception on unfulfilled promises to prevent blocking + try + { + task.responsePromise.set_exception( + std::make_exception_ptr(std::runtime_error("Hub shutting down"))); + } + catch (...) + { + // Promise might already be fulfilled, ignore + } + commandQueue_.pop(); + } + } + + // Close SDK interface and unload DLL + if (interfaceHandle_ != nullptr) + { + if (pfnCloseInterface_ != nullptr) + { + pfnCloseInterface_(interfaceHandle_); + } + interfaceHandle_ = nullptr; + } + UnloadSDK(); + + initialized_ = false; + } + return DEVICE_OK; +} + +int EvidentHubWin::OnSerialPort(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 EvidentHubWin::OnDLLPath(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(dllPath_.c_str()); + } + else if (eAct == MM::AfterSet) + { + if (initialized_) + { + pProp->Set(dllPath_.c_str()); + return ERR_PORT_CHANGE_FORBIDDEN; + } + pProp->Get(dllPath_); + } + return DEVICE_OK; +} + +int EvidentHubWin::OnAnswerTimeout(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(answerTimeoutMs_); + } + else if (eAct == MM::AfterSet) + { + pProp->Get(answerTimeoutMs_); + } + return DEVICE_OK; +} + +int EvidentHubWin::OnHandSwitchJog(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + int enable = (value == g_Enabled) ? 1 : 0; + std::string cmd = BuildCommand(CMD_JOG, enable); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + if (!IsPositiveAck(response, CMD_JOG)) + return ERR_INVALID_RESPONSE; + } + return DEVICE_OK; +} + +int EvidentHubWin::OnHandSwitchSwitches(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + int enable = (value == g_Enabled) ? 1 : 0; + std::string cmd = BuildCommand(CMD_MCZ_SWITCH, enable); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + if (!IsPositiveAck(response, CMD_MCZ_SWITCH)) + return ERR_INVALID_RESPONSE; + } + return DEVICE_OK; +} + +int EvidentHubWin::OnHandSwitchCondenser(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + int enable = (value == g_Enabled) ? 1 : 0; + std::string cmd = BuildCommand(CMD_CONDENSER_SWITCH, enable); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + if (!IsPositiveAck(response, CMD_CONDENSER_SWITCH)) + return ERR_INVALID_RESPONSE; + } + return DEVICE_OK; +} + +int EvidentHubWin::OnHandSwitchIndicators(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + int mode = 0; // Disabled + if (value == g_IndicatorNormal) + mode = 1; + else if (value == g_IndicatorDark) + mode = 2; + std::string cmd = BuildCommand(CMD_INDICATOR_CONTROL, mode); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + if (!IsPositiveAck(response, CMD_INDICATOR_CONTROL)) + return ERR_INVALID_RESPONSE; + } + return DEVICE_OK; +} + +int EvidentHubWin::SetRemoteMode() +{ + std::string cmd = BuildCommand(CMD_LOGIN, 1); // 1 = Remote mode + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_LOGIN)) + return ERR_NOT_IN_REMOTE_MODE; + + return DEVICE_OK; +} + +int EvidentHubWin::SetSettingMode(bool enable) +{ + std::string cmd = BuildCommand(CMD_OPERATION_MODE, enable ? 1 : 0); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_OPERATION_MODE)) + { + LogMessage(("SetSettingMode failed, response: " + response).c_str(), false); + return ERR_INVALID_RESPONSE; + } + + LogMessage(enable ? "Entered Setting mode" : "Exited Setting mode", false); + return DEVICE_OK; +} + +int EvidentHubWin::EnterSettingMode() +{ + return SetSettingMode(true); +} + +int EvidentHubWin::ExitSettingMode() +{ + return SetSettingMode(false); +} + +int EvidentHubWin::GetUnit(std::string& unit) +{ + std::string cmd = BuildQuery(CMD_UNIT); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Parse response: "U IX5,..." + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + unit = params[0]; + return DEVICE_OK; + } + + return ERR_INVALID_RESPONSE; +} + +int EvidentHubWin::GetUnitDirect(std::string& unit) +{ + std::string cmd = BuildQuery(CMD_UNIT); + std::string response; + int ret = SendCommand(cmd); + if (ret != DEVICE_OK) + return ret; + ret = GetSerialAnswer(port_.c_str(), TERMINATOR, response); + if (ret != DEVICE_OK) + return ret; + + // Parse response: "U IX5,..." + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + unit = params[0]; + return DEVICE_OK; + } + + return ERR_INVALID_RESPONSE; +} + +int EvidentHubWin::ExecuteCommandInternal(const std::string& command, std::string& response) +{ + // Worker thread provides serialization, no mutex needed here + + // Retry logic for empty responses (device not ready) + const int MAX_RETRIES = 20; + const int RETRY_DELAY_MS = 50; // 50ms delay between retries + + for (int attempt = 0; attempt < MAX_RETRIES; attempt++) + { + // Reset response state BEFORE sending command to avoid race condition + // where callback fires before GetResponse can set responseReady_ = false + { + std::lock_guard responseLock(responseMutex_); + responseReady_ = false; + pendingResponse_.clear(); + } + + int ret = SendCommand(command); + if (ret != DEVICE_OK) + { + return ret; + } + + // Extract expected response tag from command + // First strip any trailing whitespace/terminators from the command + std::string cleanCommand = command; + size_t end = cleanCommand.find_last_not_of(" \t\r\n"); + if (end != std::string::npos) + { + cleanCommand = cleanCommand.substr(0, end + 1); + } + std::string expectedTag = ExtractTag(cleanCommand); + + ret = GetResponse(response, answerTimeoutMs_); + if (ret != DEVICE_OK) + { + return ret; + } + + // Verify response tag matches command tag + std::string responseTag = ExtractTag(response); + + if (responseTag == expectedTag) + { + if (!IsPositiveAck(response, expectedTag.c_str())) + { + // Check if device is busy (error code 70) + if (IsDeviceBusyError(response) && attempt < MAX_RETRIES - 1) + { + LogMessage(("Device " + expectedTag + " busy(attempt " + + std::to_string(attempt + 1) + "), retrying...").c_str(), true); + CDeviceUtils::SleepMs(RETRY_DELAY_MS); + continue; // Retry + } + } + } + else // if (responseTag != expectedTag) + { + // Received wrong response - this can happen if responses arrive out of order + LogMessage(("Warning: Expected response for '" + expectedTag + + "' but received '" + responseTag + "' (" + response + + "). Discarding and waiting for correct response.").c_str(), false); + + // Wait for the correct response (with remaining timeout) + ret = GetResponse(response, answerTimeoutMs_); + if (ret != DEVICE_OK) + { + return ret; + } + + // Check again + responseTag = ExtractTag(response); + if (responseTag != expectedTag) + { + LogMessage(("Error: Still did not receive expected response for '" + + expectedTag + "', got '" + responseTag + "' instead.").c_str(), false); + return ERR_INVALID_RESPONSE; + } + } + + return DEVICE_OK; + } + return DEVICE_OK; +} + +std::future> EvidentHubWin::ExecuteCommandAsync(const std::string& command) +{ + CommandTask task(command); + auto future = task.responsePromise.get_future(); + + { + std::lock_guard lock(queueMutex_); + commandQueue_.push(std::move(task)); + } + queueCV_.notify_one(); + + return future; +} + +int EvidentHubWin::ExecuteCommand(const std::string& command, std::string& response) +{ + // Submit to queue and wait for completion + auto future = ExecuteCommandAsync(command); + + // Block until response ready + auto result = future.get(); + + // Extract return code and response + int ret = result.first; + response = result.second; + + return ret; +} + +void EvidentHubWin::CommandWorkerThread() +{ + while (workerRunning_) + { + CommandTask task(""); + + // Wait for command in queue + { + std::unique_lock lock(queueMutex_); + queueCV_.wait(lock, [this] + { + return !commandQueue_.empty() || !workerRunning_; + }); + + if (!workerRunning_ && commandQueue_.empty()) + { + break; // Shutdown signal + } + + if (commandQueue_.empty()) + { + continue; + } + + // Move task out of queue + task = std::move(commandQueue_.front()); + commandQueue_.pop(); + } + + // Execute command (outside queue lock to allow new submissions) + std::string response; + + try + { + int ret = ExecuteCommandInternal(task.command, response); + task.responsePromise.set_value(std::make_pair(ret, response)); + } + catch (...) + { + task.responsePromise.set_exception(std::current_exception()); + } + } +} + +int EvidentHubWin::SendCommand(const std::string& command) +{ + // std::string cmdString = command + TERMINATOR; + LogMessage(("Sending: " + command).c_str(), true); + + // Verify SDK is initialized + if (interfaceHandle_ == nullptr || pfnSendCommand_ == nullptr) + { + LogMessage("SDK not initialized or interface not open", false); + return DEVICE_ERR; + } + + // Initialize SDK command structure (use member to stay valid for async callback) + // commandMutex_ must be held by caller to protect pendingCommand_ + EvidentSDK::InitCommand(pendingCommand_); + EvidentSDK::SetCommandString(pendingCommand_, command); + + // Send command via SDK + if (!pfnSendCommand_(interfaceHandle_, &pendingCommand_)) + { + LogMessage("SDK SendCommand failed, retrying...", false); + CDeviceUtils::SleepMs(50); + if (!pfnSendCommand_(interfaceHandle_, &pendingCommand_)) + { + LogMessage("SDK SendCommand failed again."); + // go nuclear, unload and reload interface + if (interfaceHandle_ != nullptr) + { + if (pfnCloseInterface_ != nullptr) + { + pfnCloseInterface_(interfaceHandle_); + } + interfaceHandle_ = nullptr; + } + this->EnumerateAndOpenInterface(); + if (!pfnSendCommand_(interfaceHandle_, &pendingCommand_)) + { + return EvidentSDK::SDK_ERR_SEND_FAILED; + } + } + } + + return DEVICE_OK; +} + +int EvidentHubWin::GetResponse(std::string& response, long timeoutMs) +{ + if (timeoutMs < 0) + timeoutMs = answerTimeoutMs_; + + // Wait for the callback to provide a response (responseReady_ already reset by ExecuteCommand) + std::unique_lock lock(responseMutex_); + + if (responseCV_.wait_for(lock, std::chrono::milliseconds(timeoutMs), + [this] { return responseReady_; })) + { + response = pendingResponse_; + LogMessage(("Received: " + response).c_str(), true); + return DEVICE_OK; + } + + return ERR_COMMAND_TIMEOUT; +} + +int EvidentHubWin::DoDeviceDetection() +{ + availableDevices_.clear(); + detectedDevicesByName_.clear(); + + // Use U command to detect device presence + // The U command returns a comma-separated list of unit codes + std::string cmd = BuildQuery(CMD_UNIT); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + LogMessage("Failed to query unit codes", false); + return ret; + } + + // Parse response: "U IX5,FRM,REA,LWUCDA,..." + std::vector unitCodes = ParseParameters(response); + + // Log all detected unit codes + std::ostringstream unitLog; + unitLog << "Unit codes detected: "; + for (size_t i = 0; i < unitCodes.size(); i++) + { + if (i > 0) unitLog << ", "; + unitLog << unitCodes[i]; + } + LogMessage(unitLog.str().c_str(), false); + + // Helper lambda to check if a unit code is present + auto hasUnit = [&unitCodes](const std::string& code) -> bool { + for (const auto& unit : unitCodes) + { + if (unit == code) return true; + } + return false; + }; + + // IX5 - Focus, Light Path, Correction Collar, Magnification + if (hasUnit("IX5")) + { + LogMessage("Detected IX5 unit (Focus, LightPath, CorrectionCollar, Magnification)"); + + // Focus + model_.SetDevicePresent(DeviceType_Focus, true); + availableDevices_.push_back(DeviceType_Focus); + detectedDevicesByName_.push_back(g_FocusDeviceName); + QueryFocus(); + + // Light Path + model_.SetDevicePresent(DeviceType_LightPath, true); + availableDevices_.push_back(DeviceType_LightPath); + detectedDevicesByName_.push_back(g_LightPathDeviceName); + QueryLightPath(); + + // Correction Collar + model_.SetDevicePresent(DeviceType_CorrectionCollar, true); + availableDevices_.push_back(DeviceType_CorrectionCollar); + detectedDevicesByName_.push_back(g_CorrectionCollarDeviceName); + model_.SetPosition(DeviceType_CorrectionCollar, 0); + + // Magnification + model_.SetDevicePresent(DeviceType_Magnification, true); + availableDevices_.push_back(DeviceType_Magnification); + detectedDevicesByName_.push_back(g_MagnificationDeviceName); + QueryMagnification(); + } + + // REA - Nosepiece + if (hasUnit("REA")) + { + LogMessage("Detected REA unit (Nosepiece)"); + model_.SetDevicePresent(DeviceType_Nosepiece, true); + availableDevices_.push_back(DeviceType_Nosepiece); + detectedDevicesByName_.push_back(g_NosepieceDeviceName); + QueryNosepiece(); + QueryObjectiveInfo(); + } + + // LWUCDA - Condenser Turret, DIA Aperture, Polarizer, DIA Shutter + if (hasUnit("LWUCDA")) + { + LogMessage("Detected LWUCDA unit (CondenserTurret, Polarizer, DIAShutter)"); + + // Polarizer + model_.SetDevicePresent(DeviceType_Polarizer, true); + availableDevices_.push_back(DeviceType_Polarizer); + detectedDevicesByName_.push_back(g_PolarizerDeviceName); + QueryPolarizer(); + + // Condenser Turret + model_.SetDevicePresent(DeviceType_CondenserTurret, true); + availableDevices_.push_back(DeviceType_CondenserTurret); + detectedDevicesByName_.push_back(g_CondenserTurretDeviceName); + QueryCondenserTurret(); + + // DIA Shutter + model_.SetDevicePresent(DeviceType_DIAShutter, true); + availableDevices_.push_back(DeviceType_DIAShutter); + detectedDevicesByName_.push_back(g_DIAShutterDeviceName); + QueryDIAShutter(); + } + + // DICTA - DIC Prism and Retardation + if (hasUnit("DICTA")) + { + LogMessage("Detected DICTA unit (DICPrism)"); + model_.SetDevicePresent(DeviceType_DICPrism, true); + availableDevices_.push_back(DeviceType_DICPrism); + detectedDevicesByName_.push_back(g_DICPrismDeviceName); + QueryDICPrism(); + } + + // RFACA.1 - Mirror Unit 1 and EPI Shutter 1 + if (hasUnit("RFACA.1")) + { + LogMessage("Detected RFACA.1 unit (MirrorUnit1, EPIShutter1)"); + + // Mirror Unit 1 + model_.SetDevicePresent(DeviceType_MirrorUnit1, true); + availableDevices_.push_back(DeviceType_MirrorUnit1); + detectedDevicesByName_.push_back(g_MirrorUnit1DeviceName); + QueryMirrorUnit1(); + + // EPI Shutter 1 + model_.SetDevicePresent(DeviceType_EPIShutter1, true); + availableDevices_.push_back(DeviceType_EPIShutter1); + detectedDevicesByName_.push_back(g_EPIShutter1DeviceName); + QueryEPIShutter1(); + } + + // RFACA.2 - Mirror Unit 2 and EPI Shutter 2 + if (hasUnit("RFACA.2")) + { + LogMessage("Detected RFACA.2 unit (MirrorUnit2, EPIShutter2)"); + + // Mirror Unit 2 + model_.SetDevicePresent(DeviceType_MirrorUnit2, true); + availableDevices_.push_back(DeviceType_MirrorUnit2); + detectedDevicesByName_.push_back(g_MirrorUnit2DeviceName); + QueryMirrorUnit2(); + + // EPI Shutter 2 + model_.SetDevicePresent(DeviceType_EPIShutter2, true); + availableDevices_.push_back(DeviceType_EPIShutter2); + detectedDevicesByName_.push_back(g_EPIShutter2DeviceName); + QueryEPIShutter2(); + } + + // MCZ - Manual Control Unit (Hand Switch) + // Note: Not added to availableDevices_ - properties are added to Hub device instead + if (hasUnit("MCZ")) + { + LogMessage("Detected MCZ unit (ManualControl/HandSwitch)"); + model_.SetDevicePresent(DeviceType_ManualControl, true); + } + + // ZDC - Autofocus and Offset Lens + if (hasUnit("ZDC")) + { + LogMessage("Detected ZDC unit (Autofocus, OffsetLens)"); + + // Autofocus (includes virtual offset + model_.SetDevicePresent(DeviceType_Autofocus, true); + availableDevices_.push_back(DeviceType_Autofocus); + detectedDevicesByName_.push_back(g_AutofocusDeviceName); + model_.SetDevicePresent(DeviceType_ZDCVirtualOffset, true); + availableDevices_.push_back(DeviceType_ZDCVirtualOffset); + detectedDevicesByName_.push_back(g_ZDCVirtualOffsetDeviceName); + + // Offset Lens + model_.SetDevicePresent(DeviceType_OffsetLens, true); + availableDevices_.push_back(DeviceType_OffsetLens); + detectedDevicesByName_.push_back(g_OffsetLensDeviceName); + + // Query initial offset lens position + QueryOffsetLens(); + } + + // TODO: U-AW - EPI? (needs clarification) + // TODO: FRM - unknown devices + + // Objective Setup - Always available (utility device for configuration) + LogMessage("Adding ObjectiveSetup utility device"); + detectedDevicesByName_.push_back(g_ObjectiveSetupDeviceName); + + std::ostringstream msg; + msg << "Discovered " << availableDevices_.size() << " devices"; + LogMessage(msg.str().c_str(), false); + + return DEVICE_OK; +} + +MM::DeviceDetectionStatus EvidentHubWin::DetectDevice(void) +{ + + if (initialized_) + return MM::CanCommunicate; + + // our property port_ should have been set to one of the valid ports + + + // all conditions must be satisfied... + MM::DeviceDetectionStatus result = MM::Misconfigured; + char answerTO[MM::MaxStrLength]; + + try + { + std::string portLowerCase = port_; + for( std::string::iterator its = portLowerCase.begin(); its != portLowerCase.end(); ++its) + { + *its = (char)tolower(*its); + } + if( 0< portLowerCase.length() && 0 != portLowerCase.compare("undefined") && 0 != portLowerCase.compare("unknown") ) + { + result = MM::CanNotCommunicate; + // record current port settings + GetCoreCallback()->GetDeviceProperty(port_.c_str(), "AnswerTimeout", answerTO); + + // device specific default communication parameters + GetCoreCallback()->SetDeviceProperty(port_.c_str(), MM::g_Keyword_BaudRate, "115200" ); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), MM::g_Keyword_StopBits, "1"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), MM::g_Keyword_Parity, "Even"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "Verbose", "0"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "AnswerTimeout", "5000.0"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "DelayBetweenCharsMs", "0"); + MM::Device* pS = GetCoreCallback()->GetDevice(this, port_.c_str()); + pS->Initialize(); + std::string unit; + int ret = GetUnitDirect(unit); + if (ret != DEVICE_OK || unit != "IX5") + { + pS->Shutdown(); + // always restore the AnswerTimeout to the default + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "AnswerTimeout", answerTO); + return result; + } + result = MM::CanCommunicate; + pS->Shutdown(); + // always restore the AnswerTimeout to the default + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "AnswerTimeout", answerTO); + + } + } + catch(...) + { + LogMessage("Exception in DetectDevice!",false); + } + return result; + +} + +int EvidentHubWin::DetectInstalledDevices() +{ + for (size_t i=0; i < detectedDevicesByName_.size(); i++) + { + MM::Device* pDev = ::CreateDevice(detectedDevicesByName_[i].c_str()); + if (pDev) + AddInstalledDevice(pDev); + } + + return DEVICE_OK; +} + +bool EvidentHubWin::IsDevicePresent(EvidentIX85Win::DeviceType type) const +{ + return model_.IsDevicePresent(type); +} + +std::string EvidentHubWin::GetDeviceVersion(EvidentIX85Win::DeviceType type) const +{ + return model_.GetDeviceVersion(type); +} + +int EvidentHubWin::EnableNotification(const char* cmd, bool enable) +{ + std::string command = BuildCommand(cmd, enable ? 1 : 0); + std::string response; + + int ret = ExecuteCommand(command, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, cmd)) + return ERR_NEGATIVE_ACK; + + return DEVICE_OK; +} + +// Device query implementations +int EvidentHubWin::QueryFocus() +{ + std::string cmd = BuildQuery(CMD_FOCUS_POSITION); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + long pos = ParseLongParameter(params[0]); + model_.SetPosition(DeviceType_Focus, pos); + model_.SetLimits(DeviceType_Focus, FOCUS_MIN_POS, FOCUS_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryNosepiece() +{ + std::string cmd = BuildQuery(CMD_NOSEPIECE); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_Nosepiece, pos); + model_.SetNumPositions(DeviceType_Nosepiece, NOSEPIECE_MAX_POS); + model_.SetLimits(DeviceType_Nosepiece, NOSEPIECE_MIN_POS, NOSEPIECE_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryObjectiveInfo() +{ + objectiveInfo_.clear(); + objectiveInfo_.resize(NOSEPIECE_MAX_POS); // 6 positions + + for (int pos = 1; pos <= NOSEPIECE_MAX_POS; pos++) + { + std::string cmd = BuildCommand(CMD_GET_OBJECTIVE, pos); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + LogMessage(("Failed to query objective info for position " + std::to_string(pos)).c_str(), false); + continue; + } + + std::vector params = ParseParameters(response); + if (params.size() < 11) + { + LogMessage(("Incomplete GOB response for position " + std::to_string(pos)).c_str(), false); + continue; + } + + EvidentIX85Win::ObjectiveInfo& info = objectiveInfo_[pos - 1]; + + // p1 is position (already known), p2 is name + info.name = params[1]; + + // p3: NA (0.00-2.00, N = indefinite) + if (params[2] != "N") + info.na = std::stod(params[2]); + else + info.na = -1.0; + + // p4: Magnification (0-200, N = indefinite) + if (params[3] != "N") + info.magnification = std::stoi(params[3]); + else + info.magnification = -1; + + // p5: Medium (1-5, N = indefinite) + if (params[4] != "N") + info.medium = std::stoi(params[4]); + else + info.medium = -1; + + // p6 is always 0, skip + + // p7: AS min (0-120, N/U = indefinite/unknown) + if (params[6] != "N" && params[6] != "U") + info.asMin = std::stoi(params[6]); + else + info.asMin = -1; + + // p8: AS max (0-120, N/U = indefinite/unknown) + if (params[7] != "N" && params[7] != "U") + info.asMax = std::stoi(params[7]); + else + info.asMax = -1; + + // p9: WD (0.01-25.00, N = indefinite) + if (params[8] != "N") + info.wd = std::stod(params[8]); + else + info.wd = -1.0; + + // p10: ZDC OneShot compatibility (0-3) + info.zdcOneShotCompat = std::stoi(params[9]); + + // p11: ZDC Continuous compatibility (0-3) + info.zdcContinuousCompat = std::stoi(params[10]); + + std::ostringstream msg; + msg << "Objective " << pos << ": " << info.name + << ", NA=" << info.na + << ", Mag=" << info.magnification + << "x, WD=" << info.wd << "mm"; + LogMessage(msg.str().c_str(), false); + } + + return DEVICE_OK; +} + +int EvidentHubWin::QueryMagnification() +{ + std::string cmd = BuildQuery(CMD_MAGNIFICATION); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_Magnification, pos); + model_.SetNumPositions(DeviceType_Magnification, MAGNIFICATION_MAX_POS); + model_.SetLimits(DeviceType_Magnification, MAGNIFICATION_MIN_POS, MAGNIFICATION_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryLightPath() +{ + std::string cmd = BuildQuery(CMD_LIGHT_PATH); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_LightPath, pos); + model_.SetNumPositions(DeviceType_LightPath, 4); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryCondenserTurret() +{ + std::string cmd = BuildQuery(CMD_CONDENSER_TURRET); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_CondenserTurret, pos); + model_.SetNumPositions(DeviceType_CondenserTurret, CONDENSER_TURRET_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryDIAAperture() +{ + std::string cmd = BuildQuery(CMD_DIA_APERTURE); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_DIAAperture, pos); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryDIAShutter() +{ + std::string cmd = BuildQuery(CMD_DIA_SHUTTER); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int state = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_DIAShutter, state); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryPolarizer() +{ + std::string cmd = BuildQuery(CMD_POLARIZER); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Note: This function is now only called after V7 confirms condenser unit is present + // May still return "X" on first query due to firmware bug, but that's okay - + // we'll just set position to 0 (Out) and device will work correctly + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 0) + { + model_.SetPosition(DeviceType_Polarizer, pos); + } + else + { + // Firmware returned "X", set default position (0 = Out) + model_.SetPosition(DeviceType_Polarizer, 0); + } + model_.SetNumPositions(DeviceType_Polarizer, POLARIZER_MAX_POS); + return DEVICE_OK; + } + + return DEVICE_OK; // Device present (confirmed by V7), just couldn't get position +} + +int EvidentHubWin::QueryDICPrism() +{ + std::string cmd = BuildQuery(CMD_DIC_PRISM); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Note: This function is now only called after V8 confirms DIC unit is present + // May still return "X" on first query due to firmware bug, but that's okay - + // we'll just set position to 0 and device will work correctly + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 0) + { + model_.SetPosition(DeviceType_DICPrism, pos); + } + else + { + // Firmware returned "X", set default position (0) + model_.SetPosition(DeviceType_DICPrism, 0); + } + model_.SetNumPositions(DeviceType_DICPrism, DIC_PRISM_MAX_POS); + return DEVICE_OK; + } + + return DEVICE_OK; // Device present (confirmed by V8), just couldn't get position +} + +int EvidentHubWin::QueryDICRetardation() +{ + std::string cmd = BuildQuery(CMD_DIC_RETARDATION); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_DICRetardation, pos); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryEPIShutter1() +{ + std::string cmd = BuildQuery(CMD_EPI_SHUTTER1); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int state = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_EPIShutter1, state); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryEPIShutter2() +{ + std::string cmd = BuildQuery(CMD_EPI_SHUTTER2); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int state = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_EPIShutter2, state); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryMirrorUnit1() +{ + std::string cmd = BuildQuery(CMD_MIRROR_UNIT1); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_MirrorUnit1, pos); + model_.SetNumPositions(DeviceType_MirrorUnit1, MIRROR_UNIT_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryMirrorUnit2() +{ + std::string cmd = BuildQuery(CMD_MIRROR_UNIT2); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_MirrorUnit2, pos); + model_.SetNumPositions(DeviceType_MirrorUnit2, MIRROR_UNIT_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryEPIND() +{ + std::string cmd = BuildQuery(CMD_EPI_ND); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_EPIND, pos); + model_.SetNumPositions(DeviceType_EPIND, EPIND_MAX_POS); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryRightPort() +{ + std::string cmd = BuildQuery(CMD_RIGHT_PORT); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_RightPort, pos); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryCorrectionCollar() +{ + std::string cmd = BuildQuery(CMD_CORRECTION_COLLAR); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_CorrectionCollar, pos); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::QueryOffsetLens() +{ + std::string cmd = BuildQuery(CMD_OFFSET_LENS_POSITION); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (IsUnknown(response)) + return ERR_DEVICE_NOT_AVAILABLE; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int pos = ParseIntParameter(params[0]); + model_.SetPosition(DeviceType_OffsetLens, pos); + return DEVICE_OK; + } + + return ERR_DEVICE_NOT_AVAILABLE; +} + +int EvidentHubWin::UpdateNosepieceIndicator(int position) +{ + // Check if MCU is present + if (!model_.IsDevicePresent(DeviceType_ManualControl)) + return DEVICE_OK; // Not an error, MCU just not present + + std::string cmd; + + // Position -1 means unknown, display "---" (three dashes) + if (position == -1 || position < 1 || position > 9) + { + // Three dashes: 0x01,0x01,0x01 + cmd = "I1 010101"; + } + else + { + // Single digit display - get 7-segment code + int code = Get7SegmentCode(position); + + // Format as hex string (2 digits) + std::ostringstream oss; + oss << "I1 " << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << code; + cmd = oss.str(); + } + + // Send command without waiting for response (to avoid deadlock when called from monitoring thread) + // The "I1 +" response will be consumed by the monitoring thread as a pseudo-notification + int ret = SendCommand(cmd); + if (ret != DEVICE_OK) + { + LogMessage(("Failed to send nosepiece indicator command: " + cmd).c_str()); + return ret; + } + + LogMessage(("Sent nosepiece indicator command: " + cmd).c_str(), true); + return DEVICE_OK; +} + +int EvidentHubWin::UpdateMirrorUnitIndicator(int position, bool async) +{ + // Check if MCU is present + if (!model_.IsDevicePresent(DeviceType_ManualControl)) + return DEVICE_OK; // Not an error, MCU just not present + + std::string cmd; + + // Position -1 means unknown, display "---" (three dashes) + if (position == -1 || position < 1 || position > 9) + { + // Three dashes: 0x01,0x01,0x01 + cmd = "I2 010101"; + } + else + { + // Single digit display - get 7-segment code + int code = Get7SegmentCode(position); + + // Format as hex string (2 digits) + std::ostringstream oss; + oss << "I2 " << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << code; + cmd = oss.str(); + } + + if (async) + { + auto future = ExecuteCommandAsync(cmd); + } + else + { + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Got a response, check if it's positive + if (!IsPositiveAck(response, "I2")) + { + return ERR_NEGATIVE_ACK; + } + } + + LogMessage(("Sent mirror unit indicator command async: " + cmd).c_str(), true); + return DEVICE_OK; +} + +int EvidentHubWin::UpdateLightPathIndicator(int position, bool async) +{ + // Check if MCU is present + if (!model_.IsDevicePresent(DeviceType_ManualControl)) + return DEVICE_OK; // Not an error, MCU just not present + + std::string cmd; + + // Map LightPath position (1-4) to I4 indicator value + // Position 1 (Left Port) -> I4 1 (camera) + // Position 2 (Binocular 50/50) -> I4 2 (50:50) + // Position 3 (Binocular 100%) -> I4 4 (eyepiece) + // Position 4 (Right Port) -> I4 0 (all off) + // Unknown -> I4 0 (all off) + + int i4Value; + if (position == 1) + i4Value = 1; // Left Port -> camera + else if (position == 2) + i4Value = 2; // 50:50 + else if (position == 3) + i4Value = 4; // Binocular 100% -> eyepiece + else + i4Value = 0; // Right Port or unknown -> all off + + std::ostringstream oss; + oss << "I4 " << i4Value; + cmd = oss.str(); + + if (async) + { + auto future = ExecuteCommandAsync(cmd); + LogMessage(("Sent Light Path indicator command async " + cmd).c_str(), true); + } + else + { + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Got a response, check if it's positive + if (!IsPositiveAck(response, "I4")) + { + return ERR_NEGATIVE_ACK; + } + + LogMessage(("Sent light path indicator command: " + cmd).c_str(), true); + } + return DEVICE_OK; +} + +int EvidentHubWin::UpdateEPIShutter1Indicator(int state, bool async) +{ + // Check if MCU is present + if (!model_.IsDevicePresent(DeviceType_ManualControl)) + return DEVICE_OK; // Not an error, MCU just not present + + std::string cmd; + + // Map EPI Shutter state to I5 indicator value + // State 0 (Closed) -> I5 1 + // State 1 (Open) -> I5 2 + // Unknown or other -> I5 1 (default to closed) + + int i5Value; + if (state == 1) + i5Value = 2; // Open -> 2 + else + i5Value = 1; // Closed or unknown -> 1 + + std::ostringstream oss; + oss << "I5 " << i5Value; + cmd = oss.str(); + + if (async) + { + auto future = ExecuteCommandAsync(cmd); + // we discard the future, since we do not want to block + LogMessage(("Sent EPI shutter indicator command async: " + cmd).c_str(), true); + } + else { + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Got a response, check if it's positive + if (!IsPositiveAck(response, CMD_EPI_SHUTTER1)) + { + return ERR_NEGATIVE_ACK; + } + + LogMessage(("Sent EPI shutter indicator command: " + cmd).c_str(), true); + } + return DEVICE_OK; +} + +void EvidentHubWin::NotifyMeasuredZOffsetChanged(long offsetSteps) +{ + // Notify EvidentAutofocus device if it's in use + auto afIt = usedDevices_.find(EvidentIX85Win::DeviceType_Autofocus); + if (afIt != usedDevices_.end() && afIt->second != nullptr) + { + EvidentAutofocus* afDevice = dynamic_cast(afIt->second); + if (afDevice) + { + afDevice->UpdateMeasuredZOffset(offsetSteps); + } + } + + auto zfIt = usedDevices_.find(EvidentIX85Win::DeviceType_ZDCVirtualOffset); + if (zfIt != usedDevices_.end() && zfIt->second != nullptr) + { + EvidentZDCVirtualOffset* zdcDevice = dynamic_cast(zfIt->second); + if (zdcDevice) + { + zdcDevice->UpdateMeasuredZOffset(offsetSteps); + } + } +} + +int EvidentHubWin::SetFocusPositionSteps(long steps) +{ + // Check if we're already at the target position + long currentPos = GetModel()->GetPosition(DeviceType_Focus); + bool alreadyAtTarget = IsAtTargetPosition(currentPos, steps, FOCUS_POSITION_TOLERANCE); + + // Set target position BEFORE sending command so notifications can check against it + GetModel()->SetTargetPosition(DeviceType_Focus, steps); + GetModel()->SetBusy(DeviceType_Focus, true); + + std::string cmd = BuildCommand(CMD_FOCUS_GOTO, static_cast(steps)); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + // Command failed - clear busy state + GetModel()->SetBusy(DeviceType_Focus, false); + return ret; + } + + if (!IsPositiveAck(response, CMD_FOCUS_GOTO)) + { + // Command rejected - clear busy state + GetModel()->SetBusy(DeviceType_Focus, false); + + // Check for specific error: position out of range + if (IsPositionOutOfRangeError(response)) + { + return ERR_POSITION_OUT_OF_RANGE; + } + return ERR_NEGATIVE_ACK; + } + + // If we're already at the target, firmware won't send notifications, so clear busy immediately + if (alreadyAtTarget) + { + GetModel()->SetBusy(DeviceType_Focus, false); + } + + // Command accepted - if not already at target, busy will be cleared by notification when target reached + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// Notification Processing +/////////////////////////////////////////////////////////////////////////////// + +void EvidentHubWin::ProcessNotification(const std::string& message) +{ + bool isCommandCompletion = false; + std::string tag = ExtractTag(message); + std::vector params = ParseParameters(message); + + // Update model based on notification + if (tag == CMD_FOCUS_NOTIFY && params.size() > 0) + { + long pos = ParseLongParameter(params[0]); + if (pos >= 0) + { + model_.SetPosition(DeviceType_Focus, pos); + + // Notify core callback of stage position change + auto it = usedDevices_.find(DeviceType_Focus); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Convert from 10nm units to micrometers + double positionUm = pos * FOCUS_STEP_SIZE_UM; + GetCoreCallback()->OnStagePositionChanged(it->second, positionUm); + } + + // Check if we've reached the target position (with tolerance for mechanical settling) + long targetPos = model_.GetTargetPosition(DeviceType_Focus); + if (IsAtTargetPosition(pos, targetPos, FOCUS_POSITION_TOLERANCE)) + { + model_.SetBusy(DeviceType_Focus, false); + LogMessage("Focus reached target position", true); + } + } + } + else if (tag == CMD_NOSEPIECE_NOTIFY && params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 0) + { + model_.SetPosition(DeviceType_Nosepiece, pos); + + // Note: MCU indicator I1 is updated automatically by SDK when using OBSEQ + + // Check if we've reached the target position + long targetPos = model_.GetTargetPosition(DeviceType_Nosepiece); + bool isExpectedChange = (targetPos >= 0 && pos == targetPos); + + std::ostringstream msg; + msg << "Nosepiece notification: pos=" << pos << ", targetPos=" << targetPos << ", isExpectedChange=" << isExpectedChange; + LogMessage(msg.str().c_str(), true); + + if (isExpectedChange) + { + LogMessage("Nosepiece reached target position, clearing busy flag", true); + model_.SetBusy(DeviceType_Nosepiece, false); + } + + // Only notify core of property changes for UNSOLICITED changes (manual controls) + // For commanded changes, the core already knows about the change + if (!isExpectedChange) + { + LogMessage("Unsolicited nosepiece change detected, notifying core", true); + auto it = usedDevices_.find(DeviceType_Nosepiece); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Convert from 1-based position to 0-based state value + int stateValue = pos - 1; + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_State, + CDeviceUtils::ConvertToString(stateValue)); + + // This should works since the nosepiece is a state device + // it would be safer to test the type first + char label[MM::MaxStrLength]; + int ret = ((MM::State*) it->second)->GetPositionLabel(stateValue, label); + if (ret == DEVICE_OK) + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_Label, label); + } + } + + } + } + else if (tag == CMD_MAGNIFICATION_NOTIFY && params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 0) + { + model_.SetPosition(DeviceType_Magnification, pos); + + std::ostringstream msg; + msg << "Magnification notification: pos=" << pos; + LogMessage(msg.str().c_str(), true); + + auto it = usedDevices_.find(DeviceType_Magnification); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Map position (1-based) to magnification value + const double magnifications[3] = {1.0, 1.6, 2.0}; + if (pos >= 1 && pos <= 3) + { + double magValue = magnifications[pos - 1]; + std::string logMsg = std::string("Magnification changed to ") + CDeviceUtils::ConvertToString(magValue); + LogMessage(logMsg.c_str(), true); + + GetCoreCallback()->OnPropertyChanged(it->second, g_Keyword_Magnification, + CDeviceUtils::ConvertToString(magValue)); + // Notify core that magnification has changed + GetCoreCallback()->OnMagnifierChanged(it->second); + } + else + { + std::ostringstream errMsg; + errMsg << "Magnification position " << pos << " out of range (expected 1-3)"; + LogMessage(errMsg.str().c_str(), false); + } + } + else + { + LogMessage("Magnification device not registered, cannot notify core", true); + } + } + } + else if (tag == CMD_OFFSET_LENS_NOTIFY && params.size() > 0) + { + // Handle offset lens position change notification + long pos = ParseLongParameter(params[0]); + if (pos >= OFFSET_LENS_MIN_POS && pos <= OFFSET_LENS_MAX_POS) + { + model_.SetPosition(DeviceType_OffsetLens, pos); + + // Notify core callback of stage position change + auto it = usedDevices_.find(DeviceType_OffsetLens); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Convert from steps to micrometers + double positionUm = pos * OFFSET_LENS_STEP_SIZE_UM; + GetCoreCallback()->OnStagePositionChanged(it->second, positionUm); + } + } + } + else if (tag == CMD_AF_STATUS && params.size() > 0) + { + // Handle AF status notification (NAFST) + if (params[0] != "X") + { + int status = ParseIntParameter(params[0]); + + // Update AF status in the autofocus device + auto it = usedDevices_.find(DeviceType_Autofocus); + if (it != usedDevices_.end() && it->second != nullptr) + { + EvidentAutofocus* afDevice = dynamic_cast(it->second); + if (afDevice != nullptr) + { + afDevice->UpdateAFStatus(status); + } + } + } + } + else if (tag == CMD_ENCODER1 && params.size() > 0) + { + // Encoder 1 controls nosepiece position + int delta = ParseIntParameter(params[0]); + if (delta == -1 || delta == 1) + { + // Get current nosepiece position + long currentPos = model_.GetPosition(DeviceType_Nosepiece); + + // If position is unknown (0), we can't handle encoder input + if (currentPos == 0) + { + LogMessage("E1 encoder input ignored - nosepiece position unknown", true); + return; + } + + // Calculate new position with wrapping + int newPos = static_cast(currentPos) + delta; + int maxPos = NOSEPIECE_MAX_POS; // 6 + + if (newPos < 1) + newPos = maxPos; // Wrap from 1 to max + else if (newPos > maxPos) + newPos = 1; // Wrap from max to 1 + + // Send OB command to change nosepiece position + std::string cmd = BuildCommand(CMD_NOSEPIECE, newPos); + std::ostringstream msg; + msg << "E1 encoder: moving nosepiece from " << currentPos << " to " << newPos; + LogMessage(msg.str().c_str(), true); + + // Send the command (fire-and-forget, the NOB notification will update the model) + auto future = ExecuteCommandAsync(cmd); + /* + int ret = SendCommand(cmd); + if (ret != DEVICE_OK) + { + LogMessage("Failed to send nosepiece command from encoder", false); + } + */ + } + } + else if (tag == CMD_ENCODER2 && params.size() > 0) + { + // Encoder 2 controls MirrorUnit1 position + int delta = ParseIntParameter(params[0]); + if (delta == -1 || delta == 1) + { + // Get current mirror unit position + long currentPos = model_.GetPosition(DeviceType_MirrorUnit1); + + // If position is unknown (0), we can't handle encoder input + if (currentPos == 0) + { + LogMessage("E2 encoder input ignored - mirror unit position unknown", true); + return; + } + + // Calculate new position with wrapping + int newPos = static_cast(currentPos) + delta; + int maxPos = MIRROR_UNIT_MAX_POS; // 6 + + if (newPos < 1) + newPos = maxPos; // Wrap from 1 to max + else if (newPos > maxPos) + newPos = 1; // Wrap from max to 1 + + // Send MU1 command to change mirror unit position + std::string cmd = BuildCommand(CMD_MIRROR_UNIT1, newPos); + std::ostringstream msg; + msg << "E2 encoder: moving mirror unit from " << currentPos << " to " << newPos; + LogMessage(msg.str().c_str(), true); + + // Response to the command can be read only after we exit this function, so do not + // wait for it here, but trust it will be successful. + auto future = ExecuteCommandAsync(cmd); + // Optimistically update model and indicator (mirror unit has no position change notifications) + model_.SetPosition(DeviceType_MirrorUnit1, newPos); + UpdateMirrorUnitIndicator(newPos, true); + + // Notify core callback of State property change + auto it = usedDevices_.find(DeviceType_MirrorUnit1); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Convert from 1-based position to 0-based state value + int stateValue = newPos - 1; + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_State, + CDeviceUtils::ConvertToString(stateValue)); + + // This should works since the MirrorUnit is a state device + // it would be safer to test the type first + char label[MM::MaxStrLength]; + int ret = ((MM::State*) it->second)->GetPositionLabel(stateValue, label); + if (ret == DEVICE_OK) + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_Label, label); + } + } + } + else if (tag == CMD_ENCODER3 && params.size() > 0) + { + // Encoder 3 controls DIA brightness + int delta = ParseIntParameter(params[0]); + // Fast turning can produce delta values from -9 to 9 (excluding 0) + if (delta >= -9 && delta <= 9 && delta != 0) + { + // Get current remembered brightness (not actual brightness) + int currentRemembered = rememberedDIABrightness_; + + // Calculate new remembered brightness (each encoder step changes brightness by 3) + int newRemembered = currentRemembered + (delta * 3); + + // Clamp to valid range 0-255 + if (newRemembered < 0) + newRemembered = 0; + else if (newRemembered > 255) + newRemembered = 255; + + // Always update remembered brightness + rememberedDIABrightness_ = newRemembered; + + std::ostringstream msg; + msg << "E3 encoder: changing DIA remembered brightness from " << currentRemembered << " to " << newRemembered; + LogMessage(msg.str().c_str(), true); + + // Only send DIL command if logical shutter is open (actual brightness > 0) + long currentActual = model_.GetPosition(DeviceType_DIABrightness); + if (currentActual > 0) + { + // Shutter is open: update actual lamp brightness + std::string cmd = BuildCommand(CMD_DIA_ILLUMINATION, newRemembered); + int ret = SendCommand(cmd); + if (ret == DEVICE_OK) + { + // Update model with new brightness + model_.SetPosition(DeviceType_DIABrightness, newRemembered); + } + else + { + LogMessage("Failed to send DIA brightness command from encoder", false); + } + } + // If shutter is closed, don't send DIL, don't update model + + // Always update Brightness property callback with remembered value + auto it = usedDevices_.find(DeviceType_DIAShutter); + if (it != usedDevices_.end() && it->second != nullptr) + { + GetCoreCallback()->OnPropertyChanged(it->second, "Brightness", + CDeviceUtils::ConvertToString(newRemembered)); + } + } + } + else if (tag == CMD_DIA_ILLUMINATION_NOTIFY && params.size() > 0) + { + // Handle DIA illumination (brightness) change notification + int brightness = ParseIntParameter(params[0]); + if (brightness >= 0 && brightness <= 255) + { + model_.SetPosition(DeviceType_DIABrightness, brightness); + + // Only update I3 indicator and Brightness property if shutter is open (brightness > 0) + // When closed (brightness = 0), user wants to continue seeing remembered brightness + if (brightness > 0) + { + // Shutter is open: update remembered brightness and property + rememberedDIABrightness_ = brightness; + + // Notify core callback of Brightness property change + auto it = usedDevices_.find(DeviceType_DIAShutter); + if (it != usedDevices_.end() && it->second != nullptr) + { + GetCoreCallback()->OnPropertyChanged(it->second, "Brightness", + CDeviceUtils::ConvertToString(brightness)); + } + } + // If brightness = 0 (closed), don't update I3 or Brightness property + // They should continue showing the remembered brightness value + } + } + else if (tag == CMD_MCZ_SWITCH && params.size() > 0) + { + // Handle MCU switch press notifications + // Parameter is a hex bitmask (0-7F) indicating which switch(es) were pressed + // Bit 0 (0x01): Switch 1 - Light Path cycle + // Bit 1 (0x02): Switch 2 - EPI Shutter toggle + // Bit 2 (0x04): Switch 3 - DIA on/off toggle + // Bits 3-6: Reserved for future use + + // Parse hex parameter + int switchMask = 0; + std::istringstream iss(params[0]); + iss >> std::hex >> switchMask; + + if (switchMask < 0 || switchMask > 0x7F) + { + LogMessage(("Invalid S2 switch mask: " + params[0]).c_str(), false); + return; + } + + std::ostringstream msg; + msg << "MCU switch pressed: 0x" << std::hex << std::uppercase << switchMask; + LogMessage(msg.str().c_str(), true); + + // Switch 1 (Bit 0): Light Path cycling + if (switchMask & 0x01) + { + if (model_.IsDevicePresent(DeviceType_LightPath)) + { + long currentPos = model_.GetPosition(DeviceType_LightPath); + // Cycle: 3 (Eyepiece) → 2 (50/50) → 1 (Left Port) → 4 (Right Port) → 3 (Eyepiece) + int newPos; + if (currentPos == 3) // Eyepiece → 50/50 + newPos = 2; + else if (currentPos == 2) // 50/50 → Left Port + newPos = 1; + else if (currentPos == 1) // Left Port → Right Port + newPos = 4; + else // Right Port or unknown → Eyepiece + newPos = 3; + + std::string cmd = BuildCommand(CMD_LIGHT_PATH, newPos); + auto future = ExecuteCommandAsync(cmd); + model_.SetPosition(DeviceType_LightPath, newPos); + UpdateLightPathIndicator(newPos, true); + + auto it = usedDevices_.find(DeviceType_LightPath); + if (it != usedDevices_.end() && it->second != nullptr) + { + int stateValue = newPos - 1; + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_State, + CDeviceUtils::ConvertToString(stateValue)); + + // This should works since the LightPath is a state device + // it would be safer to test the type first + char label[MM::MaxStrLength]; + int ret = ((MM::State*) it->second)->GetPositionLabel(stateValue, label); + if (ret == DEVICE_OK) + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_Label, label); + } + LogMessage(("Switch 1: Light path changed to position " + std::to_string(newPos)).c_str(), true); + } + } + + // Switch 2 (Bit 1): EPI Shutter toggle + if (switchMask & 0x02) + { + auto it = usedDevices_.find(DeviceType_EPIShutter1); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Get current state from model + long currentState = model_.GetPosition(DeviceType_EPIShutter1); + int newState = (currentState == 0) ? 1 : 0; + + // Send toggle command using SendCommand (not ExecuteCommand) to avoid deadlock + std::string cmd = BuildCommand(CMD_EPI_SHUTTER1, newState); + std::string response; + auto future = ExecuteCommandAsync(cmd); + model_.SetPosition(DeviceType_EPIShutter1, newState); + UpdateEPIShutter1Indicator(newState, true); + + // we can not easily check if we succeeded since the above function are asynchronous + // assume all is good... + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_State, + CDeviceUtils::ConvertToString(static_cast(newState))); + GetCoreCallback()->OnShutterOpenChanged(it->second, newState == 1); + } + } + + // Switch 3 (Bit 2): DIA on/off toggle (with brightness memory) + if (switchMask & 0x04) + { + auto it = usedDevices_.find(DeviceType_DIAShutter); + if (it != usedDevices_.end() && it->second != nullptr) + { + // Get current brightness from model to determine if logical shutter is open + long currentBrightness = model_.GetPosition(DeviceType_DIABrightness); + int newBrightness; + + if (currentBrightness > 0) + { + // Currently on: remember brightness and turn off + rememberedDIABrightness_ = static_cast(currentBrightness); + newBrightness = 0; + } + else + { + // Currently off: restore remembered brightness + newBrightness = rememberedDIABrightness_; + } + + // Send command using SendCommand (not ExecuteCommand) to avoid deadlock + std::string cmd = BuildCommand(CMD_DIA_ILLUMINATION, newBrightness); + int ret = SendCommand(cmd); + if (ret == DEVICE_OK) + { + model_.SetPosition(DeviceType_DIABrightness, newBrightness); + + // Only update State property (logical shutter), NOT Brightness property + // This keeps the Brightness property at its remembered value + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_State, + CDeviceUtils::ConvertToString(newBrightness > 0 ? 1L : 0L)); + GetCoreCallback()->OnShutterOpenChanged(it->second, newBrightness > 0); + + LogMessage(("Switch 3: DIA toggled to " + std::string(newBrightness > 0 ? "on" : "off")).c_str(), true); + } + else + { + LogMessage("Failed to toggle DIA from switch", false); + } + } + } + } + else if (tag == CMD_NOSEPIECE_REQUEST_NOTIFY && params.size() > 0) + { + // Handle objective dial request from MCZ + // When user turns the objective dial, we need to: + // 1. Disable S2 and JG + // 2. Execute the objective switch + // 3. Re-enable S2 and JG + int requestedPos = ParseIntParameter(params[0]); + if (requestedPos >= 1 && requestedPos <= NOSEPIECE_MAX_POS) + { + std::ostringstream msg; + msg << "Objective dial request: position " << requestedPos; + LogMessage(msg.str().c_str(), true); + + // Note: Responses come through notification callback when called from here, + // so we use SendCommand with small delays instead of ExecuteCommand + + // Disable MCZ control + std::string cmd = BuildCommand(CMD_MCZ_SWITCH, 0); + SendCommand(cmd); + Sleep(30); + + // Disable jog control + cmd = BuildCommand(CMD_JOG, 0); + SendCommand(cmd); + Sleep(30); + + // Execute objective switch using OBSEQ command (required when responding to NROB) + // Note: Don't update model/indicator here - let NOB notification handle that + cmd = BuildCommand(CMD_NOSEPIECE_SEQ, requestedPos); + SendCommand(cmd); + LogMessage(("Objective dial: sent OBSEQ " + std::to_string(requestedPos)).c_str(), true); + Sleep(30); + + // Re-enable MCZ control + cmd = BuildCommand(CMD_MCZ_SWITCH, 1); + SendCommand(cmd); + Sleep(30); + + // Re-enable jog control + cmd = BuildCommand(CMD_JOG, 1); + SendCommand(cmd); + } + } + else if (tag == CMD_MIRROR_REQUEST_NOTIFY && params.size() > 0) + { + // Handle mirror dial request from MCZ + // When user turns the mirror dial, we need to: + // 1. Disable S2 + // 2. Execute the mirror switch + // 3. Re-enable S2 + int requestedPos = ParseIntParameter(params[0]); + if (requestedPos >= 1 && requestedPos <= MIRROR_UNIT_MAX_POS) + { + std::ostringstream msg; + msg << "Mirror dial request: position " << requestedPos; + LogMessage(msg.str().c_str(), true); + + std::string response; + + // Disable MCZ control + std::string cmd = BuildCommand(CMD_MCZ_SWITCH, 0); + ExecuteCommandAsync(cmd); + + // Execute mirror switch + cmd = BuildCommand(CMD_MIRROR_UNIT1, requestedPos); + auto future = ExecuteCommandAsync(cmd); + // Update model and indicator + model_.SetPosition(DeviceType_MirrorUnit1, requestedPos); + UpdateMirrorUnitIndicator(requestedPos, true); + + // Notify core callback + auto it = usedDevices_.find(DeviceType_MirrorUnit1); + if (it != usedDevices_.end() && it->second != nullptr) + { + int stateValue = requestedPos - 1; + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_State, + CDeviceUtils::ConvertToString(stateValue)); + + char label[MM::MaxStrLength]; + int ret = ((MM::State*) it->second)->GetPositionLabel(stateValue, label); + if (ret == DEVICE_OK) + GetCoreCallback()->OnPropertyChanged(it->second, MM::g_Keyword_Label, label); + } + + LogMessage(("Mirror dial: Async switched to position " + std::to_string(requestedPos)).c_str(), true); + + // Re-enable MCZ control + cmd = BuildCommand(CMD_MCZ_SWITCH, 1); + ExecuteCommandAsync(cmd); + } + } + else if (tag == CMD_EPI_SHUTTER1 && params.size() > 0 && params[0] == "+") + { + // this was not a notification, but a command completion: + LogMessage("Command completion for EPI Shutter 1 received", true); + isCommandCompletion = true; + } + else if (tag == CMD_EPI_SHUTTER2 && params.size() > 0 && params[0] == "+") + { + // this was not a notification, but a command completion: + LogMessage("Command completion for EPI Shutter 2 received", true); + isCommandCompletion = true; + } + else if (tag == CMD_MIRROR_UNIT1 && params.size() > 0 && params[0] == "+") + { + // this was not a notification, but a command completion: + LogMessage("Command completion for EPI Shutter 2 received", true); + isCommandCompletion = true; + } + else if (tag == CMD_MIRROR_UNIT2 && params.size() > 0 && params[0] == "+") + { + // this was not a notification, but a command completion: + LogMessage("Command completion for EPI Shutter 2 received", true); + isCommandCompletion = true; + } + else { + // Add more notification handlers as needed + LogMessage(("Unhandled notification: " + message).c_str(), true); + } + + if (isCommandCompletion) + { + // Signal waiting thread with the response + { + std::lock_guard lock(responseMutex_); + pendingResponse_ = message; + responseReady_ = true; + } + responseCV_.notify_one(); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// SDK DLL Management +/////////////////////////////////////////////////////////////////////////////// + +int EvidentHubWin::LoadSDK() +{ + LogMessage("Loading Evident SDK DLL...", false); + + // Build DLL path - check if user configured a custom path + std::string dllFullPath; + if (!dllPath_.empty()) + { + dllFullPath = dllPath_; + LogMessage(("Using custom DLL path: " + dllFullPath).c_str(), false); + } + else + { + // Default: try to find DLL in standard locations + // First try relative to the adapter DLL location + dllFullPath = "msl_pm_ix85.dll"; + LogMessage("Using default DLL path: msl_pm_ix85.dll", false); + } + + // Load the DLL + dllHandle_ = LoadLibraryExA(dllFullPath.c_str(), NULL, LOAD_WITH_ALTERED_SEARCH_PATH); + if (dllHandle_ == NULL) + { + DWORD err = GetLastError(); + std::ostringstream msg; + msg << "Failed to load SDK DLL: " << dllFullPath << " (error " << err << ")"; + LogMessage(msg.str().c_str(), false); + return EvidentSDK::SDK_ERR_DLL_NOT_FOUND; + } + + LogMessage("SDK DLL loaded successfully", false); + + // Get function pointers + pfnInitialize_ = (EvidentSDK::fn_MSL_PM_Initialize)GetProcAddress(dllHandle_, "MSL_PM_Initialize"); + pfnEnumInterface_ = (EvidentSDK::fn_MSL_PM_EnumInterface)GetProcAddress(dllHandle_, "MSL_PM_EnumInterface"); + pfnGetInterfaceInfo_ = (EvidentSDK::fn_MSL_PM_GetInterfaceInfo)GetProcAddress(dllHandle_, "MSL_PM_GetInterfaceInfo"); + pfnGetPortName_ = (EvidentSDK::fn_MSL_PM_GetPortName)GetProcAddress(dllHandle_, "MSL_PM_GetPortName"); + pfnOpenInterface_ = (EvidentSDK::fn_MSL_PM_OpenInterface)GetProcAddress(dllHandle_, "MSL_PM_OpenInterface"); + pfnCloseInterface_ = (EvidentSDK::fn_MSL_PM_CloseInterface)GetProcAddress(dllHandle_, "MSL_PM_CloseInterface"); + pfnSendCommand_ = (EvidentSDK::fn_MSL_PM_SendCommand)GetProcAddress(dllHandle_, "MSL_PM_SendCommand"); + pfnRegisterCallback_ = (EvidentSDK::fn_MSL_PM_RegisterCallback)GetProcAddress(dllHandle_, "MSL_PM_RegisterCallback"); + + // Verify all function pointers were found + if (!pfnInitialize_ || !pfnEnumInterface_ || !pfnGetInterfaceInfo_ || + !pfnGetPortName_ || !pfnOpenInterface_ || !pfnCloseInterface_ || + !pfnSendCommand_ || !pfnRegisterCallback_) + { + LogMessage("Failed to find one or more SDK functions in DLL", false); + UnloadSDK(); + return EvidentSDK::SDK_ERR_FUNCTION_NOT_FOUND; + } + + LogMessage("All SDK function pointers resolved", false); + + // Initialize the SDK + int result = pfnInitialize_(); + if (result != 0) + { + std::ostringstream msg; + msg << "SDK initialization failed with code: " << result; + LogMessage(msg.str().c_str(), false); + UnloadSDK(); + return EvidentSDK::SDK_ERR_DLL_INIT_FAILED; + } + + LogMessage("SDK initialized successfully", false); + return DEVICE_OK; +} + +int EvidentHubWin::UnloadSDK() +{ + LogMessage("Unloading Evident SDK...", false); + + // Clear function pointers + pfnInitialize_ = nullptr; + pfnEnumInterface_ = nullptr; + pfnGetInterfaceInfo_ = nullptr; + pfnGetPortName_ = nullptr; + pfnOpenInterface_ = nullptr; + pfnCloseInterface_ = nullptr; + pfnSendCommand_ = nullptr; + pfnRegisterCallback_ = nullptr; + + // Unload DLL + if (dllHandle_ != NULL) + { + FreeLibrary(dllHandle_); + dllHandle_ = NULL; + LogMessage("SDK DLL unloaded", false); + } + + return DEVICE_OK; +} + +int EvidentHubWin::EnumerateAndOpenInterface() +{ + LogMessage("Enumerating SDK interfaces...", false); + + // Enumerate available interfaces + int numInterfaces = pfnEnumInterface_(); + if (numInterfaces <= 0) + { + LogMessage("No SDK interfaces found", false); + return EvidentSDK::SDK_ERR_NO_INTERFACE; + } + + std::ostringstream msg; + msg << "Found " << numInterfaces << " interface(s)"; + LogMessage(msg.str().c_str(), false); + + // Try to find and open the interface + void* pInterface = nullptr; + std::string selectedPort; + + for (int i = 0; i < numInterfaces; i++) + { + void* pTempInterface = nullptr; + pfnGetInterfaceInfo_(i, &pTempInterface); + if (pTempInterface == nullptr) + { + continue; + } + + // Get port name + wchar_t portName[256] = {0}; + if (!pfnGetPortName_(pTempInterface, portName)) + { + continue; + } + + // Convert wide string to narrow string + char narrowPort[256]; + size_t converted = 0; + wcstombs_s(&converted, narrowPort, sizeof(narrowPort), portName, _TRUNCATE); + std::string portStr(narrowPort); + + std::ostringstream ifaceMsg; + ifaceMsg << "Interface " << i << ": " << portStr; + LogMessage(ifaceMsg.str().c_str(), false); + + // If user specified a port, match it; otherwise use first available + if (port_.empty() || port_ == "Undefined" || portStr.find(port_) != std::string::npos) + { + pInterface = pTempInterface; + selectedPort = portStr; + break; + } + } + + if (pInterface == nullptr) + { + LogMessage("No matching SDK interface found for port", false); + return EvidentSDK::SDK_ERR_NO_INTERFACE; + } + + // Open the interface + if (!pfnOpenInterface_(pInterface)) + { + LogMessage("Failed to open SDK interface", false); + return EvidentSDK::SDK_ERR_OPEN_FAILED; + } + + interfaceHandle_ = pInterface; + port_ = selectedPort; + + std::ostringstream openMsg; + openMsg << "Opened SDK interface for port: " << selectedPort; + LogMessage(openMsg.str().c_str(), false); + + // Register callbacks + if (!pfnRegisterCallback_(interfaceHandle_, + CommandCallbackStatic, + NotifyCallbackStatic, + ErrorCallbackStatic, + this)) + { + LogMessage("Failed to register SDK callbacks", false); + pfnCloseInterface_(interfaceHandle_); + interfaceHandle_ = nullptr; + return EvidentSDK::SDK_ERR_CALLBACK_FAILED; + } + + LogMessage("SDK callbacks registered successfully", false); + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// SDK Callback Handlers +/////////////////////////////////////////////////////////////////////////////// + +int CALLBACK EvidentHubWin::CommandCallbackStatic(ULONG /* MsgId */, ULONG /* wParam */, ULONG /* lParam */, + PVOID pv, PVOID pContext, PVOID /* pCaller */) +{ + EvidentHubWin* pHub = static_cast(pContext); + if (pHub != nullptr) + { + EvidentSDK::MDK_MSL_CMD* pCmd = static_cast(pv); + return pHub->OnCommandComplete(pCmd); + } + return 0; +} + +int CALLBACK EvidentHubWin::NotifyCallbackStatic(ULONG /* MsgId */, ULONG /* wParam */, ULONG /* lParam */, + PVOID pv, PVOID pContext, PVOID /* pCaller */) +{ + EvidentHubWin* pHub = static_cast(pContext); + if (pHub != nullptr && pv != nullptr) + { + // For notifications, pv is a null-terminated string, not MDK_MSL_CMD* + const char* notificationStr = static_cast(pv); + return pHub->OnNotification(notificationStr); + } + return 0; +} + +int CALLBACK EvidentHubWin::ErrorCallbackStatic(ULONG /* MsgId */, ULONG /* wParam */, ULONG /* lParam */, + PVOID pv, PVOID pContext, PVOID /* pCaller */) +{ + EvidentHubWin* pHub = static_cast(pContext); + if (pHub != nullptr) + { + EvidentSDK::MDK_MSL_CMD* pCmd = static_cast(pv); + return pHub->OnError(pCmd); + } + return 0; +} + +int EvidentHubWin::OnCommandComplete(EvidentSDK::MDK_MSL_CMD* pCmd) +{ + if (pCmd == nullptr) + return 0; + + // Extract response from the command structure + std::string response = EvidentSDK::GetResponseString(*pCmd); + if (response == "") + return 0; + + // Signal waiting thread with the response + { + std::lock_guard lock(responseMutex_); + pendingResponse_ = response; + responseReady_ = true; + } + responseCV_.notify_one(); + + return 0; +} + +int EvidentHubWin::OnNotification(const char* notificationStr) +{ + if (notificationStr == nullptr || notificationStr[0] == '\0') + return 0; + + std::string notification(notificationStr); + LogMessage(("Notification received: " + notification).c_str(), true); + + // Process notification + ProcessNotification(notification); + + return 0; +} + +int EvidentHubWin::OnError(EvidentSDK::MDK_MSL_CMD* pCmd) +{ + if (pCmd == nullptr) + return 0; + + std::ostringstream msg; + msg << "SDK Error callback - Result: " << pCmd->m_Result << " Status: " << pCmd->m_Status; + LogMessage(msg.str().c_str(), false); + + // Signal waiting thread with error + { + std::lock_guard lock(responseMutex_); + pendingResponse_ = ""; // Empty response indicates error + responseReady_ = true; + } + responseCV_.notify_one(); + + return 0; +} diff --git a/DeviceAdapters/EvidentIX85Win/EvidentHubWin.h b/DeviceAdapters/EvidentIX85Win/EvidentHubWin.h new file mode 100644 index 000000000..6d6cb7973 --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentHubWin.h @@ -0,0 +1,226 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentHubWin.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85Win microscope hub (SDK-based) +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include "DeviceBase.h" +#include "EvidentModelWin.h" +#include "EvidentProtocolWin.h" +#include "EvidentIX85Win.h" +#include "EvidentSDK.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Command task structure for worker thread queue +struct CommandTask +{ + std::string command; + std::promise> responsePromise; + + CommandTask(std::string cmd) : command(std::move(cmd)) {} + + // Move-only type (promise is not copyable) + CommandTask(CommandTask&&) = default; + CommandTask& operator=(CommandTask&&) = default; + CommandTask(const CommandTask&) = delete; + CommandTask& operator=(const CommandTask&) = delete; +}; + +class EvidentHubWin : public HubBase +{ +public: + EvidentHubWin(); + ~EvidentHubWin(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + bool SupportsDeviceDetection(void) { return true; } + MM::DeviceDetectionStatus DetectDevice(void); + // Hub API + int DetectInstalledDevices(); + + // Action handlers + int OnSerialPort(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnDLLPath(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnAnswerTimeout(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnHandSwitchJog(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnHandSwitchSwitches(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnHandSwitchCondenser(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnHandSwitchIndicators(MM::PropertyBase* pProp, MM::ActionType eAct); + + // Hub interface for devices to access state + EvidentIX85Win::MicroscopeModel* GetModel() { return &model_; } + + // Command execution (thread-safe with worker thread queue) + std::future> ExecuteCommandAsync(const std::string& command); + int ExecuteCommand(const std::string& command, std::string& response); + int SendCommand(const std::string& command); + int GetResponse(std::string& response, long timeoutMs = -1); + + // Device discovery + bool IsDevicePresent(EvidentIX85Win::DeviceType type) const; + std::string GetDeviceVersion(EvidentIX85Win::DeviceType type) const; + + // Objective lens information + const std::vector& GetObjectiveInfo() const { return objectiveInfo_; } + + // Notification control + int EnableNotification(const char* cmd, bool enable); + + void RegisterDeviceAsUsed(EvidentIX85Win::DeviceType type, MM::Device* device) { usedDevices_[type] = device;}; + void UnRegisterDeviceAsUsed(EvidentIX85Win::DeviceType type) { usedDevices_.erase(type); }; + + int UpdateMirrorUnitIndicator(int position, bool async); + int UpdateLightPathIndicator(int position, bool async); + int UpdateEPIShutter1Indicator(int state, bool async); + + // DIA brightness memory for logical shutter + int GetRememberedDIABrightness() const { return rememberedDIABrightness_; } + void SetRememberedDIABrightness(int brightness) { rememberedDIABrightness_ = brightness; } + + // Measured Z-offset notification (when autofocus measures the offset) + void NotifyMeasuredZOffsetChanged(long offsetSteps); + + int SetFocusPositionSteps(long position); + +private: + // Initialization helpers + int SetRemoteMode(); + int SetSettingMode(bool enable); + int EnterSettingMode(); + int ExitSettingMode(); + int GetUnit(std::string& unit); + int GetUnitDirect(std::string& unit); + int ClearPort(); + int DoDeviceDetection(); + int QueryObjectiveInfo(); + + // Worker thread for command queue + void CommandWorkerThread(); + int ExecuteCommandInternal(const std::string& command, std::string& response); + + // Device query helpers + int QueryFocus(); + int QueryNosepiece(); + int QueryMagnification(); + int QueryLightPath(); + int QueryCondenserTurret(); + int QueryDIAAperture(); + int QueryDIAShutter(); + int QueryPolarizer(); + int QueryDICPrism(); + int QueryDICRetardation(); + int QueryEPIShutter1(); + int QueryEPIShutter2(); + int QueryMirrorUnit1(); + int QueryMirrorUnit2(); + int QueryEPIND(); + int QueryRightPort(); + int QueryCorrectionCollar(); + int QueryManualControl(); + int QueryOffsetLens(); + + // Manual Control Unit (MCU) helpers + int UpdateNosepieceIndicator(int position); + + // SDK DLL management + int LoadSDK(); + int UnloadSDK(); + int EnumerateAndOpenInterface(); + + // Notification processing (called from SDK callbacks) + void ProcessNotification(const std::string& message); + bool IsNotificationTag(const std::string& message) const; + + // SDK Callback handlers (static - route to instance methods) + static int CALLBACK CommandCallbackStatic(ULONG MsgId, ULONG wParam, ULONG lParam, + PVOID pv, PVOID pContext, PVOID pCaller); + static int CALLBACK NotifyCallbackStatic(ULONG MsgId, ULONG wParam, ULONG lParam, + PVOID pv, PVOID pContext, PVOID pCaller); + static int CALLBACK ErrorCallbackStatic(ULONG MsgId, ULONG wParam, ULONG lParam, + PVOID pv, PVOID pContext, PVOID pCaller); + + // Instance callback handlers + int OnCommandComplete(EvidentSDK::MDK_MSL_CMD* pCmd); + int OnNotification(const char* notificationStr); + int OnError(EvidentSDK::MDK_MSL_CMD* pCmd); + + // Member variables + bool initialized_; + std::string port_; + std::string dllPath_; // Configurable SDK DLL path + long answerTimeoutMs_; + EvidentIX85Win::MicroscopeModel model_; + + // SDK DLL handles + HMODULE dllHandle_; + void* interfaceHandle_; // Opaque SDK interface handle + + // SDK function pointers + EvidentSDK::fn_MSL_PM_Initialize pfnInitialize_; + EvidentSDK::fn_MSL_PM_EnumInterface pfnEnumInterface_; + EvidentSDK::fn_MSL_PM_GetInterfaceInfo pfnGetInterfaceInfo_; + EvidentSDK::fn_MSL_PM_GetPortName pfnGetPortName_; + EvidentSDK::fn_MSL_PM_OpenInterface pfnOpenInterface_; + EvidentSDK::fn_MSL_PM_CloseInterface pfnCloseInterface_; + EvidentSDK::fn_MSL_PM_SendCommand pfnSendCommand_; + EvidentSDK::fn_MSL_PM_RegisterCallback pfnRegisterCallback_; + + // Command synchronization (keep pattern - callbacks signal instead of monitor thread) + mutable std::mutex commandMutex_; // Protects command sending (deprecated - worker thread serializes) + std::mutex responseMutex_; // Protects response handling + std::condition_variable responseCV_; + std::string pendingResponse_; + bool responseReady_; + EvidentSDK::MDK_MSL_CMD pendingCommand_; // Must be member to stay valid for async callback + + // Command queue for worker thread + std::queue commandQueue_; + std::mutex queueMutex_; + std::condition_variable queueCV_; + std::thread workerThread_; + std::atomic workerRunning_; + + // State + std::string version_; + std::string unit_; + std::vector availableDevices_; + std::vector detectedDevicesByName_; + std::vector objectiveInfo_; // Objective lens info for positions 1-6 + + // MCU switch state + int rememberedDIABrightness_; // Remembered brightness for DIA switch toggle + + // Child devices + std::map usedDevices_; +}; diff --git a/DeviceAdapters/EvidentIX85Win/EvidentIX85Win.cpp b/DeviceAdapters/EvidentIX85Win/EvidentIX85Win.cpp new file mode 100644 index 000000000..3674f409c --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentIX85Win.cpp @@ -0,0 +1,5258 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentIX85.cpp +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 microscope device implementations +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#include "EvidentIX85Win.h" +#include "EvidentObjectiveSetup.h" +#include "ModuleInterface.h" +#include +#include + +using namespace EvidentIX85Win; + +// External hub name +extern const char* g_HubDeviceName; + +// Device names +const char* g_FocusDeviceName = "IX85-Focus"; +const char* g_NosepieceDeviceName = "IX85-Nosepiece"; +const char* g_MagnificationDeviceName = "IX85-Magnification"; +const char* g_LightPathDeviceName = "IX85-LightPath"; +const char* g_CondenserTurretDeviceName = "IX85-CondenserTurret"; +const char* g_DIAShutterDeviceName = "IX85-DIAShutter"; +const char* g_EPIShutter1DeviceName = "IX85-EPIShutter1"; +const char* g_EPIShutter2DeviceName = "IX85-EPIShutter2"; +const char* g_MirrorUnit1DeviceName = "IX85-MirrorUnit1"; +const char* g_MirrorUnit2DeviceName = "IX85-MirrorUnit2"; +const char* g_PolarizerDeviceName = "IX85-Polarizer"; +const char* g_DICPrismDeviceName = "IX85-DICPrism"; +const char* g_EPINDDeviceName = "IX85-EPIND"; +const char* g_CorrectionCollarDeviceName = "IX85-CorrectionCollar"; +const char* g_AutofocusDeviceName = "IX85-Autofocus"; +const char* g_OffsetLensDeviceName = "IX85-OffsetLens"; +const char* g_ZDCVirtualOffsetDeviceName = "IX85-ZDCVirtualOffset"; +const char* g_ObjectiveSetupDeviceName = "IX85-ObjectiveSetup"; + +// Property Names +const char* g_Keyword_Magnification = "Magnification"; + +/////////////////////////////////////////////////////////////////////////////// +// MODULE_API - Exported MMDevice interface +/////////////////////////////////////////////////////////////////////////////// + +MODULE_API void InitializeModuleData() +{ + RegisterDevice(g_HubDeviceName, MM::HubDevice, "Evident IX85 Hub"); + RegisterDevice(g_FocusDeviceName, MM::StageDevice, "Evident IX85 Focus Drive"); + RegisterDevice(g_NosepieceDeviceName, MM::StateDevice, "Evident IX85 Nosepiece"); + RegisterDevice(g_MagnificationDeviceName, MM::MagnifierDevice, "Evident IX85 Magnification Changer"); + RegisterDevice(g_LightPathDeviceName, MM::StateDevice, "Evident IX85 Light Path"); + RegisterDevice(g_CondenserTurretDeviceName, MM::StateDevice, "Evident IX85 Condenser Turret"); + RegisterDevice(g_DIAShutterDeviceName, MM::ShutterDevice, "Evident IX85 DIA Shutter"); + RegisterDevice(g_EPIShutter1DeviceName, MM::ShutterDevice, "Evident IX85 EPI Shutter 1"); + RegisterDevice(g_EPIShutter2DeviceName, MM::ShutterDevice, "Evident IX85 EPI Shutter 2"); + RegisterDevice(g_MirrorUnit1DeviceName, MM::StateDevice, "Evident IX85 Mirror Unit 1"); + RegisterDevice(g_MirrorUnit2DeviceName, MM::StateDevice, "Evident IX85 Mirror Unit 2"); + RegisterDevice(g_PolarizerDeviceName, MM::StateDevice, "Evident IX85 Polarizer"); + RegisterDevice(g_DICPrismDeviceName, MM::StateDevice, "Evident IX85 DIC Prism"); + RegisterDevice(g_EPINDDeviceName, MM::StateDevice, "Evident IX85 EPI ND Filter"); + RegisterDevice(g_CorrectionCollarDeviceName, MM::StageDevice, "Evident IX85 Correction Collar"); + RegisterDevice(g_AutofocusDeviceName, MM::AutoFocusDevice, "Evident IX85 ZDC Autofocus"); + RegisterDevice(g_OffsetLensDeviceName, MM::StageDevice, "Evident IX85 Offset Lens"); + RegisterDevice(g_ObjectiveSetupDeviceName, MM::GenericDevice, "Evident IX85 Objective Setup"); +} + +MODULE_API MM::Device* CreateDevice(const char* deviceName) +{ + if (deviceName == nullptr) + return nullptr; + + if (strcmp(deviceName, g_HubDeviceName) == 0) + return new EvidentHubWin(); + else if (strcmp(deviceName, g_FocusDeviceName) == 0) + return new EvidentFocus(); + else if (strcmp(deviceName, g_NosepieceDeviceName) == 0) + return new EvidentNosepiece(); + else if (strcmp(deviceName, g_MagnificationDeviceName) == 0) + return new EvidentMagnification(); + else if (strcmp(deviceName, g_LightPathDeviceName) == 0) + return new EvidentLightPath(); + else if (strcmp(deviceName, g_CondenserTurretDeviceName) == 0) + return new EvidentCondenserTurret(); + else if (strcmp(deviceName, g_DIAShutterDeviceName) == 0) + return new EvidentDIAShutter(); + else if (strcmp(deviceName, g_EPIShutter1DeviceName) == 0) + return new EvidentEPIShutter1(); + else if (strcmp(deviceName, g_EPIShutter2DeviceName) == 0) + return new EvidentEPIShutter2(); + else if (strcmp(deviceName, g_MirrorUnit1DeviceName) == 0) + return new EvidentMirrorUnit1(); + else if (strcmp(deviceName, g_MirrorUnit2DeviceName) == 0) + return new EvidentMirrorUnit2(); + else if (strcmp(deviceName, g_PolarizerDeviceName) == 0) + return new EvidentPolarizer(); + else if (strcmp(deviceName, g_DICPrismDeviceName) == 0) + return new EvidentDICPrism(); + else if (strcmp(deviceName, g_EPINDDeviceName) == 0) + return new EvidentEPIND(); + else if (strcmp(deviceName, g_CorrectionCollarDeviceName) == 0) + return new EvidentCorrectionCollar(); + else if (strcmp(deviceName, g_AutofocusDeviceName) == 0) + return new EvidentAutofocus(); + else if (strcmp(deviceName, g_OffsetLensDeviceName) == 0) + return new EvidentOffsetLens(); + else if (strcmp(deviceName, g_ZDCVirtualOffsetDeviceName) == 0) + return new EvidentZDCVirtualOffset(); + else if (strcmp(deviceName, g_ObjectiveSetupDeviceName) == 0) + return new EvidentObjectiveSetup(); + + return nullptr; +} + +MODULE_API void DeleteDevice(MM::Device* pDevice) +{ + delete pDevice; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentFocus - Focus Drive Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentFocus::EvidentFocus() : + initialized_(false), + name_(g_FocusDeviceName), + stepSizeUm_(FOCUS_STEP_SIZE_UM) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Focus drive not available on this microscope"); + SetErrorText(ERR_POSITION_OUT_OF_RANGE, "Requested focus position is out of range"); + + // Parent ID for hub + CreateHubIDProperty(); +} + +EvidentFocus::~EvidentFocus() +{ + Shutdown(); +} + +void EvidentFocus::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentFocus::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_Focus)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Create propertiesn + CPropertyAction* pAct = new CPropertyAction(this, &EvidentFocus::OnPosition); + int ret = CreateProperty(MM::g_Keyword_Position, "0.0", MM::Float, false, pAct); + if (ret != DEVICE_OK) + return ret; + + pAct = new CPropertyAction(this, &EvidentFocus::OnSpeed); + ret = CreateProperty("Speed (um/s)", "30.0", MM::Float, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Enable active notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + return ret; + + // Register with hub so notification handler can call OnStagePositionChanged + hub->RegisterDeviceAsUsed(DeviceType_Focus, this); + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentFocus::Shutdown() +{ + if (initialized_) + { + EnableNotifications(false); + + // Unregister from hub + EvidentHubWin* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_Focus); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentFocus::Busy() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return false; + + return hub->GetModel()->IsBusy(DeviceType_Focus); +} + +int EvidentFocus::SetPositionUm(double pos) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert μm to 10nm units + long steps = static_cast(pos / stepSizeUm_); + + // Clamp to limits + if (steps < FOCUS_MIN_POS) steps = FOCUS_MIN_POS; + if (steps > FOCUS_MAX_POS) steps = FOCUS_MAX_POS; + + return hub->SetFocusPositionSteps(steps); +} + +int EvidentFocus::GetPositionUm(double& pos) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + long steps = hub->GetModel()->GetPosition(DeviceType_Focus); + if (steps < 0) // Unknown + return ERR_POSITION_UNKNOWN; + + pos = steps * stepSizeUm_; + return DEVICE_OK; +} + +int EvidentFocus::SetPositionSteps(long steps) +{ + return SetPositionUm(steps * stepSizeUm_); +} + +int EvidentFocus::GetPositionSteps(long& steps) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + steps = hub->GetModel()->GetPosition(DeviceType_Focus); + if (steps < 0) + return ERR_POSITION_UNKNOWN; + + return DEVICE_OK; +} + +int EvidentFocus::SetOrigin() +{ + // Not supported by IX85 + return DEVICE_UNSUPPORTED_COMMAND; +} + +int EvidentFocus::GetLimits(double& lower, double& upper) +{ + lower = FOCUS_MIN_POS * stepSizeUm_; + upper = FOCUS_MAX_POS * stepSizeUm_; + return DEVICE_OK; +} + +int EvidentFocus::OnPosition(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + double pos; + int ret = GetPositionUm(pos); + if (ret != DEVICE_OK) + return ret; + pProp->Set(pos); + } + else if (eAct == MM::AfterSet) + { + double pos; + pProp->Get(pos); + int ret = SetPositionUm(pos); + if (ret != DEVICE_OK) + return ret; + } + return DEVICE_OK; +} + +int EvidentFocus::OnSpeed(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + // Query current speed + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string cmd = BuildQuery(CMD_FOCUS_SPEED); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() >= 2) + { + long highSpeed = ParseLongParameter(params[1]); // High speed in 10nm/s + double speedUmPerSec = (highSpeed * stepSizeUm_); + pProp->Set(speedUmPerSec); + } + } + else if (eAct == MM::AfterSet) + { + double speedUmPerSec; + pProp->Get(speedUmPerSec); + + // Convert to 10nm/s + long highSpeed = static_cast(speedUmPerSec / stepSizeUm_); + long initialSpeed = highSpeed / 10; // Default: 10% of high speed + long acceleration = highSpeed * 5; // Default acceleration + + std::string cmd = BuildCommand(CMD_FOCUS_SPEED, + static_cast(initialSpeed), + static_cast(highSpeed), + static_cast(acceleration)); + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_FOCUS_SPEED)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentFocus::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentFocus::EnableNotifications(bool enable) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + return hub->EnableNotification(CMD_FOCUS_NOTIFY, enable); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentNosepiece - Nosepiece Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentNosepiece::EvidentNosepiece() : + initialized_(false), + name_(g_NosepieceDeviceName), + numPos_(NOSEPIECE_MAX_POS), + nearLimits_(NOSEPIECE_MAX_POS, FOCUS_MAX_POS), // Initialize with max values + parfocalPositions_(NOSEPIECE_MAX_POS, 0), // Initialize with zeros + parfocalEnabled_(false), + escapeDistance_(3) // Default to 3.0 mm +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Nosepiece not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentNosepiece::~EvidentNosepiece() +{ + Shutdown(); +} + +void EvidentNosepiece::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentNosepiece::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_Nosepiece)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_Nosepiece); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentNosepiece::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels using objective names from hub + const std::vector& objectives = hub->GetObjectiveInfo(); + for (unsigned int i = 0; i < numPos_; i++) + { + if (i < objectives.size() && !objectives[i].name.empty()) + { + SetPositionLabel(i, objectives[i].name.c_str()); + } + else + { + std::ostringstream label; + label << "Position-" << (i + 1); + SetPositionLabel(i, label.str().c_str()); + } + } + + // Create read-only properties for current objective info + pAct = new CPropertyAction(this, &EvidentNosepiece::OnObjectiveNA); + ret = CreateProperty("Objective-NA", "", MM::String, true, pAct); + if (ret != DEVICE_OK) + return ret; + + pAct = new CPropertyAction(this, &EvidentNosepiece::OnObjectiveMagnification); + ret = CreateProperty("Objective-Magnification", "", MM::String, true, pAct); + if (ret != DEVICE_OK) + return ret; + + pAct = new CPropertyAction(this, &EvidentNosepiece::OnObjectiveMedium); + ret = CreateProperty("Objective-Medium", "", MM::String, true, pAct); + if (ret != DEVICE_OK) + return ret; + + pAct = new CPropertyAction(this, &EvidentNosepiece::OnObjectiveWD); + ret = CreateProperty("Objective-WD", "", MM::String, true, pAct); + if (ret != DEVICE_OK) + return ret; + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_Nosepiece); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Query focus near limits from microscope + ret = QueryNearLimits(); + if (ret != DEVICE_OK) + { + LogMessage("Warning: Failed to query near limits, using defaults"); + // Don't fail initialization, just use default values + } + + // Create Focus-Near-Limit-um read-only property + pAct = new CPropertyAction(this, &EvidentNosepiece::OnNearLimit); + ret = CreateProperty("Focus-Near-Limit-um", "0.0", MM::Float, true, pAct); + if (ret != DEVICE_OK) + return ret; + + // Create Set-Focus-Near-Limit action property + pAct = new CPropertyAction(this, &EvidentNosepiece::OnSetNearLimit); + ret = CreateProperty("Set-Focus-Near-Limit", "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue("Set-Focus-Near-Limit", ""); + AddAllowedValue("Set-Focus-Near-Limit", "Set"); + AddAllowedValue("Set-Focus-Near-Limit", "Clear"); + + // Query parfocal settings from microscope + ret = QueryParfocalSettings(); + if (ret != DEVICE_OK) + { + LogMessage("Warning: Failed to query parfocal settings, using defaults"); + // Don't fail initialization, just use default values + } + + // Create Parfocal-Position-um read-only property + pAct = new CPropertyAction(this, &EvidentNosepiece::OnParfocalPosition); + ret = CreateProperty("Parfocal-Position-um", "0.0", MM::Float, true, pAct); + if (ret != DEVICE_OK) + return ret; + + // Create Set-Parfocal-Position action property + pAct = new CPropertyAction(this, &EvidentNosepiece::OnSetParfocalPosition); + ret = CreateProperty("Set-Parfocal-Position", "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue("Set-Parfocal-Position", ""); + AddAllowedValue("Set-Parfocal-Position", "Set"); + AddAllowedValue("Set-Parfocal-Position", "Clear"); + + // Create Parfocal-Enabled property + pAct = new CPropertyAction(this, &EvidentNosepiece::OnParfocalEnabled); + ret = CreateProperty("Parfocal-Enabled", "Disabled", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue("Parfocal-Enabled", "Disabled"); + AddAllowedValue("Parfocal-Enabled", "Enabled"); + + // Query focus escape distance + std::string escCmd = BuildQuery(CMD_FOCUS_ESCAPE); + std::string escResponse; + ret = hub->ExecuteCommand(escCmd, escResponse); + if (ret == DEVICE_OK) + { + std::vector escParams = ParseParameters(escResponse); + if (escParams.size() > 0) + { + escapeDistance_ = ParseIntParameter(escParams[0]); + } + } + + // Create Focus-Escape-Distance property + pAct = new CPropertyAction(this, &EvidentNosepiece::OnEscapeDistance); + ret = CreateProperty("Focus-Escape-Distance", "3.0 mm", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue("Focus-Escape-Distance", "0.0 mm"); + AddAllowedValue("Focus-Escape-Distance", "1.0 mm"); + AddAllowedValue("Focus-Escape-Distance", "2.0 mm"); + AddAllowedValue("Focus-Escape-Distance", "3.0 mm"); + AddAllowedValue("Focus-Escape-Distance", "4.0 mm"); + AddAllowedValue("Focus-Escape-Distance", "5.0 mm"); + AddAllowedValue("Focus-Escape-Distance", "6.0 mm"); + AddAllowedValue("Focus-Escape-Distance", "7.0 mm"); + AddAllowedValue("Focus-Escape-Distance", "8.0 mm"); + AddAllowedValue("Focus-Escape-Distance", "9.0 mm"); + + // Enable notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + return ret; + + // Register with hub so notification handler can notify property changes + hub->RegisterDeviceAsUsed(DeviceType_Nosepiece, this); + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentNosepiece::Shutdown() +{ + if (initialized_) + { + EnableNotifications(false); + + // Unregister from hub + EvidentHubWin* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_Nosepiece); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentNosepiece::Busy() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return false; + + return hub->GetModel()->IsBusy(DeviceType_Nosepiece); +} + +unsigned long EvidentNosepiece::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentNosepiece::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + long pos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + if (pos < 0) + return ERR_POSITION_UNKNOWN; + + // Convert from 1-based to 0-based + pProp->Set(pos - 1); + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + long targetPos = pos + 1; + + // Set target position BEFORE sending command so notifications can check against it + hub->GetModel()->SetTargetPosition(DeviceType_Nosepiece, targetPos); + + // Check if already at target position + long currentPos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + if (currentPos == targetPos) + { + // Already at target, no need to move + hub->GetModel()->SetBusy(DeviceType_Nosepiece, false); + return DEVICE_OK; + } + + hub->GetModel()->SetBusy(DeviceType_Nosepiece, true); + + // Use OBSEQ command - SDK handles focus escape and parfocality automatically + std::string cmd = BuildCommand(CMD_NOSEPIECE_SEQ, static_cast(targetPos)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + // Command failed - clear busy state + hub->GetModel()->SetBusy(DeviceType_Nosepiece, false); + return ret; + } + + if (!IsPositiveAck(response, CMD_NOSEPIECE_SEQ)) + { + // Command rejected - clear busy state + hub->GetModel()->SetBusy(DeviceType_Nosepiece, false); + return ERR_NEGATIVE_ACK; + } + + // Command accepted - busy state already set, will be cleared by notification when target reached + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentNosepiece::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentNosepiece::EnableNotifications(bool enable) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + return hub->EnableNotification(CMD_NOSEPIECE_NOTIFY, enable); +} + +int EvidentNosepiece::OnObjectiveNA(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (hub) + { + long pos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + const std::vector& objectives = hub->GetObjectiveInfo(); + if (pos >= 1 && pos <= (long)objectives.size()) + { + double na = objectives[pos - 1].na; + if (na >= 0) + { + std::ostringstream ss; + ss << std::fixed << std::setprecision(2) << na; + pProp->Set(ss.str().c_str()); + } + else + { + pProp->Set("N/A"); + } + } + } + } + return DEVICE_OK; +} + +int EvidentNosepiece::OnObjectiveMagnification(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (hub) + { + long pos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + const std::vector& objectives = hub->GetObjectiveInfo(); + if (pos >= 1 && pos <= (long)objectives.size()) + { + int mag = objectives[pos - 1].magnification; + if (mag >= 0) + { + pProp->Set((long)mag); + } + else + { + pProp->Set("N/A"); + } + } + } + } + return DEVICE_OK; +} + +int EvidentNosepiece::OnObjectiveMedium(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (hub) + { + long pos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + const std::vector& objectives = hub->GetObjectiveInfo(); + if (pos >= 1 && pos <= (long)objectives.size()) + { + int medium = objectives[pos - 1].medium; + const char* mediumStr = "N/A"; + switch (medium) + { + case 1: mediumStr = "Dry"; break; + case 2: mediumStr = "Water"; break; + case 3: mediumStr = "Oil"; break; + case 4: mediumStr = "Silicone Oil"; break; + case 5: mediumStr = "Silicone Gel"; break; + } + pProp->Set(mediumStr); + } + } + } + return DEVICE_OK; +} + +int EvidentNosepiece::OnObjectiveWD(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (hub) + { + long pos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + const std::vector& objectives = hub->GetObjectiveInfo(); + if (pos >= 1 && pos <= (long)objectives.size()) + { + double wd = objectives[pos - 1].wd; + if (wd >= 0) + { + std::ostringstream ss; + ss << std::fixed << std::setprecision(2) << wd << " mm"; + pProp->Set(ss.str().c_str()); + } + else + { + pProp->Set("N/A"); + } + } + } + } + return DEVICE_OK; +} + +int EvidentNosepiece::OnNearLimit(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (hub) + { + long pos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + if (pos >= 1 && pos <= (long)nearLimits_.size()) + { + // Get near limit for current objective position (convert 1-based to 0-based) + long nearLimitSteps = nearLimits_[pos - 1]; + // Convert steps to micrometers + double nearLimitUm = nearLimitSteps * FOCUS_STEP_SIZE_UM; + pProp->Set(nearLimitUm); + } + else + { + pProp->Set(0.0); + } + } + } + return DEVICE_OK; +} + +int EvidentNosepiece::OnSetNearLimit(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + + if (value == "Set") + { + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Get current nosepiece position + long nosepiecePos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + if (nosepiecePos < 1 || nosepiecePos > (long)nearLimits_.size()) + return ERR_INVALID_PARAMETER; + + // Check if focus device is available + if (!hub->IsDevicePresent(DeviceType_Focus)) + { + LogMessage("Focus device not available, cannot set near limit"); + pProp->Set(""); // Reset to empty + return ERR_DEVICE_NOT_AVAILABLE; + } + + // Get current focus position + long focusPos = hub->GetModel()->GetPosition(DeviceType_Focus); + if (focusPos < 0) + { + LogMessage("Focus position unknown, cannot set near limit"); + pProp->Set(""); // Reset to empty + return ERR_POSITION_UNKNOWN; + } + + // Update near limit for current objective (convert 1-based to 0-based) + nearLimits_[nosepiecePos - 1] = focusPos; + + // Build NL command with all 6 positions: "NL p1,p2,p3,p4,p5,p6" + std::ostringstream cmd; + cmd << CMD_FOCUS_NEAR_LIMIT << TAG_DELIMITER; + for (size_t i = 0; i < nearLimits_.size(); i++) + { + if (i > 0) + cmd << DATA_DELIMITER; + cmd << nearLimits_[i]; + } + + // Execute command + std::string response; + int ret = hub->ExecuteCommand(cmd.str(), response); + if (ret != DEVICE_OK) + { + pProp->Set(""); // Reset to empty + return ret; + } + + if (!IsPositiveAck(response, CMD_FOCUS_NEAR_LIMIT)) + { + pProp->Set(""); // Reset to empty + return ERR_NEGATIVE_ACK; + } + + // Log success + std::ostringstream logMsg; + logMsg << "Set near limit for objective " << nosepiecePos + << " to " << (focusPos * FOCUS_STEP_SIZE_UM) << " um"; + LogMessage(logMsg.str().c_str()); + + // Reset property to empty + pProp->Set(""); + } + else if (value == "Clear") + { + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Get current nosepiece position + long nosepiecePos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + if (nosepiecePos < 1 || nosepiecePos > (long)nearLimits_.size()) + return ERR_INVALID_PARAMETER; + + // Set near limit to maximum (effectively removing the limit) + nearLimits_[nosepiecePos - 1] = FOCUS_MAX_POS; + + // Build NL command with all 6 positions: "NL p1,p2,p3,p4,p5,p6" + std::ostringstream cmd; + cmd << CMD_FOCUS_NEAR_LIMIT << TAG_DELIMITER; + for (size_t i = 0; i < nearLimits_.size(); i++) + { + if (i > 0) + cmd << DATA_DELIMITER; + cmd << nearLimits_[i]; + } + + // Execute command + std::string response; + int ret = hub->ExecuteCommand(cmd.str(), response); + if (ret != DEVICE_OK) + { + pProp->Set(""); // Reset to empty + return ret; + } + + if (!IsPositiveAck(response, CMD_FOCUS_NEAR_LIMIT)) + { + pProp->Set(""); // Reset to empty + return ERR_NEGATIVE_ACK; + } + + // Log success + std::ostringstream logMsg; + logMsg << "Cleared near limit for objective " << nosepiecePos + << " (set to maximum: " << (FOCUS_MAX_POS * FOCUS_STEP_SIZE_UM) << " um)"; + LogMessage(logMsg.str().c_str()); + + // Reset property to empty + pProp->Set(""); + } + } + return DEVICE_OK; +} + +int EvidentNosepiece::OnParfocalPosition(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (hub) + { + long pos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + if (pos >= 1 && pos <= (long)parfocalPositions_.size()) + { + // Get parfocal position for current objective (convert 1-based to 0-based) + long parfocalSteps = parfocalPositions_[pos - 1]; + // Convert steps to micrometers + double parfocalUm = parfocalSteps * FOCUS_STEP_SIZE_UM; + pProp->Set(parfocalUm); + } + else + { + pProp->Set(0.0); + } + } + } + return DEVICE_OK; +} + +int EvidentNosepiece::OnSetParfocalPosition(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + + if (value == "Set") + { + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Get current nosepiece position + long nosepiecePos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + if (nosepiecePos < 1 || nosepiecePos > (long)parfocalPositions_.size()) + return ERR_INVALID_PARAMETER; + + // Check if focus device is available + if (!hub->IsDevicePresent(DeviceType_Focus)) + { + LogMessage("Focus device not available, cannot set parfocal position"); + pProp->Set(""); // Reset to empty + return ERR_DEVICE_NOT_AVAILABLE; + } + + // Get current focus position + long focusPos = hub->GetModel()->GetPosition(DeviceType_Focus); + if (focusPos < 0) + { + LogMessage("Focus position unknown, cannot set parfocal position"); + pProp->Set(""); // Reset to empty + return ERR_POSITION_UNKNOWN; + } + + std::vector parfocalPositions = parfocalPositions_; + // Update parfocal position for current objective (convert 1-based to 0-based) + parfocalPositions[nosepiecePos - 1] = focusPos; + + int ret = SendParfocalSettings(parfocalPositions); + + // Reset property to empty + pProp->Set(""); + + if (ret != DEVICE_OK) + { + std::ostringstream errMsg; + errMsg << "Failed to set parfocal positions"; + LogMessage(errMsg.str().c_str()); + return ret; + } + + // Log success + parfocalPositions_ = parfocalPositions; + std::ostringstream logMsg; + logMsg << "Set parfocal position for objective " << nosepiecePos + << " to " << (focusPos * FOCUS_STEP_SIZE_UM) << " um"; + LogMessage(logMsg.str().c_str()); + + } + else if (value == "Clear") + { + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Get current nosepiece position + long nosepiecePos = hub->GetModel()->GetPosition(DeviceType_Nosepiece); + if (nosepiecePos < 1 || nosepiecePos > (long)parfocalPositions_.size()) + return ERR_INVALID_PARAMETER; + + std::vector parfocalPositions = parfocalPositions_; + + // Set parfocal position to zero (no offset) + parfocalPositions[nosepiecePos - 1] = 0; + + int ret = SendParfocalSettings(parfocalPositions); + pProp->Set(""); // Reset to empty + + if (ret != DEVICE_OK) + { + std::ostringstream errMsg; + errMsg << "Failed to clear parfocal positions"; + LogMessage(errMsg.str().c_str()); + return ret; + } + // Log success + std::ostringstream logMsg; + logMsg << "Cleared parfocal position for objective " << nosepiecePos; + LogMessage(logMsg.str().c_str()); + + parfocalPositions_ = parfocalPositions; + } + } + return DEVICE_OK; +} + + +; // Send parfocal positions and enabled state to microscope +int EvidentNosepiece::SendParfocalSettings(std::vector parfocalPositions) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Enter Setting mode + std::string cmd = BuildCommand(CMD_OPERATION_MODE, 1); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + LogMessage("Failed to enter Setting mode"); + return ret; + } + + // Build PF command with all 6 positions: "PF p1,p2,p3,p4,p5,p6" + std::ostringstream scmd; + scmd << CMD_PARFOCAL << TAG_DELIMITER; + for (size_t i = 0; i < parfocalPositions.size(); i++) + { + if (i > 0) + scmd << DATA_DELIMITER; + scmd << parfocalPositions[i]; + } + + // Execute command + ret = hub->ExecuteCommand(scmd.str(), response); + + // Exit Setting mode (always do this, even if S_OB failed) + cmd = BuildCommand(CMD_OPERATION_MODE, 0); + std::string exitResponse; + hub->ExecuteCommand(cmd, exitResponse); + + if (ret != DEVICE_OK) + { + std::ostringstream errMsg; + errMsg << "Failed to set parfocal positions"; + LogMessage(errMsg.str().c_str()); + return ret; + } + + if (!IsPositiveAck(response, CMD_PARFOCAL)) + { + return ERR_NEGATIVE_ACK; + } + + return DEVICE_OK; +} + +int EvidentNosepiece::OnParfocalEnabled(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + pProp->Set(parfocalEnabled_ ? "Enabled" : "Disabled"); + } + else if (eAct == MM::AfterSet) + { + // Enter Setting mode + std::string cmd = BuildCommand(CMD_OPERATION_MODE, 1); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + LogMessage("Failed to enter Setting mode"); + return ret; + } + + std::string value; + pProp->Get(value); + int enabled = (value == "Enabled") ? 1 : 0; + + cmd = BuildCommand(CMD_ENABLE_PARFOCAL, enabled); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Exit Setting mode (always do this, even if CMD failed) + cmd = BuildCommand(CMD_OPERATION_MODE, 0); + std::string exitResponse; + hub->ExecuteCommand(cmd, exitResponse); + + if (!IsPositiveAck(response, CMD_ENABLE_PARFOCAL)) + return ERR_NEGATIVE_ACK; + + // Update internal state + parfocalEnabled_ = (enabled == 1); + + // Log success + LogMessage(parfocalEnabled_ ? "Parfocal enabled" : "Parfocal disabled"); + } + return DEVICE_OK; +} + +int EvidentNosepiece::OnEscapeDistance(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + // Convert escapeDistance_ (0-9) to display string "X.0 mm" + std::ostringstream ss; + ss << escapeDistance_ << ".0 mm"; + pProp->Set(ss.str().c_str()); + } + else if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + + // Parse "X.0 mm" to extract the integer (0-9) + int newDistance = 0; + if (value.length() >= 1 && value[0] >= '0' && value[0] <= '9') + { + newDistance = value[0] - '0'; + } + + // Send ESC2 command with new value + std::string cmd = BuildCommand(CMD_FOCUS_ESCAPE, newDistance); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_FOCUS_ESCAPE)) + return ERR_NEGATIVE_ACK; + + // Update internal state + escapeDistance_ = newDistance; + + // Log success + std::ostringstream logMsg; + logMsg << "Focus escape distance set to " << newDistance << ".0 mm"; + LogMessage(logMsg.str().c_str()); + } + return DEVICE_OK; +} + +int EvidentNosepiece::QueryNearLimits() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query near limits from microscope + std::string cmd = BuildQuery(CMD_FOCUS_NEAR_LIMIT); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Parse response: "NL p1,p2,p3,p4,p5,p6" + std::vector params = ParseParameters(response); + if (params.size() >= NOSEPIECE_MAX_POS) + { + for (size_t i = 0; i < NOSEPIECE_MAX_POS; i++) + { + nearLimits_[i] = ParseLongParameter(params[i]); + } + } + else + { + LogMessage("Warning: NL? response has fewer than expected parameters"); + return ERR_INVALID_RESPONSE; + } + + return DEVICE_OK; +} + +int EvidentNosepiece::QueryParfocalSettings() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query parfocal positions from microscope + std::string cmd = BuildQuery(CMD_PARFOCAL); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Parse response: "PF p1,p2,p3,p4,p5,p6" + std::vector params = ParseParameters(response); + if (params.size() >= NOSEPIECE_MAX_POS) + { + for (size_t i = 0; i < NOSEPIECE_MAX_POS; i++) + { + parfocalPositions_[i] = ParseLongParameter(params[i]); + } + } + else + { + LogMessage("Warning: PF? response has fewer than expected parameters"); + return ERR_INVALID_RESPONSE; + } + + // Query parfocal enabled state + cmd = BuildQuery(CMD_ENABLE_PARFOCAL); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Parse response: "ENPF 0" or "ENPF 1" + params = ParseParameters(response); + if (params.size() > 0) + { + int enabled = ParseIntParameter(params[0]); + parfocalEnabled_ = (enabled == 1); + } + else + { + LogMessage("Warning: ENPF? response has no parameters"); + return ERR_INVALID_RESPONSE; + } + + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentMagnification - Magnification Changer Implementation +/////////////////////////////////////////////////////////////////////////////// + +// Static magnification values +const double EvidentMagnification::magnifications_[3] = {1.0, 1.6, 2.0}; + +EvidentMagnification::EvidentMagnification() : + initialized_(false), + name_(g_MagnificationDeviceName), + numPos_(MAGNIFICATION_MAX_POS) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Magnification changer not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentMagnification::~EvidentMagnification() +{ + Shutdown(); +} + +void EvidentMagnification::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentMagnification::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_Magnification)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_Magnification); + + // Create magnification property (read-only) + CPropertyAction* pAct = new CPropertyAction(this, &EvidentMagnification::OnMagnification); + int ret = CreateProperty(g_Keyword_Magnification, "1.0", MM::Float, true, pAct); + if (ret != DEVICE_OK) + return ret; + + // Set allowed values + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream os; + os << magnifications_[i]; + AddAllowedValue(g_Keyword_Magnification, os.str().c_str()); + } + + hub->RegisterDeviceAsUsed(DeviceType_Magnification, this); + + // Enable notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + return ret; + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentMagnification::Shutdown() +{ + if (initialized_) + { + EnableNotifications(false); + GetHub()->UnRegisterDeviceAsUsed(DeviceType_Magnification); + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentMagnification::Busy() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return false; + + return hub->GetModel()->IsBusy(DeviceType_Magnification); +} + +double EvidentMagnification::GetMagnification() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return 1.0; + + long pos = hub->GetModel()->GetPosition(DeviceType_Magnification); + if (pos < 1 || pos > static_cast(numPos_)) + return 1.0; + + // pos is 1-based, array is 0-based + return magnifications_[pos - 1]; +} + +int EvidentMagnification::OnMagnification(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + double mag = GetMagnification(); + pProp->Set(mag); + } + else if (eAct == MM::AfterSet) + { + // Read-only - nothing to do + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentMagnification::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentMagnification::EnableNotifications(bool enable) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + return hub->EnableNotification(CMD_MAGNIFICATION_NOTIFY, enable); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentLightPath - Light Path Selector Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentLightPath::EvidentLightPath() : + initialized_(false), + name_(g_LightPathDeviceName) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Light path selector not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentLightPath::~EvidentLightPath() +{ + Shutdown(); +} + +void EvidentLightPath::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentLightPath::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_LightPath)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentLightPath::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, 3); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels for light path positions + SetPositionLabel(0, "Left Port"); // LIGHT_PATH_LEFT_PORT = 1 + SetPositionLabel(1, "Binocular 50/50"); // LIGHT_PATH_BI_50_50 = 2 + SetPositionLabel(2, "Binocular 100%"); // LIGHT_PATH_BI_100 = 3 + SetPositionLabel(3, "Right Port"); // LIGHT_PATH_RIGHT_PORT = 4 + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_LightPath); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Register with hub so OnState can notify indicator changes + hub->RegisterDeviceAsUsed(DeviceType_LightPath, this); + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentLightPath::Shutdown() +{ + if (initialized_) + { + // Unregister from hub + EvidentHubWin* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_LightPath); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentLightPath::Busy() +{ + return false; // Light path changes are instantaneous +} + +unsigned long EvidentLightPath::GetNumberOfPositions() const +{ + return 4; +} + +int EvidentLightPath::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query current position + std::string cmd = BuildQuery(CMD_LIGHT_PATH); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 1 && pos <= 4) + { + // Convert from 1-based to 0-based + pProp->Set(static_cast(pos - 1)); + } + } + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + std::string cmd = BuildCommand(CMD_LIGHT_PATH, static_cast(pos + 1)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_LIGHT_PATH)) + return ERR_NEGATIVE_ACK; + + // Update MCU indicator I4 with new light path position (1-based) + hub->UpdateLightPathIndicator(static_cast(pos + 1), false); + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentLightPath::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentCondenserTurret - Condenser Turret Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentCondenserTurret::EvidentCondenserTurret() : + initialized_(false), + name_(g_CondenserTurretDeviceName), + numPos_(6) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Condenser turret not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentCondenserTurret::~EvidentCondenserTurret() +{ + Shutdown(); +} + +void EvidentCondenserTurret::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentCondenserTurret::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_CondenserTurret)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_CondenserTurret); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentCondenserTurret::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream label; + label << "Position-" << (i + 1); + SetPositionLabel(i, label.str().c_str()); + } + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_CondenserTurret); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Enable notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + return ret; + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentCondenserTurret::Shutdown() +{ + if (initialized_) + { + EnableNotifications(false); + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentCondenserTurret::Busy() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return false; + + return hub->GetModel()->IsBusy(DeviceType_CondenserTurret); +} + +unsigned long EvidentCondenserTurret::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentCondenserTurret::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + long pos = hub->GetModel()->GetPosition(DeviceType_CondenserTurret); + if (pos < 0) + return ERR_POSITION_UNKNOWN; + + // Convert from 1-based to 0-based + pProp->Set(pos - 1); + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + long targetPos = pos + 1; + + // Check if already at target position + long currentPos = hub->GetModel()->GetPosition(DeviceType_CondenserTurret); + if (currentPos == targetPos) + { + // Already at target, no need to move + hub->GetModel()->SetBusy(DeviceType_CondenserTurret, false); + return DEVICE_OK; + } + + // Set busy before sending command + hub->GetModel()->SetBusy(DeviceType_CondenserTurret, true); + + std::string cmd = BuildCommand(CMD_CONDENSER_TURRET, static_cast(targetPos)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + hub->GetModel()->SetBusy(DeviceType_CondenserTurret, false); + return ret; + } + + if (!IsPositiveAck(response, CMD_CONDENSER_TURRET)) + { + hub->GetModel()->SetBusy(DeviceType_CondenserTurret, false); + return ERR_NEGATIVE_ACK; + } + + // CondenserTurret does not send notifications (NTR) when movement completes. + // The positive ack ("TR +") is only returned after movement completes, + // so we can clear busy immediately and update position. + hub->GetModel()->SetPosition(DeviceType_CondenserTurret, targetPos); + hub->GetModel()->SetBusy(DeviceType_CondenserTurret, false); + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentCondenserTurret::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentCondenserTurret::EnableNotifications(bool enable) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Use TR (command tag) not NTR (notification tag) to enable notifications + return hub->EnableNotification(CMD_CONDENSER_TURRET, enable); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentDIAShutter - DIA (Transmitted Light) Shutter Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentDIAShutter::EvidentDIAShutter() : + initialized_(false), + name_(g_DIAShutterDeviceName) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "DIA shutter not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentDIAShutter::~EvidentDIAShutter() +{ + Shutdown(); +} + +void EvidentDIAShutter::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentDIAShutter::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_DIAShutter)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Create state property + CPropertyAction* pAct = new CPropertyAction(this, &EvidentDIAShutter::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + AddAllowedValue(MM::g_Keyword_State, "0"); // Closed + AddAllowedValue(MM::g_Keyword_State, "1"); // Open + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_DIAShutter); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Create Brightness property (DIA illumination intensity) + pAct = new CPropertyAction(this, &EvidentDIAShutter::OnBrightness); + ret = CreateProperty("Brightness", "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits("Brightness", 0, 255); + + // Query current brightness value + std::string cmd = BuildQuery(CMD_DIA_ILLUMINATION); + std::string response; + ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int brightness = ParseIntParameter(params[0]); + if (brightness >= 0 && brightness <= 255) + { + hub->GetModel()->SetPosition(DeviceType_DIABrightness, brightness); + SetProperty("Brightness", CDeviceUtils::ConvertToString(brightness)); + } + } + } + + // Create Mechanical Shutter property (controls physical shutter, independent of logical shutter) + pAct = new CPropertyAction(this, &EvidentDIAShutter::OnMechanicalShutter); + ret = CreateProperty("Mechanical Shutter", "Closed", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + AddAllowedValue("Mechanical Shutter", "Closed"); + AddAllowedValue("Mechanical Shutter", "Open"); + + // Query current mechanical shutter state + cmd = BuildQuery(CMD_DIA_SHUTTER); + ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int state = ParseIntParameter(params[0]); + // Note: DSH 0 = Open, DSH 1 = Closed (reversed) + SetProperty("Mechanical Shutter", (state == 0) ? "Open" : "Closed"); + } + } + + // Enable brightness change notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + return ret; + + // Register with hub so notification handler can call OnPropertyChanged + hub->RegisterDeviceAsUsed(DeviceType_DIAShutter, this); + + initialized_ = true; + + // Close logical shutter on startup (set brightness to 0) + SetOpen(false); + + return DEVICE_OK; +} + +int EvidentDIAShutter::Shutdown() +{ + if (initialized_) + { + // Disable brightness change notifications + EnableNotifications(false); + + // Unregister from hub + EvidentHubWin* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_DIAShutter); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentDIAShutter::Busy() +{ + return false; // Shutter changes are instantaneous +} + +int EvidentDIAShutter::SetOpen(bool open) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (open) + { + // Logical open: Set brightness to remembered value + int rememberedBrightness = hub->GetRememberedDIABrightness(); + std::string cmd = BuildCommand(CMD_DIA_ILLUMINATION, rememberedBrightness); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_DIA_ILLUMINATION)) + return ERR_NEGATIVE_ACK; + + // Update model + hub->GetModel()->SetPosition(DeviceType_DIABrightness, rememberedBrightness); + } + else + { + // Logical close: Remember current brightness, then set to 0 + std::string cmd = BuildQuery(CMD_DIA_ILLUMINATION); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int brightness = ParseIntParameter(params[0]); + if (brightness > 0) + hub->SetRememberedDIABrightness(brightness); + } + + // Set brightness to 0 + cmd = BuildCommand(CMD_DIA_ILLUMINATION, 0); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_DIA_ILLUMINATION)) + return ERR_NEGATIVE_ACK; + + // Update model + hub->GetModel()->SetPosition(DeviceType_DIABrightness, 0); + } + + return DEVICE_OK; +} + +int EvidentDIAShutter::GetOpen(bool& open) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Logical shutter state is based on brightness: open if brightness > 0 + std::string cmd = BuildQuery(CMD_DIA_ILLUMINATION); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int brightness = ParseIntParameter(params[0]); + open = (brightness > 0); + + // Update model + hub->GetModel()->SetPosition(DeviceType_DIABrightness, brightness); + } + + return DEVICE_OK; +} + +int EvidentDIAShutter::Fire(double /*deltaT*/) +{ + // Not implemented for this shutter + return DEVICE_UNSUPPORTED_COMMAND; +} + +int EvidentDIAShutter::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + bool open; + int ret = GetOpen(open); + if (ret != DEVICE_OK) + return ret; + pProp->Set(open ? 1L : 0L); + } + else if (eAct == MM::AfterSet) + { + long state; + pProp->Get(state); + return SetOpen(state != 0); + } + return DEVICE_OK; +} + +int EvidentDIAShutter::OnBrightness(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + // Return remembered brightness (what brightness will be when shutter opens) + int rememberedBrightness = hub->GetRememberedDIABrightness(); + pProp->Set(static_cast(rememberedBrightness)); + } + else if (eAct == MM::AfterSet) + { + long brightness; + pProp->Get(brightness); + + // Always update remembered brightness + hub->SetRememberedDIABrightness(static_cast(brightness)); + + // Only send DIL command if logical shutter is open (actual brightness > 0) + long currentBrightness = hub->GetModel()->GetPosition(DeviceType_DIABrightness); + if (currentBrightness > 0) + { + // Shutter is open: update actual lamp brightness + std::string cmd = BuildCommand(CMD_DIA_ILLUMINATION, static_cast(brightness)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_DIA_ILLUMINATION)) + return ERR_NEGATIVE_ACK; + + // Update model + hub->GetModel()->SetPosition(DeviceType_DIABrightness, brightness); + } + // If shutter is closed, don't send DIL command, don't update model + } + + return DEVICE_OK; +} + +int EvidentDIAShutter::OnMechanicalShutter(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + // Query physical shutter state + std::string cmd = BuildQuery(CMD_DIA_SHUTTER); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int state = ParseIntParameter(params[0]); + // Note: DSH 0 = Open, DSH 1 = Closed (reversed) + pProp->Set((state == 0) ? "Open" : "Closed"); + } + } + else if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + + // Convert "Open"/"Closed" to 0/1 (reversed: Open=0, Closed=1) + int state = (value == "Open") ? 0 : 1; + + // Send DSH command to control physical shutter + std::string cmd = BuildCommand(CMD_DIA_SHUTTER, state); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_DIA_SHUTTER)) + return ERR_NEGATIVE_ACK; + } + + return DEVICE_OK; +} + +int EvidentDIAShutter::EnableNotifications(bool enable) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + return hub->EnableNotification(CMD_DIA_ILLUMINATION_NOTIFY, enable); +} + +EvidentHubWin* EvidentDIAShutter::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentEPIShutter1 - EPI (Reflected Light) Shutter 1 Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentEPIShutter1::EvidentEPIShutter1() : + initialized_(false), + name_(g_EPIShutter1DeviceName) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "EPI shutter 1 not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentEPIShutter1::~EvidentEPIShutter1() +{ + Shutdown(); +} + +void EvidentEPIShutter1::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentEPIShutter1::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_EPIShutter1)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Create state property + CPropertyAction* pAct = new CPropertyAction(this, &EvidentEPIShutter1::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + AddAllowedValue(MM::g_Keyword_State, "0"); // Closed + AddAllowedValue(MM::g_Keyword_State, "1"); // Open + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_EPIShutter1); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Register with hub so SetOpen can notify indicator changes + hub->RegisterDeviceAsUsed(DeviceType_EPIShutter1, this); + + initialized_ = true; + + // Close shutter on startup + SetOpen(false); + + return DEVICE_OK; +} + +int EvidentEPIShutter1::Shutdown() +{ + if (initialized_) + { + // Unregister from hub + EvidentHubWin* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_EPIShutter1); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentEPIShutter1::Busy() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return false; + + return hub->GetModel()->IsBusy(DeviceType_EPIShutter1);; // Shutter changes are instantaneous +} + +int EvidentEPIShutter1::SetOpen(bool open) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string cmd = BuildCommand(CMD_EPI_SHUTTER1, open ? 1 : 0); + std::string response; + + hub->GetModel()->SetBusy(DeviceType_EPIShutter1, true); // Shutter changes are instantaneous + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Got a response, check if it's positive + if (!IsPositiveAck(response, CMD_EPI_SHUTTER1)) + { + return ERR_NEGATIVE_ACK; + } + + // Success - update indicator and return + hub->GetModel()->SetBusy(DeviceType_EPIShutter1, false); + hub->UpdateEPIShutter1Indicator(open ? 1 : 0, true); + return DEVICE_OK; +} + +int EvidentEPIShutter1::GetOpen(bool& open) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string cmd = BuildQuery(CMD_EPI_SHUTTER1); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int state = ParseIntParameter(params[0]); + open = (state == 1); + } + + return DEVICE_OK; +} + +int EvidentEPIShutter1::Fire(double /*deltaT*/) +{ + // Not implemented for this shutter + return DEVICE_UNSUPPORTED_COMMAND; +} + +int EvidentEPIShutter1::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + bool open; + int ret = GetOpen(open); + if (ret != DEVICE_OK) + return ret; + pProp->Set(open ? 1L : 0L); + } + else if (eAct == MM::AfterSet) + { + long state; + pProp->Get(state); + return SetOpen(state != 0); + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentEPIShutter1::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentMirrorUnit1 - Mirror Unit 1 (Filter Cube Turret) Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentMirrorUnit1::EvidentMirrorUnit1() : + initialized_(false), + name_(g_MirrorUnit1DeviceName), + numPos_(6) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Mirror unit 1 not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentMirrorUnit1::~EvidentMirrorUnit1() +{ + Shutdown(); +} + +void EvidentMirrorUnit1::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentMirrorUnit1::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_MirrorUnit1)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_MirrorUnit1); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentMirrorUnit1::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream label; + label << "Position-" << (i + 1); + SetPositionLabel(i, label.str().c_str()); + } + + // Note: MirrorUnit uses NMUINIT1 which is an initialization notification, + // not a position change notification, so we use query-based position tracking + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_MirrorUnit1); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Register with hub so encoder can notify property changes + hub->RegisterDeviceAsUsed(DeviceType_MirrorUnit1, this); + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentMirrorUnit1::Shutdown() +{ + if (initialized_) + { + // Unregister from hub + EvidentHubWin* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_MirrorUnit1); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentMirrorUnit1::Busy() +{ + return false; // Mirror unit changes are instantaneous +} + +unsigned long EvidentMirrorUnit1::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentMirrorUnit1::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query current position from hardware + std::string cmd = BuildQuery(CMD_MIRROR_UNIT1); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 1 && pos <= static_cast(numPos_)) + { + // Convert from 1-based to 0-based + pProp->Set(static_cast(pos - 1)); + } + } + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + std::string cmd = BuildCommand(CMD_MIRROR_UNIT1, static_cast(pos + 1)); + std::string response; + + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + + if (!IsPositiveAck(response, CMD_MIRROR_UNIT1)) + { + return ERR_NEGATIVE_ACK; + } + + // Success - update indicator and return + hub->UpdateMirrorUnitIndicator(static_cast(pos + 1), false); + return DEVICE_OK; + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentMirrorUnit1::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + + +//int EvidentMirrorUnit1::EnableNotifications(bool /*enable*/) +//{ + // NMUINIT1 is an initialization notification, not a position change notification + // MirrorUnit1 uses query-based position tracking instead +// return DEVICE_OK; +//} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentPolarizer - Polarizer Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentPolarizer::EvidentPolarizer() : + initialized_(false), + name_(g_PolarizerDeviceName), + numPos_(6) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Polarizer not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentPolarizer::~EvidentPolarizer() +{ + Shutdown(); +} + +void EvidentPolarizer::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentPolarizer::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_Polarizer)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_Polarizer); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentPolarizer::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels - Polarizer has Out (0) and In (1) + SetPositionLabel(0, "Out"); + SetPositionLabel(1, "In"); + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_Polarizer); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + // Enable notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + return ret; + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentPolarizer::Shutdown() +{ + if (initialized_) + { + EnableNotifications(false); + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentPolarizer::Busy() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return false; + + return hub->GetModel()->IsBusy(DeviceType_Polarizer); +} + +unsigned long EvidentPolarizer::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentPolarizer::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + long pos = hub->GetModel()->GetPosition(DeviceType_Polarizer); + if (pos < 0) + return ERR_POSITION_UNKNOWN; + + // Polarizer uses 0-based indexing (PO 0 = Out, PO 1 = In), no conversion needed + pProp->Set(pos); + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Set target position BEFORE sending command + // Polarizer uses 0-based indexing (PO 0 = Out, PO 1 = In) + hub->GetModel()->SetTargetPosition(DeviceType_Polarizer, pos); + + // Check if already at target position + long currentPos = hub->GetModel()->GetPosition(DeviceType_Polarizer); + if (currentPos == pos) + { + // Already at target, no need to move + hub->GetModel()->SetBusy(DeviceType_Polarizer, false); + return DEVICE_OK; + } + + hub->GetModel()->SetBusy(DeviceType_Polarizer, true); + + std::string cmd = BuildCommand(CMD_POLARIZER, static_cast(pos)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + hub->GetModel()->SetBusy(DeviceType_Polarizer, false); + return ret; + } + + if (!IsPositiveAck(response, CMD_POLARIZER)) + { + hub->GetModel()->SetBusy(DeviceType_Polarizer, false); + return ERR_NEGATIVE_ACK; + } + + // Polarizer does not send notifications (NPO) when movement completes. + // The positive ack ("PO +") is only returned after movement completes, + // so we can clear busy immediately and update position. + hub->GetModel()->SetPosition(DeviceType_Polarizer, pos); + hub->GetModel()->SetBusy(DeviceType_Polarizer, false); + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentPolarizer::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentPolarizer::EnableNotifications(bool enable) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Use PO (command tag) not NPO (notification tag) to enable notifications + return hub->EnableNotification(CMD_POLARIZER, enable); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentDICPrism - DIC Prism Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentDICPrism::EvidentDICPrism() : + initialized_(false), + name_(g_DICPrismDeviceName), + numPos_(6) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "DIC prism not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentDICPrism::~EvidentDICPrism() +{ + Shutdown(); +} + +void EvidentDICPrism::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentDICPrism::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_DICPrism)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_DICPrism); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentDICPrism::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream label; + label << "Position-" << (i + 1); + SetPositionLabel(i, label.str().c_str()); + } + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_DICPrism); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentDICPrism::Shutdown() +{ + if (initialized_) + { + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentDICPrism::Busy() +{ + return false; // DIC prism changes are instantaneous +} + +unsigned long EvidentDICPrism::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentDICPrism::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query current position + std::string cmd = BuildQuery(CMD_DIC_PRISM); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 1 && pos <= static_cast(numPos_)) + { + // Convert from 1-based to 0-based + pProp->Set(static_cast(pos - 1)); + } + } + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + std::string cmd = BuildCommand(CMD_DIC_PRISM, static_cast(pos + 1)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_DIC_PRISM)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentDICPrism::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentEPIND - EPI ND Filter Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentEPIND::EvidentEPIND() : + initialized_(false), + name_(g_EPINDDeviceName), + numPos_(6) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "EPI ND filter not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentEPIND::~EvidentEPIND() +{ + Shutdown(); +} + +void EvidentEPIND::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentEPIND::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_EPIND)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_EPIND); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentEPIND::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream label; + label << "Position-" << (i + 1); + SetPositionLabel(i, label.str().c_str()); + } + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_EPIND); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentEPIND::Shutdown() +{ + if (initialized_) + { + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentEPIND::Busy() +{ + return false; // ND filter changes are instantaneous +} + +unsigned long EvidentEPIND::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentEPIND::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query current position + std::string cmd = BuildQuery(CMD_EPI_ND); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 1 && pos <= static_cast(numPos_)) + { + // Convert from 1-based to 0-based + pProp->Set(static_cast(pos - 1)); + } + } + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + std::string cmd = BuildCommand(CMD_EPI_ND, static_cast(pos + 1)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_EPI_ND)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentEPIND::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentCorrectionCollar - Correction Collar Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentCorrectionCollar::EvidentCorrectionCollar() : + initialized_(false), + linked_(false), + name_(g_CorrectionCollarDeviceName), + stepSizeUm_(CORRECTION_COLLAR_STEP_SIZE_UM) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Correction collar not available on this microscope"); + SetErrorText(ERR_CORRECTION_COLLAR_NOT_LINKED, "Correction Collar must be linked before setting position. Set Activate property to 'Linked'."); + SetErrorText(ERR_CORRECTION_COLLAR_LINK_FAILED, "Correction Collar linking failed. Ensure correct objective is installed (typically objective 6)."); + + CreateHubIDProperty(); +} + +EvidentCorrectionCollar::~EvidentCorrectionCollar() +{ + Shutdown(); +} + +void EvidentCorrectionCollar::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentCorrectionCollar::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_CorrectionCollar)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Create Activate property for linking/unlinking + CPropertyAction* pAct = new CPropertyAction(this, &EvidentCorrectionCollar::OnActivate); + int ret = CreateProperty("Activate", "Unlinked", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + AddAllowedValue("Activate", "Linked"); + AddAllowedValue("Activate", "Unlinked"); + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_CorrectionCollar); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentCorrectionCollar::Shutdown() +{ + if (initialized_) + { + // Auto-unlink on shutdown if linked + if (linked_) + { + EvidentHubWin* hub = GetHub(); + if (hub) + { + std::string cmd = BuildCommand(CMD_CORRECTION_COLLAR_LINK, 0); // 0 = Unlink + std::string response; + hub->ExecuteCommand(cmd, response); + // Don't check response - best effort unlink + } + linked_ = false; + + // Notify core that position changed to 0 + GetCoreCallback()->OnStagePositionChanged(this, 0.0); + } + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentCorrectionCollar::Busy() +{ + return false; // Correction collar changes are instantaneous +} + +int EvidentCorrectionCollar::OnActivate(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + // Return current linked state + pProp->Set(linked_ ? "Linked" : "Unlinked"); + } + else if (eAct == MM::AfterSet) + { + std::string state; + pProp->Get(state); + + if (state == "Linked" && !linked_) + { + // Link the correction collar + std::string cmd = BuildCommand(CMD_CORRECTION_COLLAR_LINK, 1); // 1 = Link + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_CORRECTION_COLLAR_LINK)) + return ERR_CORRECTION_COLLAR_LINK_FAILED; + + // Wait for link to complete before initializing + // The hardware needs time after linking before accepting init command + CDeviceUtils::SleepMs(500); + + // Initialize the correction collar + cmd = BuildCommand(CMD_CORRECTION_COLLAR_INIT); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + // Link succeeded but init failed - try to unlink + cmd = BuildCommand(CMD_CORRECTION_COLLAR_LINK, 0); + hub->ExecuteCommand(cmd, response); + return ERR_CORRECTION_COLLAR_LINK_FAILED; + } + + if (!IsPositiveAck(response, CMD_CORRECTION_COLLAR_INIT)) + { + // Link succeeded but init failed - try to unlink + cmd = BuildCommand(CMD_CORRECTION_COLLAR_LINK, 0); + hub->ExecuteCommand(cmd, response); + return ERR_CORRECTION_COLLAR_LINK_FAILED; + } + + // Wait for correction collar initialization to complete + // The hardware needs time to initialize before accepting position commands + CDeviceUtils::SleepMs(500); + + // Successfully linked and initialized + linked_ = true; + } + else if (state == "Unlinked" && linked_) + { + // Unlink the correction collar + std::string cmd = BuildCommand(CMD_CORRECTION_COLLAR_LINK, 0); // 0 = Unlink + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_CORRECTION_COLLAR_LINK)) + return ERR_NEGATIVE_ACK; + + // Successfully unlinked + linked_ = false; + + // Notify core that position changed to 0 + GetCoreCallback()->OnStagePositionChanged(this, 0.0); + } + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentCorrectionCollar::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentCorrectionCollar::SetPositionUm(double pos) +{ + // Convert μm to steps (1:1 ratio) + long steps = static_cast(pos / stepSizeUm_); + return SetPositionSteps(steps); +} + +int EvidentCorrectionCollar::GetPositionUm(double& pos) +{ + long steps; + int ret = GetPositionSteps(steps); + if (ret != DEVICE_OK) + return ret; + + pos = steps * stepSizeUm_; + return DEVICE_OK; +} + +int EvidentCorrectionCollar::SetPositionSteps(long steps) +{ + // Check if linked + if (!linked_) + return ERR_CORRECTION_COLLAR_NOT_LINKED; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Clamp to limits + if (steps < CORRECTION_COLLAR_MIN_POS) steps = CORRECTION_COLLAR_MIN_POS; + if (steps > CORRECTION_COLLAR_MAX_POS) steps = CORRECTION_COLLAR_MAX_POS; + + // Send CC command with position + std::string cmd = BuildCommand(CMD_CORRECTION_COLLAR, static_cast(steps)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_CORRECTION_COLLAR)) + return ERR_NEGATIVE_ACK; + + return DEVICE_OK; +} + +int EvidentCorrectionCollar::GetPositionSteps(long& steps) +{ + // If not linked, return 0 (no error) + if (!linked_) + { + steps = 0; + return DEVICE_OK; + } + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query current position + std::string cmd = BuildQuery(CMD_CORRECTION_COLLAR); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= CORRECTION_COLLAR_MIN_POS && pos <= CORRECTION_COLLAR_MAX_POS) + { + steps = pos; + return DEVICE_OK; + } + } + + return ERR_INVALID_RESPONSE; +} + +int EvidentCorrectionCollar::SetOrigin() +{ + // Not supported by IX85 correction collar + return DEVICE_UNSUPPORTED_COMMAND; +} + +int EvidentCorrectionCollar::GetLimits(double& lower, double& upper) +{ + lower = CORRECTION_COLLAR_MIN_POS * stepSizeUm_; + upper = CORRECTION_COLLAR_MAX_POS * stepSizeUm_; + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentEPIShutter2 - EPI (Reflected Light) Shutter 2 Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentEPIShutter2::EvidentEPIShutter2() : + initialized_(false), + name_(g_EPIShutter2DeviceName) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "EPI shutter 2 not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentEPIShutter2::~EvidentEPIShutter2() +{ + Shutdown(); +} + +void EvidentEPIShutter2::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentEPIShutter2::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_EPIShutter2)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Create state property + CPropertyAction* pAct = new CPropertyAction(this, &EvidentEPIShutter2::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + AddAllowedValue(MM::g_Keyword_State, "0"); // Closed + AddAllowedValue(MM::g_Keyword_State, "1"); // Open + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_EPIShutter2); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + initialized_ = true; + + // Close shutter on startup + SetOpen(false); + + return DEVICE_OK; +} + +int EvidentEPIShutter2::Shutdown() +{ + if (initialized_) + { + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentEPIShutter2::Busy() +{ + return false; // Shutter changes are instantaneous +} + +int EvidentEPIShutter2::SetOpen(bool open) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string cmd = BuildCommand(CMD_EPI_SHUTTER2, open ? 1 : 0); + std::string response; + + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Got a response, check if it's positive + if (!IsPositiveAck(response, CMD_EPI_SHUTTER2)) + { + return ERR_NEGATIVE_ACK; + } + + // Success + return DEVICE_OK; +} + +int EvidentEPIShutter2::GetOpen(bool& open) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string cmd = BuildQuery(CMD_EPI_SHUTTER2); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int state = ParseIntParameter(params[0]); + open = (state == 1); + } + + return DEVICE_OK; +} + +int EvidentEPIShutter2::Fire(double /*deltaT*/) +{ + // Not implemented for this shutter + return DEVICE_UNSUPPORTED_COMMAND; +} + +int EvidentEPIShutter2::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + bool open; + int ret = GetOpen(open); + if (ret != DEVICE_OK) + return ret; + pProp->Set(open ? 1L : 0L); + } + else if (eAct == MM::AfterSet) + { + long state; + pProp->Get(state); + return SetOpen(state != 0); + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentEPIShutter2::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentMirrorUnit2 - Mirror Unit 2 (Filter Cube Turret) Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentMirrorUnit2::EvidentMirrorUnit2() : + initialized_(false), + name_(g_MirrorUnit2DeviceName), + numPos_(6) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Mirror unit 2 not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentMirrorUnit2::~EvidentMirrorUnit2() +{ + Shutdown(); +} + +void EvidentMirrorUnit2::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentMirrorUnit2::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(DeviceType_MirrorUnit2)) + return ERR_DEVICE_NOT_AVAILABLE; + + numPos_ = hub->GetModel()->GetNumPositions(DeviceType_MirrorUnit2); + + // Create properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentMirrorUnit2::OnState); + int ret = CreateProperty(MM::g_Keyword_State, "0", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + + SetPropertyLimits(MM::g_Keyword_State, 0, numPos_ - 1); + + // Create label property + pAct = new CPropertyAction(this, &CStateDeviceBase::OnLabel); + ret = CreateProperty(MM::g_Keyword_Label, "", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Define labels + for (unsigned int i = 0; i < numPos_; i++) + { + std::ostringstream label; + label << "Position-" << (i + 1); + SetPositionLabel(i, label.str().c_str()); + } + + // Note: MirrorUnit uses NMUINIT2 which is an initialization notification, + // not a position change notification, so we use query-based position tracking + + // Add firmware version as read-only property + std::string version = hub->GetDeviceVersion(DeviceType_MirrorUnit2); + if (!version.empty()) + { + ret = CreateProperty("Firmware Version", version.c_str(), MM::String, true); + if (ret != DEVICE_OK) + return ret; + } + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentMirrorUnit2::Shutdown() +{ + if (initialized_) + { + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentMirrorUnit2::Busy() +{ + return false; // Mirror unit changes are instantaneous +} + +unsigned long EvidentMirrorUnit2::GetNumberOfPositions() const +{ + return numPos_; +} + +int EvidentMirrorUnit2::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Query current position from hardware + std::string cmd = BuildQuery(CMD_MIRROR_UNIT2); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int pos = ParseIntParameter(params[0]); + if (pos >= 1 && pos <= static_cast(numPos_)) + { + // Convert from 1-based to 0-based + pProp->Set(static_cast(pos - 1)); + } + } + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert from 0-based to 1-based for the microscope + std::string cmd = BuildCommand(CMD_MIRROR_UNIT2, static_cast(pos + 1)); + std::string response; + + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + + if (!IsPositiveAck(response, CMD_MIRROR_UNIT2)) + { + return ERR_NEGATIVE_ACK; + } + + // Success + return DEVICE_OK; + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentMirrorUnit2::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentMirrorUnit2::EnableNotifications(bool /*enable*/) +{ + // NMUINIT2 is an initialization notification, not a position change notification + // MirrorUnit2 uses query-based position tracking instead + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentAutofocus - ZDC Autofocus Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentAutofocus::EvidentAutofocus() : + initialized_(false), + name_(g_AutofocusDeviceName), + continuousFocusing_(false), + afStatus_(0), + nearLimit_(1050000), // Near = upper limit (closer to sample) + farLimit_(0), // Far = lower limit (farther from sample) + lastNosepiecePos_(-1), + lastCoverslipType_(-1), + zdcInitNeeded_(false), + workflowMode_(2) // Default to Find-Focus-With-Offset mode +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "ZDC Autofocus not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentAutofocus::~EvidentAutofocus() +{ + Shutdown(); +} + +void EvidentAutofocus::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentAutofocus::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(EvidentIX85Win::DeviceType_Autofocus)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Query initial AF status + std::string cmd = BuildQuery(CMD_AF_STATUS); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + afStatus_ = ParseIntParameter(params[0]); + } + } + + // Query initial AF limits + cmd = BuildQuery(CMD_AF_LIMIT); + ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() >= 2 && params[0] != "X" && params[1] != "X") + { + nearLimit_ = ParseIntParameter(params[0]); + farLimit_ = ParseIntParameter(params[1]); + } + } + + // Create AF Status property (read-only) + CPropertyAction* pAct = new CPropertyAction(this, &EvidentAutofocus::OnAFStatus); + ret = CreateProperty("AF Status", GetAFStatusString(afStatus_).c_str(), MM::String, true, pAct); + if (ret != DEVICE_OK) + return ret; + + // Create Near Limit property + pAct = new CPropertyAction(this, &EvidentAutofocus::OnNearLimit); + ret = CreateProperty("Near Limit (um)", CDeviceUtils::ConvertToString(nearLimit_ * 0.01), MM::Float, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Create Far Limit property + pAct = new CPropertyAction(this, &EvidentAutofocus::OnFarLimit); + ret = CreateProperty("Far Limit (um)", CDeviceUtils::ConvertToString(farLimit_ * 0.01), MM::Float, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Create Cover Slip Type property + pAct = new CPropertyAction(this, &EvidentAutofocus::OnCoverSlipType); + ret = CreateProperty("Cover Slip Type", "Glass", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue("Cover Slip Type", "Glass"); + AddAllowedValue("Cover Slip Type", "Plastic"); + + // Create Cover Slip Thickness Glass property + pAct = new CPropertyAction(this, &EvidentAutofocus::OnCoverSlipThicknessGlass); + ret = CreateProperty("Cover Slip Thickness Glass (um)", "170", MM::Float, false, pAct); + if (ret != DEVICE_OK) + return ret; + SetPropertyLimits("Cover Slip Thickness Glass (um)", 150, 500); + + // Create Cover Slip Thickness Plastic property + pAct = new CPropertyAction(this, &EvidentAutofocus::OnCoverSlipThicknessPlastic); + ret = CreateProperty("Cover Slip Thickness Plastic (um)", "1000", MM::Float, false, pAct); + if (ret != DEVICE_OK) + return ret; + SetPropertyLimits("Cover Slip Thickness Plastic (um)", 700, 1500); + + // Create DIC Mode property + pAct = new CPropertyAction(this, &EvidentAutofocus::OnDICMode); + ret = CreateProperty("DIC Mode", "Off", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue("DIC Mode", "Off"); + AddAllowedValue("DIC Mode", "On"); + + // Create Buzzer Success property + pAct = new CPropertyAction(this, &EvidentAutofocus::OnBuzzerSuccess); + ret = CreateProperty("Buzzer Success", "On", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue("Buzzer Success", "Off"); + AddAllowedValue("Buzzer Success", "On"); + + // Create Buzzer Failure property + pAct = new CPropertyAction(this, &EvidentAutofocus::OnBuzzerFailure); + ret = CreateProperty("Buzzer Failure", "On", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue("Buzzer Failure", "Off"); + AddAllowedValue("Buzzer Failure", "On"); + + // Create AF Workflow Mode property + pAct = new CPropertyAction(this, &EvidentAutofocus::OnWorkflowMode); + ret = CreateProperty("AF-Workflow-Mode", "Find-Focus-With-Offset", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue("AF-Workflow-Mode", "Measure-Offset"); + AddAllowedValue("AF-Workflow-Mode", "Find-Focus-With-Offset"); + AddAllowedValue("AF-Workflow-Mode", "Continuous-Focus"); + + // Create Measured Focus Offset property + pAct = new CPropertyAction(this, &EvidentAutofocus::OnMeasuredFocusOffset); + ret = CreateProperty("Measured-Focus-Offset-um", "0.0", MM::Float, false, pAct); + if (ret != DEVICE_OK) + return ret; + SetPropertyLimits("Measured-Focus-Offset-um", -10000.0, 10000.0); // ±10mm range + + // Enable AF status notifications + EnableNotifications(true); + + // Mark that ZDC needs initialization (deferred to first FullFocus() call) + zdcInitNeeded_ = true; + + hub->RegisterDeviceAsUsed(EvidentIX85Win::DeviceType_Autofocus, this); + initialized_ = true; + return DEVICE_OK; +} + +int EvidentAutofocus::Shutdown() +{ + if (initialized_) + { + // Stop AF if running + if (continuousFocusing_) + { + StopAF(); + } + + EvidentHubWin* hub = GetHub(); + if (hub) + { + EnableNotifications(false); + hub->UnRegisterDeviceAsUsed(EvidentIX85Win::DeviceType_Autofocus); + } + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentAutofocus::Busy() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return false; + + // AF is busy during One-Shot or Focus Search operations + // also check the focus drive as we may be moving it to an in focus position + return (afStatus_ == 4) || hub->GetModel()->IsBusy(DeviceType_Focus); // 4 = Search +} + +void EvidentAutofocus::UpdateAFStatus(int status) +{ + if (afStatus_ != status) + { + afStatus_ = status; + OnPropertyChanged("AF Status", GetAFStatusString(afStatus_).c_str()); + } +} + +void EvidentAutofocus::UpdateMeasuredZOffset(long offsetSteps) +{ + // Update property to reflect new offset + double offsetUm = offsetSteps * FOCUS_STEP_SIZE_UM; + std::ostringstream valStr; + valStr << offsetUm; + OnPropertyChanged("Measured-Focus-Offset-um", valStr.str().c_str()); +} + +int EvidentAutofocus::SetContinuousFocusing(bool state) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (state) + { + // Check if ZDC needs re-initialization (objective changed, or settings changed) + long nosepiecePos = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_Nosepiece); + if (nosepiecePos != lastNosepiecePos_ || zdcInitNeeded_) + { + int ret = InitializeZDC(); + if (ret != DEVICE_OK) + return ret; + + // Clear the flag after successful initialization + zdcInitNeeded_ = false; + } + + // Determine AF mode to use + int afMode; + if (workflowMode_ == 3) // Continuous Focus workflow + { + // Use AF mode 2 (Continuous Focus Drive) + afMode = 2; + LogMessage("Continuous Focus mode: Using AF 2 (Focus Drive tracking)"); + } + else + { + // Use AF mode 2 (Focus Drive) as default + afMode = 2; + } + + // Start continuous AF + std::string cmd = BuildCommand(CMD_AF_START_STOP, afMode); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_AF_START_STOP)) + return ERR_NEGATIVE_ACK; + + continuousFocusing_ = true; + } + else + { + // Stop AF + int ret = StopAF(); + if (ret != DEVICE_OK) + return ret; + + continuousFocusing_ = false; + } + + return DEVICE_OK; +} + +int EvidentAutofocus::GetContinuousFocusing(bool& state) +{ + state = continuousFocusing_; + return DEVICE_OK; +} + +bool EvidentAutofocus::IsContinuousFocusLocked() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return false; + + // Query current AF status + std::string cmd = BuildQuery(CMD_AF_STATUS); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return false; + + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + int newStatus = ParseIntParameter(params[0]); + if (afStatus_ != newStatus) + { + afStatus_ = newStatus; + OnPropertyChanged("AF Status", GetAFStatusString(afStatus_).c_str()); + } + } + + // Locked when in Focus (1) or Track (2) state + return (afStatus_ == 1 || afStatus_ == 2); +} + +int EvidentAutofocus::FullFocus() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Handle workflow-specific modes + if (workflowMode_ == 1) // Measure Offset mode + { + return MeasureZOffset(); + } + else if (workflowMode_ == 2) // Find Focus with Offset mode + { + return FindFocusWithOffset(); + } + + // Check if ZDC needs re-initialization (objective changed, or settings changed) + long nosepiecePos = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_Nosepiece); + if (nosepiecePos != lastNosepiecePos_ || zdcInitNeeded_) + { + int ret = InitializeZDC(); + if (ret != DEVICE_OK) + return ret; + + // Clear the flag after successful initialization + zdcInitNeeded_ = false; + } + + // Use continuous AF approach to avoid resetting offset lens position + // Start continuous AF with AF mode 2 (Focus Drive) + std::string cmd = BuildCommand(CMD_AF_START_STOP, 2); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_AF_START_STOP)) + return ERR_NEGATIVE_ACK; + + // Wait for focus to be achieved (afStatus_ becomes 1=Focus) + // afStatus_ is updated by notifications (NAFST), no polling needed + // Timeout: max 30 seconds + const int maxWaitMs = 30000; + const int sleepIntervalMs = 100; + int elapsedMs = 0; + + while (elapsedMs < maxWaitMs) + { + CDeviceUtils::SleepMs(sleepIntervalMs); + elapsedMs += sleepIntervalMs; + + // Check afStatus_ which is updated by NAFST notifications + if (afStatus_ == 1) // 1 = Focus achieved + { + // AF 2 (Focus drive): manually stop after achieving focus + ret = StopAF(); + if (ret != DEVICE_OK) + return ret; + return DEVICE_OK; + } + else if (afStatus_ == 0) // Stopped + { + // AF 2 stopped unexpectedly - failure + return ERR_NEGATIVE_ACK; + } + } + + // Timeout - stop AF and return error + StopAF(); + return ERR_COMMAND_TIMEOUT; +} + +int EvidentAutofocus::IncrementalFocus() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Execute Focus Search + std::string cmd = BuildCommand(CMD_AF_START_STOP, 3); // 3 = Focus Search + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_AF_START_STOP)) + return ERR_NEGATIVE_ACK; + + return DEVICE_OK; +} + +int EvidentAutofocus::GetLastFocusScore(double& score) +{ + score = 0.0; + return DEVICE_OK; +} + +int EvidentAutofocus::GetCurrentFocusScore(double& score) +{ + score = 0.0; + return DEVICE_OK; +} + +int EvidentAutofocus::GetOffset(double& offset) +{ + // Get offset lens position + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + long pos = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_OffsetLens); + offset = pos * OFFSET_LENS_STEP_SIZE_UM; + return DEVICE_OK; +} + +int EvidentAutofocus::SetOffset(double offset) +{ + // Set offset lens position + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + long steps = static_cast(offset / OFFSET_LENS_STEP_SIZE_UM); + + std::string cmd = BuildCommand(CMD_OFFSET_LENS_GOTO, static_cast(steps)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_OFFSET_LENS_GOTO)) + return ERR_NEGATIVE_ACK; + + hub->GetModel()->SetPosition(EvidentIX85Win::DeviceType_OffsetLens, steps); + return DEVICE_OK; +} + +int EvidentAutofocus::GetMeasuredZOffset(double& offset) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Get stored Z-offset in micrometers from the model + offset = hub->GetModel()->GetMeasuredZOffset() * FOCUS_STEP_SIZE_UM; + return DEVICE_OK; +} + +int EvidentAutofocus::SetMeasuredZOffset(double offset) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Set stored Z-offset (for manual override) + long offsetSteps = static_cast(offset / FOCUS_STEP_SIZE_UM); + hub->GetModel()->SetMeasuredZOffset(offsetSteps); + + // Notify both devices that the offset has changed + hub->NotifyMeasuredZOffsetChanged(offsetSteps); + + return DEVICE_OK; +} + +int EvidentAutofocus::StopAF() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string cmd = BuildCommand(CMD_AF_STOP); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // AFSTP returns + when stopped successfully + if (!IsPositiveAck(response, CMD_AF_STOP)) + return ERR_NEGATIVE_ACK; + + if (afStatus_ != 0) + { + afStatus_ = 0; + OnPropertyChanged("AF Status", GetAFStatusString(afStatus_).c_str()); + } + return DEVICE_OK; +} + +int EvidentAutofocus::InitializeZDC() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + std::string cmd; + std::string response; + int ret; + + // Get current nosepiece position + long nosepiecePos = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_Nosepiece); + if (nosepiecePos < 1 || nosepiecePos > 6) + nosepiecePos = 1; // Default to position 1 + + // Get objective name for current nosepiece position + const std::vector& objectives = hub->GetObjectiveInfo(); + std::string objectiveName = "Unknown"; + if (nosepiecePos >= 1 && nosepiecePos <= (long)objectives.size()) + { + objectiveName = objectives[nosepiecePos - 1].name; + } + + // Step 1: Enter Setting status (OPE 1) + cmd = BuildCommand(CMD_OPERATION_MODE, 1); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + if (!IsPositiveAck(response, CMD_OPERATION_MODE)) + return ERR_NEGATIVE_ACK; + + // Step 2: Set Coverslip type - query current setting first + cmd = BuildQuery(CMD_COVERSLIP_TYPE); + ret = hub->ExecuteCommand(cmd, response); + int coverslipType = 1; // Default to Glass + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + coverslipType = ParseIntParameter(params[0]); + } + } + cmd = BuildCommand(CMD_COVERSLIP_TYPE, coverslipType); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + // Exit setting mode before returning error + hub->ExecuteCommand(BuildCommand(CMD_OPERATION_MODE, 0), response); + return ret; + } + if (!IsPositiveAck(response, CMD_COVERSLIP_TYPE)) + { + hub->ExecuteCommand(BuildCommand(CMD_OPERATION_MODE, 0), response); + return ERR_NEGATIVE_ACK; + } + + // Step 3: Set objective lens for AF (S_OB position,name) + std::ostringstream sobCmd; + sobCmd << CMD_AF_SET_OBJECTIVE << " " << nosepiecePos << "," << objectiveName; + ret = hub->ExecuteCommand(sobCmd.str(), response); + if (ret != DEVICE_OK) + { + hub->ExecuteCommand(BuildCommand(CMD_OPERATION_MODE, 0), response); + return ret; + } + if (!IsPositiveAck(response, CMD_AF_SET_OBJECTIVE)) + { + hub->ExecuteCommand(BuildCommand(CMD_OPERATION_MODE, 0), response); + return ERR_NEGATIVE_ACK; + } + + // Step 4: Exit Setting status (OPE 0) + cmd = BuildCommand(CMD_OPERATION_MODE, 0); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + if (!IsPositiveAck(response, CMD_OPERATION_MODE)) + return ERR_NEGATIVE_ACK; + + // Step 5: Set ZDC DM In (AFDM 1) + cmd = BuildCommand(CMD_AF_DICHROIC, 1); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + if (!IsPositiveAck(response, CMD_AF_DICHROIC)) + return ERR_NEGATIVE_ACK; + + // Step 6: Move offset lens to base position for current objective (ABBP) + cmd = BuildCommand(CMD_OFFSET_LENS_BASE_POSITION, static_cast(nosepiecePos)); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + if (!IsPositiveAck(response, CMD_OFFSET_LENS_BASE_POSITION)) + return ERR_NEGATIVE_ACK; + + // Step 7: Enable Focus Jog (JG 1) + cmd = BuildCommand(CMD_JOG, 1); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + if (!IsPositiveAck(response, CMD_JOG)) + return ERR_NEGATIVE_ACK; + + // Step 8: Set AF search range (AFL nearLimit,farLimit) + cmd = BuildCommand(CMD_AF_LIMIT, static_cast(nearLimit_), static_cast(farLimit_)); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + if (!IsPositiveAck(response, CMD_AF_LIMIT)) + return ERR_NEGATIVE_ACK; + + // Update tracking variables + lastNosepiecePos_ = nosepiecePos; + lastCoverslipType_ = coverslipType; + + return DEVICE_OK; +} + +int EvidentAutofocus::MeasureZOffset() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Re-initialize ZDC if needed + long nosepiecePos = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_Nosepiece); + if (nosepiecePos != lastNosepiecePos_ || zdcInitNeeded_) + { + int ret = InitializeZDC(); + if (ret != DEVICE_OK) + return ret; + zdcInitNeeded_ = false; + } + + // Step 1: Store current focus position (user should have focused on sample) + long originalZPos = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_Focus); + if (originalZPos < 0) + { + LogMessage("Focus position unknown, cannot measure offset"); + return ERR_POSITION_UNKNOWN; + } + + std::ostringstream logMsg1; + logMsg1 << "Measuring Z-offset: Starting from position " << originalZPos; + LogMessage(logMsg1.str().c_str()); + + // Step 2: Run AF mode 1 (One-Shot Z-only) + std::string cmd = BuildCommand(CMD_AF_START_STOP, 1); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_AF_START_STOP)) + return ERR_NEGATIVE_ACK; + + // Step 3: Wait for AF to achieve focus (max 30 seconds) + const int maxWaitMs = 30000; + const int sleepIntervalMs = 100; + int elapsedMs = 0; + + while (elapsedMs < maxWaitMs) + { + CDeviceUtils::SleepMs(sleepIntervalMs); + elapsedMs += sleepIntervalMs; + + if (afStatus_ == 1) // Focus achieved + { + // Stop AF (mode 1 requires manual stop) + ret = StopAF(); + if (ret != DEVICE_OK) + return ret; + + // Step 4: Read new focus position + long newZPos = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_Focus); + + // Step 5: Calculate and store offset + // Offset = how to correct from ZDC's focus to user's desired focus + long measuredZOffset = originalZPos - newZPos; + hub->GetModel()->SetMeasuredZOffset(measuredZOffset); + + // Notify both devices and core of property change + double offsetUm = measuredZOffset * FOCUS_STEP_SIZE_UM; + std::ostringstream valStr; + valStr << offsetUm; + OnPropertyChanged("Measured-Focus-Offset-um", valStr.str().c_str()); + + std::ostringstream logMsg2; + logMsg2 << "Measured Z-offset: " << measuredZOffset << + " steps (" << offsetUm << " um)"; + LogMessage(logMsg2.str().c_str()); + + // Notify other devices of offset change + hub->NotifyMeasuredZOffsetChanged(measuredZOffset); + + // Step 6: Return focus to original position + cmd = BuildCommand(CMD_FOCUS_GOTO, static_cast(originalZPos)); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_FOCUS_GOTO)) + return ERR_NEGATIVE_ACK; + + // Wait for focus to return to original position + elapsedMs = 0; + while (elapsedMs < maxWaitMs) + { + CDeviceUtils::SleepMs(sleepIntervalMs); + elapsedMs += sleepIntervalMs; + + long currentPos = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_Focus); + if (abs(currentPos - originalZPos) <= FOCUS_POSITION_TOLERANCE) + { + LogMessage("Z-offset measurement complete, returned to original position"); + return DEVICE_OK; + } + } + + LogMessage("Warning: Timeout returning to original position"); + return DEVICE_OK; // Offset measured successfully even if return timed out + } + else if (afStatus_ == 0) // Stopped + { + // AF mode 1 auto-stops after completion (like AF mode 3) + // Check if focus position changed from original - if so, AF succeeded + long currentZPos = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_Focus); + + if (currentZPos != originalZPos) + { + // Position changed - AF succeeded and auto-stopped + LogMessage("AF mode 1 completed and auto-stopped"); + + // Step 4: Read new focus position (already have it as currentZPos) + long newZPos = currentZPos; + + // Step 5: Calculate and store offset + // Offset = how to correct from ZDC's focus to user's desired focus + long measuredZOffset = originalZPos - newZPos; + hub->GetModel()->SetMeasuredZOffset(measuredZOffset); + + // Notify core of property change + double offsetUm = measuredZOffset * FOCUS_STEP_SIZE_UM; + std::ostringstream valStr; + valStr << offsetUm; + OnPropertyChanged("Measured-Focus-Offset-um", valStr.str().c_str()); + + std::ostringstream logMsg2; + logMsg2 << "Measured Z-offset: " << measuredZOffset << + " steps (" << offsetUm << " um)"; + LogMessage(logMsg2.str().c_str()); + + // Notify other devices of offset change + hub->NotifyMeasuredZOffsetChanged(measuredZOffset); + + // Step 6: Return focus to original position + cmd = BuildCommand(CMD_FOCUS_GOTO, static_cast(originalZPos)); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_FOCUS_GOTO)) + return ERR_NEGATIVE_ACK; + + // Wait for focus to return to original position + elapsedMs = 0; + while (elapsedMs < maxWaitMs) + { + CDeviceUtils::SleepMs(sleepIntervalMs); + elapsedMs += sleepIntervalMs; + + long currentPos = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_Focus); + if (abs(currentPos - originalZPos) <= FOCUS_POSITION_TOLERANCE) + { + LogMessage("Z-offset measurement complete, returned to original position"); + return DEVICE_OK; + } + } + + LogMessage("Warning: Timeout returning to original position"); + return DEVICE_OK; // Offset measured successfully even if return timed out + } + else + { + // Position didn't change - AF failed + LogMessage("AF stopped without finding focus (position unchanged)"); + return ERR_NEGATIVE_ACK; + } + } + } + + // Timeout - stop AF and return error + StopAF(); + LogMessage("Timeout during Z-offset measurement"); + return ERR_COMMAND_TIMEOUT; +} + +int EvidentAutofocus::FindFocusWithOffset() +{ + // TODO: evaluate if we should set Busy to true and execute this in a separate thread + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Re-initialize ZDC if needed + long nosepiecePos = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_Nosepiece); + if (nosepiecePos != lastNosepiecePos_ || zdcInitNeeded_) + { + int ret = InitializeZDC(); + if (ret != DEVICE_OK) + return ret; + zdcInitNeeded_ = false; + } + + // Step 1: Run AF mode 3 (Offset lens mode) + std::string cmd = BuildCommand(CMD_AF_START_STOP, 3); + std::string response; + + + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_AF_START_STOP)) + { + return ERR_NEGATIVE_ACK; + } + + LogMessage("Find Focus with Offset: Running AF mode 3"); + + // Step 2: Wait for AF to achieve focus (max 30 seconds) + const int maxWaitMs = 30000; + const int sleepIntervalMs = 100; + int elapsedMs = 0; + bool focusAchieved = false; + + while (elapsedMs < maxWaitMs) + { + CDeviceUtils::SleepMs(sleepIntervalMs); + elapsedMs += sleepIntervalMs; + + if (afStatus_ == 1 || afStatus_ == 0) // Focus achieved or auto-stopped + { + focusAchieved = true; + break; + } + } + + if (!focusAchieved) + { + StopAF(); + LogMessage("Timeout during Find Focus with Offset"); + return ERR_COMMAND_TIMEOUT; + } + + // Step 3: Stop AF (mode 3 may auto-stop, but ensure it's stopped) + ret = StopAF(); + if (ret != DEVICE_OK) + LogMessage("Warning: Failed to stop AF, but continuing with offset application"); + + // Step 4: Apply stored offset to Focus Drive + long currentZPos = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_Focus); + long measuredZOffset = hub->GetModel()->GetMeasuredZOffset(); + long targetZPos = currentZPos + measuredZOffset; + + std::ostringstream logMsg; + logMsg << "Applying Z-offset: " << measuredZOffset << + " steps (from " << currentZPos << + " to " << targetZPos << ")"; + LogMessage(logMsg.str().c_str()); + + // Step 5: Move Focus Drive to new position + // Sets the Focus drive busy flag + return hub->SetFocusPositionSteps(targetZPos); +} + +std::string EvidentAutofocus::GetAFStatusString(int status) +{ + switch (status) + { + case 0: return "Stop"; + case 1: return "Focus"; + case 2: return "Track"; + case 3: return "Wait"; + case 4: return "Search"; + default: return "Unknown"; + } +} + +EvidentHubWin* EvidentAutofocus::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentAutofocus::EnableNotifications(bool enable) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + return hub->EnableNotification(CMD_AF_STATUS, enable); +} + +int EvidentAutofocus::OnAFStatus(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + // Query current status + EvidentHubWin* hub = GetHub(); + if (hub) + { + std::string cmd = BuildQuery(CMD_AF_STATUS); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + afStatus_ = ParseIntParameter(params[0]); + } + } + } + pProp->Set(GetAFStatusString(afStatus_).c_str()); + } + return DEVICE_OK; +} + +int EvidentAutofocus::OnNearLimit(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + pProp->Set(nearLimit_ * 0.01); // Convert 0.01um units to um + } + else if (eAct == MM::AfterSet) + { + double val; + pProp->Get(val); + long newNear = static_cast(val * 100); // Convert um to 0.01um units + + // Validate: Near limit must be > Far limit + if (newNear <= farLimit_) + { + return ERR_INVALID_PARAMETER; + } + + std::string cmd = BuildCommand(CMD_AF_LIMIT, static_cast(newNear), static_cast(farLimit_)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_AF_LIMIT)) + return ERR_NEGATIVE_ACK; + + nearLimit_ = newNear; + + // Mark that ZDC needs re-initialization (deferred until next AF operation) + zdcInitNeeded_ = true; + } + return DEVICE_OK; +} + +int EvidentAutofocus::OnFarLimit(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + pProp->Set(farLimit_ * 0.01); // Convert 0.01um units to um + } + else if (eAct == MM::AfterSet) + { + double val; + pProp->Get(val); + long newFar = static_cast(val * 100); // Convert um to 0.01um units + + // Validate: Near limit must be > Far limit + if (nearLimit_ <= newFar) + { + return ERR_INVALID_PARAMETER; + } + + std::string cmd = BuildCommand(CMD_AF_LIMIT, static_cast(nearLimit_), static_cast(newFar)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_AF_LIMIT)) + return ERR_NEGATIVE_ACK; + + farLimit_ = newFar; + + // Mark that ZDC needs re-initialization (deferred until next AF operation) + zdcInitNeeded_ = true; + } + return DEVICE_OK; +} + +int EvidentAutofocus::OnCoverSlipType(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + std::string cmd = BuildQuery(CMD_COVERSLIP_TYPE); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int type = ParseIntParameter(params[0]); + pProp->Set(type == 1 ? "Glass" : "Plastic"); + } + } + } + else if (eAct == MM::AfterSet) + { + std::string val; + pProp->Get(val); + int type = (val == "Glass") ? 1 : 2; + + std::string cmd = BuildCommand(CMD_COVERSLIP_TYPE, type); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_COVERSLIP_TYPE)) + return ERR_NEGATIVE_ACK; + + // Track coverslip type change + lastCoverslipType_ = type; + + // Mark that ZDC needs re-initialization (deferred until next AF operation) + zdcInitNeeded_ = true; + } + return DEVICE_OK; +} + +int EvidentAutofocus::OnCoverSlipThicknessGlass(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + std::string cmd = BuildQuery(CMD_COVERSLIP_THICKNESS); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() >= 1 && params[0] != "X") + { + int thickness = ParseIntParameter(params[0]); + pProp->Set(thickness * 10.0); // Convert 10um units to um + } + } + } + else if (eAct == MM::AfterSet) + { + double val; + pProp->Get(val); + int thickness = static_cast(val / 10.0); // Convert um to 10um units + + // Query current plastic thickness to preserve it + std::string cmd = BuildQuery(CMD_COVERSLIP_THICKNESS); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + int plasticThickness = 100; // Default + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() >= 2 && params[1] != "X") + { + plasticThickness = ParseIntParameter(params[1]); + } + } + + cmd = BuildCommand(CMD_COVERSLIP_THICKNESS, thickness, plasticThickness); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_COVERSLIP_THICKNESS)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +int EvidentAutofocus::OnCoverSlipThicknessPlastic(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + std::string cmd = BuildQuery(CMD_COVERSLIP_THICKNESS); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() >= 2 && params[1] != "X") + { + int thickness = ParseIntParameter(params[1]); + pProp->Set(thickness * 10.0); // Convert 10um units to um + } + } + } + else if (eAct == MM::AfterSet) + { + double val; + pProp->Get(val); + int thickness = static_cast(val / 10.0); // Convert um to 10um units + + // Query current glass thickness to preserve it + std::string cmd = BuildQuery(CMD_COVERSLIP_THICKNESS); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + int glassThickness = 17; // Default + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() >= 1 && params[0] != "X") + { + glassThickness = ParseIntParameter(params[0]); + } + } + + cmd = BuildCommand(CMD_COVERSLIP_THICKNESS, glassThickness, thickness); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_COVERSLIP_THICKNESS)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +int EvidentAutofocus::OnDICMode(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + std::string cmd = BuildQuery(CMD_AF_DIC); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + int mode = ParseIntParameter(params[0]); + pProp->Set(mode == 0 ? "Off" : "On"); + } + } + } + else if (eAct == MM::AfterSet) + { + std::string val; + pProp->Get(val); + int mode = (val == "Off") ? 0 : 1; + + std::string cmd = BuildCommand(CMD_AF_DIC, mode); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_AF_DIC)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +int EvidentAutofocus::OnBuzzerSuccess(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + std::string cmd = BuildQuery(CMD_AF_BUZZER); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() >= 1) + { + int success = ParseIntParameter(params[0]); + pProp->Set(success == 0 ? "Off" : "On"); + } + } + } + else if (eAct == MM::AfterSet) + { + std::string val; + pProp->Get(val); + int success = (val == "Off") ? 0 : 1; + + // Query current failure setting to preserve it + std::string cmd = BuildQuery(CMD_AF_BUZZER); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + int failure = 1; // Default + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() >= 2) + { + failure = ParseIntParameter(params[1]); + } + } + + cmd = BuildCommand(CMD_AF_BUZZER, success, failure); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_AF_BUZZER)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +int EvidentAutofocus::OnBuzzerFailure(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (eAct == MM::BeforeGet) + { + std::string cmd = BuildQuery(CMD_AF_BUZZER); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() >= 2) + { + int failure = ParseIntParameter(params[1]); + pProp->Set(failure == 0 ? "Off" : "On"); + } + } + } + else if (eAct == MM::AfterSet) + { + std::string val; + pProp->Get(val); + int failure = (val == "Off") ? 0 : 1; + + // Query current success setting to preserve it + std::string cmd = BuildQuery(CMD_AF_BUZZER); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + int success = 1; // Default + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() >= 1) + { + success = ParseIntParameter(params[0]); + } + } + + cmd = BuildCommand(CMD_AF_BUZZER, success, failure); + ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_AF_BUZZER)) + return ERR_NEGATIVE_ACK; + } + return DEVICE_OK; +} + +int EvidentAutofocus::OnWorkflowMode(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + std::string mode; + switch (workflowMode_) + { + case 1: mode = "Measure-Offset"; break; + case 2: mode = "Find-Focus-With-Offset"; break; + case 3: mode = "Continuous-Focus"; break; + default: mode = "Continuous-Focus"; break; + } + pProp->Set(mode.c_str()); + } + else if (eAct == MM::AfterSet) + { + std::string val; + pProp->Get(val); + if (val == "Measure-Offset") + workflowMode_ = 1; + else if (val == "Find-Focus-With-Offset") + workflowMode_ = 2; + else if (val == "Continuous-Focus") + workflowMode_ = 3; + } + return DEVICE_OK; +} + +int EvidentAutofocus::OnMeasuredFocusOffset(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + double offset; + GetMeasuredZOffset(offset); // Already converts steps to µm + pProp->Set(offset); + } + else if (eAct == MM::AfterSet) + { + double offset; + pProp->Get(offset); + SetMeasuredZOffset(offset); // Already converts µm to steps + + // Notify core of property change + std::ostringstream valStr; + valStr << offset; + OnPropertyChanged("Measured-Focus-Offset-um", valStr.str().c_str()); + + // Log the change + std::ostringstream logMsg; + logMsg << "Measured focus offset set to " << offset << " um"; + LogMessage(logMsg.str().c_str()); + } + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentOffsetLens - Offset Lens Implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentOffsetLens::EvidentOffsetLens() : + initialized_(false), + name_(g_OffsetLensDeviceName), + stepSizeUm_(OFFSET_LENS_STEP_SIZE_UM) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Offset lens not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentOffsetLens::~EvidentOffsetLens() +{ + Shutdown(); +} + +void EvidentOffsetLens::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentOffsetLens::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(EvidentIX85Win::DeviceType_OffsetLens)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Query initial position + std::string cmd = BuildQuery(CMD_OFFSET_LENS_POSITION); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret == DEVICE_OK) + { + std::vector params = ParseParameters(response); + if (params.size() > 0 && params[0] != "X") + { + long pos = ParseIntParameter(params[0]); + hub->GetModel()->SetPosition(EvidentIX85Win::DeviceType_OffsetLens, pos); + } + } + + // Create Position property + CPropertyAction* pAct = new CPropertyAction(this, &EvidentOffsetLens::OnPosition); + ret = CreateProperty("Position (um)", "0", MM::Float, false, pAct); + if (ret != DEVICE_OK) + return ret; + + // Enable notifications + EnableNotifications(true); + + hub->RegisterDeviceAsUsed(EvidentIX85Win::DeviceType_OffsetLens, this); + initialized_ = true; + return DEVICE_OK; +} + +int EvidentOffsetLens::Shutdown() +{ + if (initialized_) + { + EvidentHubWin* hub = GetHub(); + if (hub) + { + EnableNotifications(false); + hub->UnRegisterDeviceAsUsed(EvidentIX85Win::DeviceType_OffsetLens); + } + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentOffsetLens::Busy() +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return false; + + return hub->GetModel()->IsBusy(EvidentIX85Win::DeviceType_OffsetLens); +} + +int EvidentOffsetLens::SetPositionUm(double pos) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert μm to steps + long steps = static_cast(pos / stepSizeUm_); + + // Clamp to limits + if (steps < OFFSET_LENS_MIN_POS) steps = OFFSET_LENS_MIN_POS; + if (steps > OFFSET_LENS_MAX_POS) steps = OFFSET_LENS_MAX_POS; + + hub->GetModel()->SetBusy(EvidentIX85Win::DeviceType_OffsetLens, true); + + std::string cmd = BuildCommand(CMD_OFFSET_LENS_GOTO, static_cast(steps)); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + hub->GetModel()->SetBusy(EvidentIX85Win::DeviceType_OffsetLens, false); + return ret; + } + + if (!IsPositiveAck(response, CMD_OFFSET_LENS_GOTO)) + { + hub->GetModel()->SetBusy(EvidentIX85Win::DeviceType_OffsetLens, false); + return ERR_NEGATIVE_ACK; + } + + hub->GetModel()->SetPosition(EvidentIX85Win::DeviceType_OffsetLens, steps); + hub->GetModel()->SetBusy(EvidentIX85Win::DeviceType_OffsetLens, false); + + return DEVICE_OK; +} + +int EvidentOffsetLens::GetPositionUm(double& pos) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + long steps = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_OffsetLens); + pos = steps * stepSizeUm_; + return DEVICE_OK; +} + +int EvidentOffsetLens::SetPositionSteps(long steps) +{ + return SetPositionUm(steps * stepSizeUm_); +} + +int EvidentOffsetLens::GetPositionSteps(long& steps) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + steps = hub->GetModel()->GetPosition(EvidentIX85Win::DeviceType_OffsetLens); + return DEVICE_OK; +} + +int EvidentOffsetLens::SetOrigin() +{ + // Not supported - origin is factory set + return DEVICE_UNSUPPORTED_COMMAND; +} + +int EvidentOffsetLens::GetLimits(double& lower, double& upper) +{ + lower = OFFSET_LENS_MIN_POS * stepSizeUm_; + upper = OFFSET_LENS_MAX_POS * stepSizeUm_; + return DEVICE_OK; +} + +int EvidentOffsetLens::OnPosition(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + double pos; + int ret = GetPositionUm(pos); + if (ret != DEVICE_OK) + return ret; + pProp->Set(pos); + } + else if (eAct == MM::AfterSet) + { + double pos; + pProp->Get(pos); + int ret = SetPositionUm(pos); + if (ret != DEVICE_OK) + return ret; + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentOffsetLens::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +int EvidentOffsetLens::EnableNotifications(bool enable) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + return hub->EnableNotification(CMD_OFFSET_LENS_NOTIFY, enable); +} + + +/////////////////////////////////////////////////////////////////////////////// +// EvidentZDCVirtualOffset - Virtual Offset Implementation +/////////////////////////////////////////////////////////////////////////////// + + + EvidentZDCVirtualOffset::EvidentZDCVirtualOffset() : + initialized_(false), + name_(g_ZDCVirtualOffsetDeviceName), + // since we operate on the Focus drive, the step size is the same as Focus + stepSizeUm_(FOCUS_STEP_SIZE_UM) +{ + InitializeDefaultErrorMessages(); + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Offset lens not available on this microscope"); + + CreateHubIDProperty(); +} + +EvidentZDCVirtualOffset::~EvidentZDCVirtualOffset() +{ + Shutdown(); +} + +void EvidentZDCVirtualOffset::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +int EvidentZDCVirtualOffset::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + if (!hub->IsDevicePresent(EvidentIX85Win::DeviceType_Autofocus)) + return ERR_DEVICE_NOT_AVAILABLE; + + // Get initial position from model + long offset = hub->GetModel()->GetMeasuredZOffset(); + double offsetUm = offset * stepSizeUm_; + + // Create Position property + CPropertyAction* pAct = new CPropertyAction(this, &EvidentZDCVirtualOffset::OnPosition); + int ret = CreateProperty(MM::g_Keyword_Position, std::to_string(offsetUm).c_str(), MM::Float, false, pAct); + if (ret != DEVICE_OK) + return ret; + + hub->RegisterDeviceAsUsed(DeviceType_ZDCVirtualOffset, this); + + initialized_ = true; + return DEVICE_OK; +} + +int EvidentZDCVirtualOffset::Shutdown() +{ + if (initialized_) + { + // Unregister from hub + EvidentHubWin* hub = GetHub(); + if (hub) + hub->UnRegisterDeviceAsUsed(DeviceType_ZDCVirtualOffset); + + initialized_ = false; + } + return DEVICE_OK; +} + +bool EvidentZDCVirtualOffset::Busy() +{ + // Virtual offset is never busy + return false; +} + +int EvidentZDCVirtualOffset::SetPositionUm(double pos) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + // Convert μm to steps + long steps = static_cast(pos / stepSizeUm_); + + // Update the model + hub->GetModel()->SetMeasuredZOffset(steps); + + // Notify both devices that the offset has changed + hub->NotifyMeasuredZOffsetChanged(steps); + + return DEVICE_OK; +} + +int EvidentZDCVirtualOffset::GetPositionUm(double& pos) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + long steps = hub->GetModel()->GetMeasuredZOffset(); + pos = steps * stepSizeUm_; + return DEVICE_OK; +} + +int EvidentZDCVirtualOffset::SetPositionSteps(long steps) +{ + return SetPositionUm(steps * stepSizeUm_); +} + +int EvidentZDCVirtualOffset::GetPositionSteps(long& steps) +{ + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + steps = hub->GetModel()->GetMeasuredZOffset(); + return DEVICE_OK; +} + +int EvidentZDCVirtualOffset::SetOrigin() +{ + // Set current position as zero offset + EvidentHubWin* hub = GetHub(); + if (!hub) + return DEVICE_ERR; + + hub->GetModel()->SetMeasuredZOffset(0); + hub->NotifyMeasuredZOffsetChanged(0); + + return DEVICE_OK; +} + +int EvidentZDCVirtualOffset::GetLimits(double& lower, double& upper) +{ + // Use focus limits as reference + lower = FOCUS_MIN_POS * stepSizeUm_; + upper = FOCUS_MAX_POS * stepSizeUm_; + return DEVICE_OK; +} + +int EvidentZDCVirtualOffset::OnPosition(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + double pos; + int ret = GetPositionUm(pos); + if (ret != DEVICE_OK) + return ret; + pProp->Set(pos); + } + else if (eAct == MM::AfterSet) + { + double pos; + pProp->Get(pos); + int ret = SetPositionUm(pos); + if (ret != DEVICE_OK) + return ret; + } + return DEVICE_OK; +} + +EvidentHubWin* EvidentZDCVirtualOffset::GetHub() +{ + MM::Hub* hub = GetParentHub(); + if (!hub) + return nullptr; + return dynamic_cast(hub); +} + +void EvidentZDCVirtualOffset::UpdateMeasuredZOffset(long offsetSteps) +{ + // Update property to reflect new offset + double offsetUm = offsetSteps * stepSizeUm_; + std::ostringstream valStr; + valStr << offsetUm; + OnPropertyChanged(MM::g_Keyword_Position, valStr.str().c_str()); + OnStagePositionChanged(offsetUm); +} diff --git a/DeviceAdapters/EvidentIX85Win/EvidentIX85Win.h b/DeviceAdapters/EvidentIX85Win/EvidentIX85Win.h new file mode 100644 index 000000000..6c42fb2f2 --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentIX85Win.h @@ -0,0 +1,632 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentIX85Win.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 microscope device classes +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include "DeviceBase.h" +#include "EvidentHubWin.h" +#include "EvidentModelWin.h" +#include "EvidentProtocolWin.h" + +////////////////////////////////////////////////////////////////////////////// +// Focus Drive (Z-Stage) +////////////////////////////////////////////////////////////////////////////// + +class EvidentHubWin; + +class EvidentFocus : public CStageBase +{ +public: + EvidentFocus(); + ~EvidentFocus(); + + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Stage API + int SetPositionUm(double pos); + int GetPositionUm(double& pos); + int SetPositionSteps(long steps); + int GetPositionSteps(long& steps); + int SetOrigin(); + int GetLimits(double& lower, double& upper); + int IsStageSequenceable(bool& isSequenceable) const { isSequenceable = false; return DEVICE_OK; }; + bool IsContinuousFocusDrive() const { return false; }; + + // Action interface + int OnPosition(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnSpeed(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + double stepSizeUm_; +}; + +////////////////////////////////////////////////////////////////////////////// +// Nosepiece (Objective Turret) +////////////////////////////////////////////////////////////////////////////// + +class EvidentNosepiece : public CStateDeviceBase +{ +public: + EvidentNosepiece(); + ~EvidentNosepiece(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnObjectiveNA(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnObjectiveMagnification(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnObjectiveMedium(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnObjectiveWD(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnNearLimit(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnSetNearLimit(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnParfocalPosition(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnSetParfocalPosition(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnParfocalEnabled(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnEscapeDistance(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + int EnableNotifications(bool enable); + int QueryNearLimits(); // Query and store near limits from microscope + int QueryParfocalSettings(); // Query parfocal positions and enabled state + int SendParfocalSettings(std::vector parfocalPositions); // Send parfocal positions and enabled state to microscope + + bool initialized_; + std::string name_; + unsigned int numPos_; + std::vector nearLimits_; // Focus near limits for each objective (in steps) + std::vector parfocalPositions_; // Parfocal positions for each objective (in steps) + bool parfocalEnabled_; // Whether parfocal is enabled + int escapeDistance_; // Focus escape distance (0-9 mm) +}; + +////////////////////////////////////////////////////////////////////////////// +// Magnification Changer +////////////////////////////////////////////////////////////////////////////// + +class EvidentMagnification : public CMagnifierBase +{ +public: + EvidentMagnification(); + ~EvidentMagnification(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // CMagnifierBase API + double GetMagnification(); + + // Action interface + int OnMagnification(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; + static const double magnifications_[3]; // 1.0x, 1.6x, 2.0x +}; + +////////////////////////////////////////////////////////////////////////////// +// Light Path Selector +////////////////////////////////////////////////////////////////////////////// + +class EvidentLightPath : public CStateDeviceBase +{ +public: + EvidentLightPath(); + ~EvidentLightPath(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + + bool initialized_; + std::string name_; +}; + +////////////////////////////////////////////////////////////////////////////// +// Condenser Turret +////////////////////////////////////////////////////////////////////////////// + +class EvidentCondenserTurret : public CStateDeviceBase +{ +public: + EvidentCondenserTurret(); + ~EvidentCondenserTurret(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; +}; + +////////////////////////////////////////////////////////////////////////////// +// DIA Shutter +////////////////////////////////////////////////////////////////////////////// + +class EvidentDIAShutter : public CShutterBase +{ +public: + EvidentDIAShutter(); + ~EvidentDIAShutter(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Shutter API + int SetOpen(bool open = true); + int GetOpen(bool& open); + int Fire(double deltaT); + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnBrightness(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnMechanicalShutter(MM::PropertyBase* pProp, MM::ActionType eAct); + + // Notification control + int EnableNotifications(bool enable); + +private: + EvidentHubWin* GetHub(); + + bool initialized_; + std::string name_; +}; + +////////////////////////////////////////////////////////////////////////////// +// EPI Shutter 1 +////////////////////////////////////////////////////////////////////////////// + +class EvidentEPIShutter1 : public CShutterBase +{ +public: + EvidentEPIShutter1(); + ~EvidentEPIShutter1(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Shutter API + int SetOpen(bool open = true); + int GetOpen(bool& open); + int Fire(double deltaT); + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + + bool initialized_; + std::string name_; +}; + +////////////////////////////////////////////////////////////////////////////// +// Mirror Unit 1 (Filter Cube Turret) +////////////////////////////////////////////////////////////////////////////// + +class EvidentMirrorUnit1 : public CStateDeviceBase +{ +public: + EvidentMirrorUnit1(); + ~EvidentMirrorUnit1(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; +}; + +////////////////////////////////////////////////////////////////////////////// +// EPI Shutter 2 +////////////////////////////////////////////////////////////////////////////// + +class EvidentEPIShutter2 : public CShutterBase +{ +public: + EvidentEPIShutter2(); + ~EvidentEPIShutter2(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Shutter API + int SetOpen(bool open = true); + int GetOpen(bool& open); + int Fire(double deltaT); + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + + bool initialized_; + std::string name_; +}; + +////////////////////////////////////////////////////////////////////////////// +// Mirror Unit 2 (Filter Cube Turret) +////////////////////////////////////////////////////////////////////////////// + +class EvidentMirrorUnit2 : public CStateDeviceBase +{ +public: + EvidentMirrorUnit2(); + ~EvidentMirrorUnit2(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; +}; + +////////////////////////////////////////////////////////////////////////////// +// Polarizer +////////////////////////////////////////////////////////////////////////////// + +class EvidentPolarizer : public CStateDeviceBase +{ +public: + EvidentPolarizer(); + ~EvidentPolarizer(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; +}; + +////////////////////////////////////////////////////////////////////////////// +// DIC Prism +////////////////////////////////////////////////////////////////////////////// + +class EvidentDICPrism : public CStateDeviceBase +{ +public: + EvidentDICPrism(); + ~EvidentDICPrism(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; +}; + +////////////////////////////////////////////////////////////////////////////// +// EPI ND Filter +////////////////////////////////////////////////////////////////////////////// + +class EvidentEPIND : public CStateDeviceBase +{ +public: + EvidentEPIND(); + ~EvidentEPIND(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + unsigned long GetNumberOfPositions() const; + + // Action interface + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + unsigned int numPos_; +}; + +////////////////////////////////////////////////////////////////////////////// +// Correction Collar +////////////////////////////////////////////////////////////////////////////// + +class EvidentCorrectionCollar : public CStageBase +{ +public: + EvidentCorrectionCollar(); + ~EvidentCorrectionCollar(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Stage API + int SetPositionUm(double pos); + int GetPositionUm(double& pos); + int SetPositionSteps(long steps); + int GetPositionSteps(long& steps); + int SetOrigin(); + int GetLimits(double& lower, double& upper); + int IsStageSequenceable(bool& isSequenceable) const { isSequenceable = false; return DEVICE_OK; }; + bool IsContinuousFocusDrive() const { return false; }; + + // Action interface + int OnActivate(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + + bool initialized_; + bool linked_; + std::string name_; + double stepSizeUm_; +}; + +////////////////////////////////////////////////////////////////////////////// +// ZDC Autofocus +////////////////////////////////////////////////////////////////////////////// + +class EvidentAutofocus : public CAutoFocusBase +{ +public: + EvidentAutofocus(); + ~EvidentAutofocus(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // AutoFocus API + int SetContinuousFocusing(bool state); + int GetContinuousFocusing(bool& state); + bool IsContinuousFocusLocked(); + int FullFocus(); + int IncrementalFocus(); + int GetLastFocusScore(double& score); + int GetCurrentFocusScore(double& score); + int GetOffset(double& offset); + int SetOffset(double offset); + int GetMeasuredZOffset(double& offset); + int SetMeasuredZOffset(double offset); + + // Action interface + int OnAFStatus(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnNearLimit(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnFarLimit(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnCoverSlipType(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnCoverSlipThicknessGlass(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnCoverSlipThicknessPlastic(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnDICMode(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnBuzzerSuccess(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnBuzzerFailure(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnWorkflowMode(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnMeasuredFocusOffset(MM::PropertyBase* pProp, MM::ActionType eAct); + + // Public method for hub to update AF status from notifications + void UpdateAFStatus(int status); + void UpdateMeasuredZOffset(long offsetSteps); + + +private: + EvidentHubWin* GetHub(); + int EnableNotifications(bool enable); + int StopAF(); + int InitializeZDC(); // Run full ZDC initialization sequence + std::string GetAFStatusString(int status); + int MeasureZOffset(); // Mode 1: Measure Z-offset + int FindFocusWithOffset(); // Mode 2: Find focus and apply offset + + bool initialized_; + std::string name_; + bool continuousFocusing_; + int afStatus_; // 0=Stop, 1=Focus, 2=Track, 3=Wait, 4=Search + long nearLimit_; + long farLimit_; + long lastNosepiecePos_; // Track objective changes + int lastCoverslipType_; // Track coverslip type changes + bool zdcInitNeeded_; // Flag to defer ZDC initialization + int workflowMode_; // 1=Measure Offset, 2=Find Focus with Offset, 3=Continuous Focus +}; + +////////////////////////////////////////////////////////////////////////////// +// Offset Lens (ZDC) +////////////////////////////////////////////////////////////////////////////// + +class EvidentOffsetLens : public CStageBase +{ +public: + EvidentOffsetLens(); + ~EvidentOffsetLens(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Stage API + int SetPositionUm(double pos); + int GetPositionUm(double& pos); + int SetPositionSteps(long steps); + int GetPositionSteps(long& steps); + int SetOrigin(); + int GetLimits(double& lower, double& upper); + int IsStageSequenceable(bool& isSequenceable) const { isSequenceable = false; return DEVICE_OK; }; + // Check if a stage has continuous focusing capability (positions can be set while continuous focus runs). + bool IsContinuousFocusDrive() const { return true; }; + + // Action interface + int OnPosition(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + int EnableNotifications(bool enable); + + bool initialized_; + std::string name_; + double stepSizeUm_; +}; + +////////////////////////////////////////////////////////////////////////////// +// ZDC Virtual Offset +////////////////////////////////////////////////////////////////////////////// + +class EvidentZDCVirtualOffset : public CStageBase +{ +public: + EvidentZDCVirtualOffset(); + ~EvidentZDCVirtualOffset(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Stage API + int SetPositionUm(double pos); + int GetPositionUm(double& pos); + int SetPositionSteps(long steps); + int GetPositionSteps(long& steps); + int SetOrigin(); + int GetLimits(double& lower, double& upper); + int IsStageSequenceable(bool& isSequenceable) const { isSequenceable = false; return DEVICE_OK; }; + bool IsContinuousFocusDrive() const { return true; }; + + // Action interface + int OnPosition(MM::PropertyBase* pProp, MM::ActionType eAct); + + // Public method for hub to notify of measured offset changes + void UpdateMeasuredZOffset(long offsetSteps); + +private: + EvidentHubWin* GetHub(); + + bool initialized_; + std::string name_; + double stepSizeUm_; +}; diff --git a/DeviceAdapters/EvidentIX85Win/EvidentIX85Win.vcxproj b/DeviceAdapters/EvidentIX85Win/EvidentIX85Win.vcxproj new file mode 100644 index 000000000..6c823246e --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentIX85Win.vcxproj @@ -0,0 +1,126 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {F2B9E7A6-3C5D-4F1E-8D2A-9B4C7E6F3A5D} + EvidentIX85Win + Win32Proj + 10.0 + + + + DynamicLibrary + MultiByte + v143 + false + + + DynamicLibrary + MultiByte + v143 + true + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + true + false + + + + X64 + + + Disabled + true + Speed + NOMINMAX;WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + EnableFastChecks + true + + + 4290;%(DisableSpecificWarnings) + stdcpp17 + $(MM_3RDPARTYPRIVATE)\Evident\IX5_SDK_v1\PortManager_SampleApp\02_src\SDK_sample_src\include;%(AdditionalIncludeDirectories) + + + Windows + + + %(AdditionalDependencies) + %(AdditionalLibraryDirectories) + + + + + X64 + + + MaxSpeed + true + Speed + NOMINMAX;WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + + + 4290;%(DisableSpecificWarnings) + Default + $(MM_3RDPARTYPRIVATE)\Evident\IX5_SDK_v1\PortManager_SampleApp\02_src\SDK_sample_src\include;%(AdditionalIncludeDirectories) + + + Windows + true + true + + + %(AdditionalDependencies) + %(AdditionalLibraryDirectories) + + + + + + + + + + + + + + + + + + + + + {b8c95f39-54bf-40a9-807b-598df2821d55} + + + + + + \ No newline at end of file diff --git a/DeviceAdapters/EvidentIX85Win/EvidentIX85Win.vcxproj.filters b/DeviceAdapters/EvidentIX85Win/EvidentIX85Win.vcxproj.filters new file mode 100644 index 000000000..2f372be7a --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentIX85Win.vcxproj.filters @@ -0,0 +1,57 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/DeviceAdapters/EvidentIX85Win/EvidentLensDatabase.h b/DeviceAdapters/EvidentIX85Win/EvidentLensDatabase.h new file mode 100644 index 000000000..2eb60de74 --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentLensDatabase.h @@ -0,0 +1,345 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentLensDatabase.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident objective lens database from SDK LensInfo.unit +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include +#include + +namespace EvidentLens { + +// Immersion medium types (from SDK SubType values) +enum ImmersionType +{ + Immersion_Dry = 0x00106010, + Immersion_Water = 0x00106011, + Immersion_Oil = 0x00106012, + Immersion_Silicon = 0x00106013, + Immersion_Gel = 0x00106016 +}; + +// Convert immersion type to string +inline const char* ImmersionTypeToString(ImmersionType type) +{ + switch (type) + { + case Immersion_Dry: return "Dry"; + case Immersion_Water: return "Water"; + case Immersion_Oil: return "Oil"; + case Immersion_Silicon: return "Silicon"; + case Immersion_Gel: return "Gel"; + default: return "Unknown"; + } +} + +// Lens information structure +struct LensInfo +{ + const char* model; + unsigned int modelCode; + double magnification; + ImmersionType immersion; + double na; + double wd; +}; + +// Complete lens database from SDK LensInfo.unit file +static const LensInfo LENS_DATABASE[] = { + // Lens0 + {"UPLSAPO4X", 0x125010, 4.0, Immersion_Dry, 0.160, 13.0}, + // Lens1 + {"UPLSAPO10X2", 0x125011, 10.0, Immersion_Dry, 0.400, 3.1}, + // Lens2 + {"UPLSAPO20X", 0x125012, 20.0, Immersion_Dry, 0.750, 0.6}, + // Lens3 + {"UPLSAPO20XO", 0x125013, 20.0, Immersion_Oil, 0.850, 0.17}, + // Lens4 + {"UPLSAPO30XS", 0x125014, 30.0, Immersion_Silicon, 1.050, 0.8}, + // Lens5 + {"UPLSAPO40X2", 0x125015, 40.0, Immersion_Dry, 0.950, 0.18}, + // Lens6 + {"UPLSAPO60XW", 0x125016, 60.0, Immersion_Water, 1.200, 0.28}, + // Lens7 + {"UPLSAPO60XO", 0x125017, 60.0, Immersion_Oil, 1.350, 0.15}, + // Lens8 + {"UPLSAPO60XS", 0x125018, 60.0, Immersion_Silicon, 1.300, 0.3}, + // Lens9 + {"UPLSAPO100XO", 0x125019, 100.0, Immersion_Oil, 1.400, 0.13}, + // Lens10 + {"PLAPON1.25X", 0x12501a, 1.25, Immersion_Dry, 0.040, 5.0}, + // Lens11 + {"PLAPON2X", 0x12501b, 2.0, Immersion_Dry, 0.080, 6.2}, + // Lens12 + {"PLAPON60XO", 0x12501c, 60.0, Immersion_Oil, 1.420, 0.15}, + // Lens13 + {"PLAPON60XOSC", 0x12501d, 60.0, Immersion_Oil, 1.400, 0.12}, + // Lens14 + {"UPLFLN4X", 0x12501e, 4.0, Immersion_Dry, 0.130, 17.0}, + // Lens15 + {"UPLFLN10X2", 0x12501f, 10.0, Immersion_Dry, 0.300, 10.0}, + // Lens16 + {"UPLFLN20X", 0x125020, 20.0, Immersion_Dry, 0.500, 2.1}, + // Lens17 + {"UPLFLN40X", 0x125021, 40.0, Immersion_Dry, 0.750, 0.51}, + // Lens18 + {"UPLFLN40XO", 0x125022, 40.0, Immersion_Oil, 1.300, 0.2}, + // Lens19 + {"UPLFLN60X", 0x125023, 60.0, Immersion_Dry, 0.900, 0.2}, + // Lens20 + {"UPLFLN60XOI", 0x125024, 60.0, Immersion_Oil, 1.250, 0.12}, + // Lens21 + {"UPLFLN100XO2", 0x125025, 100.0, Immersion_Oil, 1.300, 0.2}, + // Lens22 + {"UPLFLN100XOI2", 0x125026, 100.0, Immersion_Oil, 1.300, 0.2}, + // Lens23 + {"UPLFLN4XPH", 0x125027, 4.0, Immersion_Dry, 0.130, 17.0}, + // Lens24 + {"UPLFLN10X2PH", 0x125028, 10.0, Immersion_Dry, 0.300, 10.0}, + // Lens25 + {"UPLFLN20XPH", 0x125029, 20.0, Immersion_Dry, 0.500, 2.1}, + // Lens26 + {"UPLFLN40XPH", 0x12502a, 40.0, Immersion_Dry, 0.750, 0.51}, + // Lens27 + {"UPLFLN60XOIPH", 0x12502b, 60.0, Immersion_Oil, 1.250, 0.12}, + // Lens28 + {"UPLFLN100XO2PH", 0x12502c, 100.0, Immersion_Oil, 1.300, 0.2}, + // Lens29 + {"UPLFLN4XP", 0x12502d, 4.0, Immersion_Dry, 0.130, 17.0}, + // Lens30 + {"UPLFLN10XP", 0x12502e, 10.0, Immersion_Dry, 0.300, 10.0}, + // Lens31 + {"UPLFLN20XP", 0x12502f, 20.0, Immersion_Dry, 0.500, 2.1}, + // Lens32 + {"UPLFLN40XP", 0x125030, 40.0, Immersion_Dry, 0.750, 0.51}, + // Lens33 + {"UPLFLN100XOP", 0x125031, 100.0, Immersion_Oil, 1.300, 0.2}, + // Lens34 + {"LUCPLFLN20X", 0x125032, 20.0, Immersion_Dry, 0.450, 6.6}, + // Lens35 + {"LUCPLFLN40X", 0x125033, 40.0, Immersion_Dry, 0.600, 2.7}, + // Lens36 + {"LUCPLFLN60X", 0x125034, 60.0, Immersion_Dry, 0.700, 1.5}, + // Lens37 + {"CPLFLN10XPH", 0x125035, 10.0, Immersion_Dry, 0.300, 9.5}, + // Lens38 + {"LUCPLFLN20XPH", 0x125036, 20.0, Immersion_Dry, 0.450, 6.6}, + // Lens39 + {"LUCPLFLN40XPH", 0x125037, 40.0, Immersion_Dry, 0.600, 3.0}, + // Lens40 + {"LUCPLFLN60XPH", 0x125038, 60.0, Immersion_Dry, 0.700, 1.5}, + // Lens41 + {"CPLFLN10XRC", 0x125039, 10.0, Immersion_Dry, 0.300, 9.0}, + // Lens42 + {"LUCPLFLN20XRC", 0x12503a, 20.0, Immersion_Dry, 0.450, 6.6}, + // Lens43 + {"LUCPLFLN40XRC", 0x12503b, 40.0, Immersion_Dry, 0.600, 3.0}, + // Lens44 + {"CPLN10XPH", 0x12503c, 10.0, Immersion_Dry, 0.250, 10.0}, + // Lens45 + {"LCACHN20XPH", 0x12503d, 20.0, Immersion_Dry, 0.400, 3.2}, + // Lens46 + {"LCACHN40XPH", 0x12503e, 40.0, Immersion_Dry, 0.550, 2.2}, + // Lens47 + {"CPLN10XRC", 0x12503f, 10.0, Immersion_Dry, 0.250, 9.7}, + // Lens48 + {"LCACHN20XRC", 0x125040, 20.0, Immersion_Dry, 0.400, 2.8}, + // Lens49 + {"LCACHN40XRC", 0x125041, 40.0, Immersion_Dry, 0.550, 1.9}, + // Lens50 + {"UAPON20XW340", 0x125042, 20.0, Immersion_Water, 0.700, 0.35}, + // Lens51 + {"UAPON40XO340", 0x125043, 40.0, Immersion_Oil, 1.350, 0.1}, + // Lens52 + {"UAPON40XW340", 0x125044, 40.0, Immersion_Water, 1.150, 0.25}, + // Lens53 + {"APON60XOTIRF", 0x125045, 60.0, Immersion_Oil, 1.490, 0.1}, + // Lens54 + {"UAPON100XOTIRF", 0x125046, 100.0, Immersion_Oil, 1.490, 0.1}, + // Lens55 + {"UAPON150XOTIRF", 0x125047, 150.0, Immersion_Oil, 1.450, 0.08}, + // Lens56 + {"MPLFLN1.25X", 0x125048, 1.25, Immersion_Dry, 0.040, 3.5}, + // Lens57 + {"MPLN5X", 0x125049, 5.0, Immersion_Dry, 0.100, 20.0}, + // Lens58 + {"LCPLFLN100XLCD", 0x12504a, 100.0, Immersion_Dry, 0.850, 0.9}, + // Lens59 + {"UCPLFLN20X", 0x12504b, 20.0, Immersion_Dry, 0.700, 0.8}, + // Lens60 + {"UCPLFLN20XPH", 0x12504c, 20.0, Immersion_Dry, 0.700, 0.8}, + // Lens61 + {"UPLSAPO100XOPH", 0x12504d, 100.0, Immersion_Oil, 1.400, 0.13}, + // Lens62 + {"APON100XHOTIRF", 0x12504e, 100.0, Immersion_Oil, 1.700, 0.08}, + // Lens63 + {"PLAPON60XOPH", 0x12504f, 60.0, Immersion_Oil, 1.420, 0.15}, + // Lens64 + {"UPLSAPO40XS", 0x125050, 40.0, Immersion_Silicon, 1.250, 0.3}, + // Lens65 + {"UAPON40XO340-2", 0x125051, 40.0, Immersion_Oil, 1.350, 0.1}, + // Lens66 + {"PLAPON60XOSC2", 0x125052, 60.0, Immersion_Oil, 1.400, 0.12}, + // Lens67 + {"LMPLFLN5X", 0x125053, 5.0, Immersion_Dry, 0.130, 22.5}, + // Lens68 + {"LMPLFLN10X", 0x125054, 10.0, Immersion_Dry, 0.250, 21.0}, + // Lens69 + {"LMPLFLN20X", 0x125055, 20.0, Immersion_Dry, 0.400, 12.0}, + // Lens70 + {"LMPLFLN50X", 0x125056, 50.0, Immersion_Dry, 0.500, 10.6}, + // Lens71 + {"UPLSAPO30XSIR", 0x125057, 30.0, Immersion_Silicon, 1.050, 0.8}, + // Lens72 + {"UPLSAPO100XS", 0x125058, 100.0, Immersion_Silicon, 1.350, 0.2}, + // Lens73 + {"LUMFLN60XW", 0x125059, 60.0, Immersion_Water, 1.100, 1.5}, + // Lens74 + {"UPLSAPO60XS2", 0x12505a, 60.0, Immersion_Silicon, 1.300, 0.3}, + // Lens75 + {"UPLXAPO4X", 0x12505b, 4.0, Immersion_Dry, 0.160, 13.0}, + // Lens76 + {"UPLXAPO10X", 0x12505c, 10.0, Immersion_Dry, 0.400, 3.1}, + // Lens77 + {"UPLXAPO20X", 0x12505d, 20.0, Immersion_Dry, 0.800, 0.6}, + // Lens78 + {"UPLXAPO40X", 0x12505e, 40.0, Immersion_Dry, 0.950, 0.18}, + // Lens79 + {"UPLXAPO40XO", 0x12505f, 40.0, Immersion_Oil, 1.400, 0.13}, + // Lens80 + {"UPLXAPO60XO", 0x125060, 60.0, Immersion_Oil, 1.420, 0.15}, + // Lens81 + {"UPLXAPO60XOPH", 0x125061, 60.0, Immersion_Oil, 1.420, 0.15}, + // Lens82 + {"UPLXAPO100XO", 0x125062, 100.0, Immersion_Oil, 1.450, 0.13}, + // Lens83 + {"UPLXAPO100XOPH", 0x125063, 100.0, Immersion_Oil, 1.450, 0.13}, + // Lens84 + {"UPLAPO60XOHR", 0x125064, 60.0, Immersion_Oil, 1.500, 0.11}, + // Lens85 + {"UPLAPO100XOHR", 0x125065, 100.0, Immersion_Oil, 1.500, 0.12}, + // Lens86 + {"LUCPLFLN20X2", 0x125066, 20.0, Immersion_Dry, 0.450, 6.6}, + // Lens87 + {"LUCPLFLN20XPH2", 0x125067, 20.0, Immersion_Dry, 0.450, 6.6}, + // Lens88 + {"LUCPLFLN20XRC2", 0x125068, 20.0, Immersion_Dry, 0.450, 6.6}, + // Lens89 + {"LUCPLFLN40X2", 0x125069, 40.0, Immersion_Dry, 0.600, 2.7}, + // Lens90 + {"LUCPLFLN40XPH2", 0x12506a, 40.0, Immersion_Dry, 0.600, 3.0}, + // Lens91 + {"LUCPLFLN40XRC2", 0x12506b, 40.0, Immersion_Dry, 0.600, 3.0}, + // Lens92 + {"LUCPLFLN60X2", 0x12506c, 60.0, Immersion_Dry, 0.700, 1.5}, + // Lens93 + {"LUCPLFLN60XPH2", 0x12506d, 60.0, Immersion_Dry, 0.700, 1.5}, + // Lens94 + {"UCPLFLN20X2", 0x12506e, 20.0, Immersion_Dry, 0.700, 0.8}, + // Lens95 + {"UCPLFLN20XPH2", 0x12506f, 20.0, Immersion_Dry, 0.700, 0.8}, + // Lens96 + {"UPLXAPO60XW", 0x125070, 60.0, Immersion_Water, 1.200, 0.28}, + // Lens97 + {"LUPLAPO25XS", 0x125071, 25.0, Immersion_Silicon, 0.850, 2.0}, + // Lens98 + {"LUPLAPO25XS_W", 0x125072, 25.0, Immersion_Water, 0.850, 2.0}, + // Lens99 + {"LUPLAPO25XS_GEL", 0x125073, 25.0, Immersion_Gel, 0.850, 2.0}, + // Lens100 + {"MPLFLN2.5X2", 0x125074, 2.5, Immersion_Dry, 0.080, 10.7}, + // Lens101 + {"MPLFLN5X2", 0x125075, 5.0, Immersion_Dry, 0.150, 20.0} +}; + +static const int LENS_DATABASE_SIZE = sizeof(LENS_DATABASE) / sizeof(LensInfo); + +// Helper function: Get all lenses matching a specific magnification +inline std::vector GetLensesByMagnification(double magnification) +{ + std::vector result; + for (int i = 0; i < LENS_DATABASE_SIZE; i++) + { + if (LENS_DATABASE[i].magnification == magnification) + { + result.push_back(&LENS_DATABASE[i]); + } + } + return result; +} + +// Helper function: Get all lenses matching a specific immersion type +inline std::vector GetLensesByImmersion(ImmersionType immersion) +{ + std::vector result; + for (int i = 0; i < LENS_DATABASE_SIZE; i++) + { + if (LENS_DATABASE[i].immersion == immersion) + { + result.push_back(&LENS_DATABASE[i]); + } + } + return result; +} + +// Helper function: Get all lenses matching both magnification and immersion type +inline std::vector GetLensesByMagnificationAndImmersion( + double magnification, ImmersionType immersion) +{ + std::vector result; + for (int i = 0; i < LENS_DATABASE_SIZE; i++) + { + if (LENS_DATABASE[i].magnification == magnification && + LENS_DATABASE[i].immersion == immersion) + { + result.push_back(&LENS_DATABASE[i]); + } + } + return result; +} + +// Helper function: Find lens by model name +inline const LensInfo* GetLensByModel(const char* model) +{ + for (int i = 0; i < LENS_DATABASE_SIZE; i++) + { + if (strcmp(LENS_DATABASE[i].model, model) == 0) + { + return &LENS_DATABASE[i]; + } + } + return nullptr; +} + +// Helper function: Find lens by model code +inline const LensInfo* GetLensByModelCode(unsigned int modelCode) +{ + for (int i = 0; i < LENS_DATABASE_SIZE; i++) + { + if (LENS_DATABASE[i].modelCode == modelCode) + { + return &LENS_DATABASE[i]; + } + } + return nullptr; +} + +} // namespace EvidentLens diff --git a/DeviceAdapters/EvidentIX85Win/EvidentModelWin.cpp b/DeviceAdapters/EvidentIX85Win/EvidentModelWin.cpp new file mode 100644 index 000000000..ee3b7fd03 --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentModelWin.cpp @@ -0,0 +1,229 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentModel.cpp +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 microscope state model implementation +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#include "EvidentModelWin.h" +#include + +namespace EvidentIX85Win { + +MicroscopeModel::MicroscopeModel() : + measuredZOffset_(0), + measuredZOffsetValid_(false) +{ +} + +MicroscopeModel::~MicroscopeModel() +{ +} + +long long MicroscopeModel::SteadyMicroseconds() +{ + using namespace std::chrono; + auto now = steady_clock::now().time_since_epoch(); + auto usec = duration_cast(now); + return usec.count(); +} + +void MicroscopeModel::SetDevicePresent(DeviceType type, bool present) +{ + std::lock_guard lock(mutex_); + GetOrCreateState(type).present = present; +} + +bool MicroscopeModel::IsDevicePresent(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).present; +} + +void MicroscopeModel::SetPosition(DeviceType type, long position) +{ + std::lock_guard lock(mutex_); + auto& state = GetOrCreateState(type); + state.currentPos = position; + state.lastUpdateTime = MM::MMTime::fromUs(SteadyMicroseconds()); +} + +long MicroscopeModel::GetPosition(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).currentPos; +} + +bool MicroscopeModel::IsPositionUnknown(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).currentPos == -1; +} + +void MicroscopeModel::SetTargetPosition(DeviceType type, long position) +{ + std::lock_guard lock(mutex_); + auto& state = GetOrCreateState(type); + state.targetPos = position; + state.lastRequestTime = MM::MMTime::fromUs(SteadyMicroseconds()); +} + +long MicroscopeModel::GetTargetPosition(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).targetPos; +} + +void MicroscopeModel::SetBusy(DeviceType type, bool busy) +{ + std::lock_guard lock(mutex_); + GetOrCreateState(type).busy = busy; +} + +bool MicroscopeModel::IsBusy(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).busy; +} + +void MicroscopeModel::SetLimits(DeviceType type, long minPos, long maxPos) +{ + std::lock_guard lock(mutex_); + auto& state = GetOrCreateState(type); + state.minPos = minPos; + state.maxPos = maxPos; +} + +void MicroscopeModel::GetLimits(DeviceType type, long& minPos, long& maxPos) const +{ + std::lock_guard lock(mutex_); + const auto& state = GetStateConst(type); + minPos = state.minPos; + maxPos = state.maxPos; +} + +void MicroscopeModel::SetNumPositions(DeviceType type, int numPos) +{ + std::lock_guard lock(mutex_); + GetOrCreateState(type).numPositions = numPos; +} + +int MicroscopeModel::GetNumPositions(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).numPositions; +} + +void MicroscopeModel::SetLastUpdateTime(DeviceType type, MM::MMTime time) +{ + std::lock_guard lock(mutex_); + GetOrCreateState(type).lastUpdateTime = time; +} + +MM::MMTime MicroscopeModel::GetLastUpdateTime(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).lastUpdateTime; +} + +void MicroscopeModel::SetLastRequestTime(DeviceType type, MM::MMTime time) +{ + std::lock_guard lock(mutex_); + GetOrCreateState(type).lastRequestTime = time; +} + +MM::MMTime MicroscopeModel::GetLastRequestTime(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).lastRequestTime; +} + +DeviceState MicroscopeModel::GetDeviceState(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type); +} + +void MicroscopeModel::SetDeviceState(DeviceType type, const DeviceState& state) +{ + std::lock_guard lock(mutex_); + devices_[type] = state; +} + +void MicroscopeModel::SetDeviceVersion(DeviceType type, const std::string& version) +{ + std::lock_guard lock(mutex_); + GetOrCreateState(type).version = version; +} + +std::string MicroscopeModel::GetDeviceVersion(DeviceType type) const +{ + std::lock_guard lock(mutex_); + return GetStateConst(type).version; +} + +void MicroscopeModel::Clear() +{ + std::lock_guard lock(mutex_); + devices_.clear(); + measuredZOffset_ = 0; + measuredZOffsetValid_ = false; +} + +DeviceState& MicroscopeModel::GetOrCreateState(DeviceType type) +{ + auto it = devices_.find(type); + if (it == devices_.end()) + { + DeviceState newState; + newState.type = type; + devices_[type] = newState; + return devices_[type]; + } + return it->second; +} + +const DeviceState& MicroscopeModel::GetStateConst(DeviceType type) const +{ + static DeviceState emptyState; + auto it = devices_.find(type); + if (it == devices_.end()) + return emptyState; + return it->second; +} + +void MicroscopeModel::SetMeasuredZOffset(long offset) +{ + std::lock_guard lock(mutex_); + measuredZOffset_ = offset; + measuredZOffsetValid_ = true; +} + +long MicroscopeModel::GetMeasuredZOffset() const +{ + std::lock_guard lock(mutex_); + return measuredZOffset_; +} + +bool MicroscopeModel::IsMeasuredZOffsetValid() const +{ + std::lock_guard lock(mutex_); + return measuredZOffsetValid_; +} + +} // namespace EvidentIX85Win diff --git a/DeviceAdapters/EvidentIX85Win/EvidentModelWin.h b/DeviceAdapters/EvidentIX85Win/EvidentModelWin.h new file mode 100644 index 000000000..e5b083050 --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentModelWin.h @@ -0,0 +1,188 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentModel.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 microscope state model +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include "MMDevice.h" +#include +#include +#include + +namespace EvidentIX85Win { + +// Device type enumeration +enum DeviceType +{ + DeviceType_Unknown = 0, + DeviceType_Focus, + DeviceType_Nosepiece, + DeviceType_Magnification, + DeviceType_LightPath, + DeviceType_CondenserTurret, + DeviceType_Condenser, + DeviceType_DIAAperture, + DeviceType_DIAShutter, + DeviceType_DIABrightness, + DeviceType_Polarizer, + DeviceType_DICPrism, + DeviceType_DICRetardation, + DeviceType_EPIShutter1, + DeviceType_EPIShutter2, + DeviceType_MirrorUnit1, + DeviceType_MirrorUnit2, + DeviceType_EPIND, + DeviceType_RightPort, + DeviceType_CorrectionCollar, + DeviceType_Autofocus, + DeviceType_OffsetLens, + DeviceType_ManualControl, + DeviceType_ZDCVirtualOffset +}; + +// Objective lens information structure +struct ObjectiveInfo +{ + std::string name; // p2: Name of objective lens + double na; // p3: Numerical aperture (0.00-2.00, -1 = indefinite) + int magnification; // p4: Magnification (0-200, -1 = indefinite) + int medium; // p5: 1=dry, 2=water, 3=oil, 4=silicone oil, 5=silicone gel, -1=indefinite + int asMin; // p7: AS minimum value % (0-120, -1 = indefinite/unknown) + int asMax; // p8: AS maximum value % (0-120, -1 = indefinite/unknown) + double wd; // p9: Working distance (0.01-25.00, -1 = indefinite) + int zdcOneShotCompat; // p10: ZDC OneShot AF compatibility (0-3) + int zdcContinuousCompat; // p11: ZDC Continuous AF compatibility (0-3) + + ObjectiveInfo() : + name(""), + na(-1.0), + magnification(-1), + medium(-1), + asMin(-1), + asMax(-1), + wd(-1.0), + zdcOneShotCompat(0), + zdcContinuousCompat(0) + {} +}; + +// Device state structure +struct DeviceState +{ + DeviceType type; + bool present; + bool busy; + long currentPos; + long targetPos; + long minPos; + long maxPos; + int numPositions; // For state devices + std::string version; // Firmware version from V command + MM::MMTime lastUpdateTime; + MM::MMTime lastRequestTime; + + DeviceState() : + type(DeviceType_Unknown), + present(false), + busy(false), + currentPos(0), + targetPos(0), + minPos(0), + maxPos(0), + numPositions(0), + version(""), + lastUpdateTime(0.0), + lastRequestTime(0.0) + {} +}; + +// Microscope model - centralized state for all devices +class MicroscopeModel +{ +public: + MicroscopeModel(); + ~MicroscopeModel(); + + // Gets current time in microseconds + static long long SteadyMicroseconds(); + + // Device presence + void SetDevicePresent(DeviceType type, bool present); + bool IsDevicePresent(DeviceType type) const; + + // Position access (thread-safe) + void SetPosition(DeviceType type, long position); + long GetPosition(DeviceType type) const; + bool IsPositionUnknown(DeviceType type) const; + + // Target position + void SetTargetPosition(DeviceType type, long position); + long GetTargetPosition(DeviceType type) const; + + // Busy state + void SetBusy(DeviceType type, bool busy); + bool IsBusy(DeviceType type) const; + + // Position limits + void SetLimits(DeviceType type, long minPos, long maxPos); + void GetLimits(DeviceType type, long& minPos, long& maxPos) const; + + // Number of positions (for state devices) + void SetNumPositions(DeviceType type, int numPos); + int GetNumPositions(DeviceType type) const; + + // Firmware version + void SetDeviceVersion(DeviceType type, const std::string& version); + std::string GetDeviceVersion(DeviceType type) const; + + // Timestamps + void SetLastUpdateTime(DeviceType type, MM::MMTime time); + MM::MMTime GetLastUpdateTime(DeviceType type) const; + + void SetLastRequestTime(DeviceType type, MM::MMTime time); + MM::MMTime GetLastRequestTime(DeviceType type) const; + + // Full state access (for initialization) + DeviceState GetDeviceState(DeviceType type) const; + void SetDeviceState(DeviceType type, const DeviceState& state); + + // Clear all state + void Clear(); + + // ZDC measured offset management (in steps) + void SetMeasuredZOffset(long offset); + long GetMeasuredZOffset() const; + bool IsMeasuredZOffsetValid() const; + +private: + mutable std::mutex mutex_; + std::map devices_; + + // ZDC measured offset (in steps) - shared between Autofocus and ZDCVirtualOffset + long measuredZOffset_; + bool measuredZOffsetValid_; + + // Helper to get or create device state + DeviceState& GetOrCreateState(DeviceType type); + const DeviceState& GetStateConst(DeviceType type) const; +}; + +} // namespace EvidentIX85Win diff --git a/DeviceAdapters/EvidentIX85Win/EvidentObjectiveSetup.cpp b/DeviceAdapters/EvidentIX85Win/EvidentObjectiveSetup.cpp new file mode 100644 index 000000000..c8fac060c --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentObjectiveSetup.cpp @@ -0,0 +1,825 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentObjectiveSetup.cpp +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 objective setup device - sends full objective +// specifications to SDK using S_SOB command +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#include "EvidentObjectiveSetup.h" +#include +#include + +using namespace EvidentIX85Win; + +/////////////////////////////////////////////////////////////////////////////// +// EvidentObjectiveSetup implementation +/////////////////////////////////////////////////////////////////////////////// + +EvidentObjectiveSetup::EvidentObjectiveSetup() : + initialized_(false), + name_("IX85-ObjectiveSetup"), + filterMagnification_("All"), + filterImmersion_("All") +{ + // Initialize arrays + for (int i = 0; i < 6; i++) + { + selectedLensModel_[i] = "NONE"; // Default to NONE + detectedObjectives_[i].name = ""; + detectedObjectives_[i].na = 0.0; + detectedObjectives_[i].magnification = 0.0; + detectedObjectives_[i].medium = 1; // Default to Dry + detectedObjectives_[i].detected = false; + + // Initialize special objective specs + specialNA_[i] = 0.16; // Default NA + specialMagnification_[i] = 10.0; // Default magnification + specialImmersion_[i] = "Dry"; // Default immersion + } + + InitializeDefaultErrorMessages(); + + // Custom error messages + SetErrorText(ERR_DEVICE_NOT_AVAILABLE, "Hub device not found. Please initialize EvidentHubWin first."); + SetErrorText(ERR_NEGATIVE_ACK, "Microscope rejected command (negative acknowledgment)."); + SetErrorText(ERR_INVALID_RESPONSE, "Invalid response from microscope."); + + // Hub property (pre-initialization) + CreateHubIDProperty(); +} + +EvidentObjectiveSetup::~EvidentObjectiveSetup() +{ + Shutdown(); +} + +void EvidentObjectiveSetup::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +bool EvidentObjectiveSetup::Busy() +{ + return false; // This is a setup device, not busy during normal operation +} + +EvidentHubWin* EvidentObjectiveSetup::GetHub() +{ + MM::Device* device = GetParentHub(); + if (device == 0) + return 0; + + EvidentHubWin* hub = dynamic_cast(device); + return hub; +} + +int EvidentObjectiveSetup::Shutdown() +{ + if (initialized_) + { + initialized_ = false; + } + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// Initialization +/////////////////////////////////////////////////////////////////////////////// + +int EvidentObjectiveSetup::Initialize() +{ + EvidentHubWin* hub = GetHub(); + if (hub == 0) + return ERR_DEVICE_NOT_AVAILABLE; + + if (initialized_) + return DEVICE_OK; + + // Query all 6 nosepiece positions to detect installed objectives + LogMessage("Querying installed objectives..."); + for (int pos = 1; pos <= 6; pos++) + { + int ret = QueryObjectiveAtPosition(pos); + if (ret != DEVICE_OK) + { + std::ostringstream msg; + msg << "Warning: Failed to query objective at position " << pos; + LogMessage(msg.str().c_str()); + } + } + + // Create filter properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentObjectiveSetup::OnFilterMagnification); + int ret = CreateProperty("Filter-By-Magnification", "All", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue("Filter-By-Magnification", "All"); + AddAllowedValue("Filter-By-Magnification", "1.25x"); + AddAllowedValue("Filter-By-Magnification", "2.0x"); + AddAllowedValue("Filter-By-Magnification", "2.5x"); + AddAllowedValue("Filter-By-Magnification", "4.0x"); + AddAllowedValue("Filter-By-Magnification", "5.0x"); + AddAllowedValue("Filter-By-Magnification", "10.0x"); + AddAllowedValue("Filter-By-Magnification", "20.0x"); + AddAllowedValue("Filter-By-Magnification", "25.0x"); + AddAllowedValue("Filter-By-Magnification", "30.0x"); + AddAllowedValue("Filter-By-Magnification", "40.0x"); + AddAllowedValue("Filter-By-Magnification", "50.0x"); + AddAllowedValue("Filter-By-Magnification", "60.0x"); + AddAllowedValue("Filter-By-Magnification", "100.0x"); + AddAllowedValue("Filter-By-Magnification", "150.0x"); + + pAct = new CPropertyAction(this, &EvidentObjectiveSetup::OnFilterImmersion); + ret = CreateProperty("Filter-By-Immersion", "All", MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue("Filter-By-Immersion", "All"); + AddAllowedValue("Filter-By-Immersion", "Dry"); + AddAllowedValue("Filter-By-Immersion", "Water"); + AddAllowedValue("Filter-By-Immersion", "Oil"); + AddAllowedValue("Filter-By-Immersion", "Silicon"); + AddAllowedValue("Filter-By-Immersion", "Gel"); + + // Create properties for each of the 6 nosepiece positions + for (int pos = 1; pos <= 6; pos++) + { + std::ostringstream propName; + + // Detected name (read-only with action handler) + propName.str(""); + propName << "Position-" << pos << "-Detected-Name"; + CPropertyActionEx* pActNameEX = new CPropertyActionEx(this, &EvidentObjectiveSetup::OnPosDetectedName, pos); + ret = CreateProperty(propName.str().c_str(), detectedObjectives_[pos-1].name.c_str(), MM::String, true, pActNameEX); + if (ret != DEVICE_OK) + return ret; + + // Detected specs (read-only with action handler) + propName.str(""); + propName << "Position-" << pos << "-Detected-Specs"; + CPropertyActionEx* pActSpecsEX = new CPropertyActionEx(this, &EvidentObjectiveSetup::OnPosDetectedSpecs, pos); + std::string detectedSpecs = FormatSpecsString( + detectedObjectives_[pos-1].na, + detectedObjectives_[pos-1].magnification, + detectedObjectives_[pos-1].medium); + ret = CreateProperty(propName.str().c_str(), detectedSpecs.c_str(), MM::String, true, pActSpecsEX); + if (ret != DEVICE_OK) + return ret; + + // Database selection dropdown + propName.str(""); + propName << "Position-" << pos << "-Database-Selection"; + CPropertyActionEx* pActSelEX = new CPropertyActionEx(this, &EvidentObjectiveSetup::OnPosDatabaseSelection, pos); + ret = CreateProperty(propName.str().c_str(), "NONE", MM::String, false, pActSelEX); + if (ret != DEVICE_OK) + return ret; + + // Populate database dropdown with all lenses + ret = UpdateDatabaseDropdown(pos); + if (ret != DEVICE_OK) + return ret; + + // Send to SDK action button for this position + propName.str(""); + propName << "Position-" << pos << "-Send-To-SDK"; + CPropertyActionEx* pActSendEX = new CPropertyActionEx(this, &EvidentObjectiveSetup::OnPosSendToSDK, pos); + ret = CreateProperty(propName.str().c_str(), "Press to send", MM::String, false, pActSendEX); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue(propName.str().c_str(), "Press to send"); + AddAllowedValue(propName.str().c_str(), "Sending..."); + + // Special objective properties + // Special NA + propName.str(""); + propName << "Position-" << pos << "-Special-NA"; + CPropertyActionEx* pActNAEX = new CPropertyActionEx(this, &EvidentObjectiveSetup::OnPosSpecialNA, pos); + ret = CreateProperty(propName.str().c_str(), "0.16", MM::Float, false, pActNAEX); + if (ret != DEVICE_OK) + return ret; + SetPropertyLimits(propName.str().c_str(), 0.04, 2.00); + + // Special Magnification + propName.str(""); + propName << "Position-" << pos << "-Special-Magnification"; + CPropertyActionEx* pActMagEX = new CPropertyActionEx(this, &EvidentObjectiveSetup::OnPosSpecialMagnification, pos); + ret = CreateProperty(propName.str().c_str(), "10.0", MM::Float, false, pActMagEX); + if (ret != DEVICE_OK) + return ret; + SetPropertyLimits(propName.str().c_str(), 0.01, 150.0); + + // Special Immersion + propName.str(""); + propName << "Position-" << pos << "-Special-Immersion"; + CPropertyActionEx* pActImmEX = new CPropertyActionEx(this, &EvidentObjectiveSetup::OnPosSpecialImmersion, pos); + ret = CreateProperty(propName.str().c_str(), "Dry", MM::String, false, pActImmEX); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue(propName.str().c_str(), "Dry"); + AddAllowedValue(propName.str().c_str(), "Water"); + AddAllowedValue(propName.str().c_str(), "Oil"); + AddAllowedValue(propName.str().c_str(), "Silicon oil"); + AddAllowedValue(propName.str().c_str(), "Silicon gel"); + + // Special Send to SDK button + propName.str(""); + propName << "Position-" << pos << "-Special-Send-To-SDK"; + CPropertyActionEx* pActSpecialSendEX = new CPropertyActionEx(this, &EvidentObjectiveSetup::OnPosSpecialSendToSDK, pos); + ret = CreateProperty(propName.str().c_str(), "Press to send", MM::String, false, pActSpecialSendEX); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue(propName.str().c_str(), "Press to send"); + AddAllowedValue(propName.str().c_str(), "Sending..."); + } + + // Status message (read-only) + CreateProperty("Last-Status", "Ready", MM::String, true); + + initialized_ = true; + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// Helper Functions +/////////////////////////////////////////////////////////////////////////////// + +int EvidentObjectiveSetup::QueryObjectiveAtPosition(int position) +{ + EvidentHubWin* hub = GetHub(); + if (hub == 0) + return ERR_DEVICE_NOT_AVAILABLE; + + // Send GOB command to query objective information + std::string cmd = BuildCommand(CMD_GET_OBJECTIVE, position); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Parse response: GOB p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11 + // p1: Position (1-6) + // p2: Name (string) + // p3: NA (0.00-2.00 or "N") + // p4: Magnification (0-200 or "N") + // p5: Medium (1=dry, 2=water, 3=oil, 4=silicone oil, 5=silicone gel, or "N") + // p6-p11: Additional parameters (not needed for S_SOB) + + std::vector params = ParseParameters(response); + if (params.size() < 5) + { + std::ostringstream msg; + msg << "GOB response has insufficient parameters: " << response; + LogMessage(msg.str().c_str()); + return ERR_INVALID_RESPONSE; + } + + int idx = position - 1; // Array index + detectedObjectives_[idx].detected = true; + detectedObjectives_[idx].name = params[1]; // p2 + + // Parse NA (p3) + if (params[2] == "N" || params[2] == "n") + { + detectedObjectives_[idx].na = 0.0; + } + else + { + try + { + detectedObjectives_[idx].na = std::stod(params[2]); + } + catch (...) + { + detectedObjectives_[idx].na = 0.0; + } + } + + // Parse Magnification (p4) + if (params[3] == "N" || params[3] == "n") + { + detectedObjectives_[idx].magnification = 0.0; + } + else + { + try + { + detectedObjectives_[idx].magnification = std::stod(params[3]); + } + catch (...) + { + detectedObjectives_[idx].magnification = 0.0; + } + } + + // Parse Medium (p5) + if (params[4] == "N" || params[4] == "n") + { + detectedObjectives_[idx].medium = 1; // Default to Dry + } + else + { + detectedObjectives_[idx].medium = ParseIntParameter(params[4]); + if (detectedObjectives_[idx].medium < 1 || detectedObjectives_[idx].medium > 5) + detectedObjectives_[idx].medium = 1; // Default to Dry + } + + std::ostringstream msg; + msg << "Position " << position << ": " << detectedObjectives_[idx].name + << " (Mag=" << detectedObjectives_[idx].magnification + << ", NA=" << detectedObjectives_[idx].na + << ", Medium=" << detectedObjectives_[idx].medium << ")"; + LogMessage(msg.str().c_str()); + + return DEVICE_OK; +} + +int EvidentObjectiveSetup::SendObjectiveToSDK(int position) +{ + EvidentHubWin* hub = GetHub(); + if (hub == 0) + return ERR_DEVICE_NOT_AVAILABLE; + + int idx = position - 1; + + // Get the model name for this position + // selectedLensModel_[idx] is always set to either "NONE" or a specific model name + std::string modelName = selectedLensModel_[idx]; + + std::ostringstream msg; + msg << "Sending to SDK - Position " << position << ": " << modelName; + LogMessage(msg.str().c_str()); + + // Enter Setting mode + std::string cmd = BuildCommand(CMD_OPERATION_MODE, 1); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + LogMessage("Failed to enter Setting mode"); + return ret; + } + + // Send S_OB command: S_OB position,name + std::ostringstream sobCmd; + sobCmd << CMD_AF_SET_OBJECTIVE << " " << position << "," << modelName; + + ret = hub->ExecuteCommand(sobCmd.str(), response); + + // Exit Setting mode (always do this, even if S_OB failed) + cmd = BuildCommand(CMD_OPERATION_MODE, 0); + std::string exitResponse; + int exitRet = hub->ExecuteCommand(cmd, exitResponse); + + if (ret != DEVICE_OK) + { + std::ostringstream errMsg; + errMsg << "S_OB command failed for position " << position << ": " << response; + LogMessage(errMsg.str().c_str()); + return ret; + } + + if (exitRet != DEVICE_OK) + { + LogMessage("Warning: Failed to exit Setting mode"); + return exitRet; + } + + msg.str(""); + msg << "Successfully sent objective " << modelName << " for position " << position; + LogMessage(msg.str().c_str()); + + // Re-query the objective to update detected properties + ret = QueryObjectiveAtPosition(position); + if (ret == DEVICE_OK) + { + // Update the detected name and specs properties + std::ostringstream propName; + + propName.str(""); + propName << "Position-" << position << "-Detected-Name"; + SetProperty(propName.str().c_str(), detectedObjectives_[idx].name.c_str()); + + propName.str(""); + propName << "Position-" << position << "-Detected-Specs"; + std::string detectedSpecs = FormatSpecsString( + detectedObjectives_[idx].na, + detectedObjectives_[idx].magnification, + detectedObjectives_[idx].medium); + SetProperty(propName.str().c_str(), detectedSpecs.c_str()); + } + else + { + LogMessage("Warning: Failed to re-query objective after sending"); + } + + return DEVICE_OK; +} + +int EvidentObjectiveSetup::SendSpecialObjectiveToSDK(int position) +{ + EvidentHubWin* hub = GetHub(); + if (hub == 0) + return ERR_DEVICE_NOT_AVAILABLE; + + int idx = position - 1; + + // Get special objective specs for this position + double na = specialNA_[idx]; + double magnification = specialMagnification_[idx]; + int medium = ConvertImmersionStringToMediumCode(specialImmersion_[idx]); + + std::ostringstream msg; + msg << "Sending special objective to SDK - Position " << position + << ": NA=" << na << ", Mag=" << magnification << ", Medium=" << medium; + LogMessage(msg.str().c_str()); + + // Enter Setting mode + std::string cmd = BuildCommand(CMD_OPERATION_MODE, 1); + std::string response; + int ret = hub->ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + LogMessage("Failed to enter Setting mode"); + return ret; + } + + // Send S_SOB command: S_SOB position,NA,magnification,medium + std::ostringstream sobCmd; + sobCmd << CMD_AF_SET_OBJECTIVE_FULL << " " << position << "," + << std::fixed << std::setprecision(2) << na << "," + << std::fixed << std::setprecision(2) << magnification << "," + << medium; + + ret = hub->ExecuteCommand(sobCmd.str(), response); + if (ret != DEVICE_OK) + { + LogMessage("S_SOB command failed"); + std::string exitCmd = BuildCommand(CMD_OPERATION_MODE, 0); + hub->ExecuteCommand(exitCmd, response); + return ret; + } + + // Exit Setting mode + cmd = BuildCommand(CMD_OPERATION_MODE, 0); + std::string exitResponse; + hub->ExecuteCommand(cmd, exitResponse); + + // Re-query the objective to update detected properties + ret = QueryObjectiveAtPosition(position); + if (ret == DEVICE_OK) + { + // Update detected properties + std::ostringstream propName; + propName << "Position-" << position << "-Detected-Name"; + SetProperty(propName.str().c_str(), detectedObjectives_[idx].name.c_str()); + + propName.str(""); + propName << "Position-" << position << "-Detected-Specs"; + std::string detectedSpecs = FormatSpecsString( + detectedObjectives_[idx].na, + detectedObjectives_[idx].magnification, + detectedObjectives_[idx].medium); + SetProperty(propName.str().c_str(), detectedSpecs.c_str()); + } + else + { + LogMessage("Warning: Failed to re-query objective after sending"); + } + + return DEVICE_OK; +} + +int EvidentObjectiveSetup::ConvertImmersionToMediumCode(EvidentLens::ImmersionType immersion) +{ + switch (immersion) + { + case EvidentLens::Immersion_Dry: return 1; + case EvidentLens::Immersion_Water: return 2; + case EvidentLens::Immersion_Oil: return 3; + case EvidentLens::Immersion_Silicon: return 4; + case EvidentLens::Immersion_Gel: return 5; + default: return 1; // Default to Dry + } +} + +int EvidentObjectiveSetup::ConvertImmersionStringToMediumCode(const std::string& immersion) +{ + if (immersion == "Dry") + return 1; + else if (immersion == "Water") + return 2; + else if (immersion == "Oil") + return 3; + else if (immersion == "Silicon oil") + return 4; + else if (immersion == "Silicon gel") + return 5; + else + return 1; // Default to Dry +} + +std::string EvidentObjectiveSetup::FormatLensForDropdown(const EvidentLens::LensInfo* lens) +{ + if (lens == nullptr) + return "(None)"; + + // Just return model name to avoid comma issues with MM property system + return lens->model; +} + +void EvidentObjectiveSetup::GetEffectiveObjectiveSpecs(int position, double& na, double& mag, int& medium) +{ + int idx = position - 1; + + // Check if a specific database objective is selected (not "NONE") + if (selectedLensModel_[idx] != "NONE") + { + // Try to use database selection + const EvidentLens::LensInfo* lens = EvidentLens::GetLensByModel(selectedLensModel_[idx].c_str()); + if (lens != nullptr) + { + na = lens->na; + mag = lens->magnification; + medium = ConvertImmersionToMediumCode(lens->immersion); + return; + } + } + + // Use detected objective (default for "NONE" or if database lookup fails) + na = detectedObjectives_[idx].na; + mag = detectedObjectives_[idx].magnification; + medium = detectedObjectives_[idx].medium; +} + +int EvidentObjectiveSetup::UpdateDatabaseDropdown(int position) +{ + std::ostringstream propName; + propName << "Position-" << position << "-Database-Selection"; + + // Clear existing allowed values + ClearAllowedValues(propName.str().c_str()); + + // Add "NONE" option (clear position in SDK) + AddAllowedValue(propName.str().c_str(), "NONE"); + + // Get filter criteria + double filterMag = 0.0; + if (filterMagnification_ != "All") + { + // Remove 'x' suffix and parse + std::string magStr = filterMagnification_; + size_t xPos = magStr.find('x'); + if (xPos != std::string::npos) + magStr = magStr.substr(0, xPos); + try + { + filterMag = std::stod(magStr); + } + catch (...) + { + filterMag = 0.0; + } + } + + EvidentLens::ImmersionType filterImm = EvidentLens::Immersion_Dry; + bool filterByImmersion = (filterImmersion_ != "All"); + if (filterByImmersion) + { + if (filterImmersion_ == "Dry") + filterImm = EvidentLens::Immersion_Dry; + else if (filterImmersion_ == "Water") + filterImm = EvidentLens::Immersion_Water; + else if (filterImmersion_ == "Oil") + filterImm = EvidentLens::Immersion_Oil; + else if (filterImmersion_ == "Silicon") + filterImm = EvidentLens::Immersion_Silicon; + else if (filterImmersion_ == "Gel") + filterImm = EvidentLens::Immersion_Gel; + } + + // Add filtered lenses from database + for (int i = 0; i < EvidentLens::LENS_DATABASE_SIZE; i++) + { + const EvidentLens::LensInfo* lens = &EvidentLens::LENS_DATABASE[i]; + + // Apply filters + if (filterMag > 0.0 && lens->magnification != filterMag) + continue; + if (filterByImmersion && lens->immersion != filterImm) + continue; + + std::string formatted = FormatLensForDropdown(lens); + AddAllowedValue(propName.str().c_str(), formatted.c_str()); + } + + return DEVICE_OK; +} + +std::string EvidentObjectiveSetup::FormatSpecsString(double na, double mag, int medium) +{ + std::ostringstream specs; + specs << std::fixed << std::setprecision(1) << mag << "x, " + << "NA=" << std::fixed << std::setprecision(2) << na << ", "; + + switch (medium) + { + case 1: specs << "Dry"; break; + case 2: specs << "Water"; break; + case 3: specs << "Oil"; break; + case 4: specs << "Silicon"; break; + case 5: specs << "Gel"; break; + default: specs << "Unknown"; break; + } + + return specs.str(); +} + +/////////////////////////////////////////////////////////////////////////////// +// Property Handlers +/////////////////////////////////////////////////////////////////////////////// + +int EvidentObjectiveSetup::OnFilterMagnification(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(filterMagnification_.c_str()); + } + else if (eAct == MM::AfterSet) + { + pProp->Get(filterMagnification_); + + // Update all database dropdowns with new filter + for (int pos = 1; pos <= 6; pos++) + { + UpdateDatabaseDropdown(pos); + } + } + return DEVICE_OK; +} + +int EvidentObjectiveSetup::OnFilterImmersion(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(filterImmersion_.c_str()); + } + else if (eAct == MM::AfterSet) + { + pProp->Get(filterImmersion_); + + // Update all database dropdowns with new filter + for (int pos = 1; pos <= 6; pos++) + { + UpdateDatabaseDropdown(pos); + } + } + return DEVICE_OK; +} + +// Position handlers (parameterized) +int EvidentObjectiveSetup::OnPosDetectedName(MM::PropertyBase* pProp, MM::ActionType eAct, long position) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(detectedObjectives_[position - 1].name.c_str()); + } + return DEVICE_OK; +} + +int EvidentObjectiveSetup::OnPosDetectedSpecs(MM::PropertyBase* pProp, MM::ActionType eAct, long position) +{ + if (eAct == MM::BeforeGet) + { + std::string specs = FormatSpecsString( + detectedObjectives_[position - 1].na, + detectedObjectives_[position - 1].magnification, + detectedObjectives_[position - 1].medium); + pProp->Set(specs.c_str()); + } + return DEVICE_OK; +} + +int EvidentObjectiveSetup::OnPosDatabaseSelection(MM::PropertyBase* pProp, MM::ActionType eAct, long position) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(selectedLensModel_[position - 1].c_str()); + } + else if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + selectedLensModel_[position - 1] = value; + } + return DEVICE_OK; +} + +int EvidentObjectiveSetup::OnPosSendToSDK(MM::PropertyBase* pProp, MM::ActionType eAct, long position) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set("Press to send"); + } + else if (eAct == MM::AfterSet) + { + pProp->Set("Sending..."); + + int ret = SendObjectiveToSDK(position); + + std::ostringstream statusMsg; + if (ret == DEVICE_OK) + statusMsg << "Position " << position << ": Sent successfully"; + else + statusMsg << "Position " << position << ": Failed to send"; + + SetProperty("Last-Status", statusMsg.str().c_str()); + LogMessage(statusMsg.str().c_str()); + + pProp->Set("Press to send"); + } + return DEVICE_OK; +} + + +// Special Objective handlers (parameterized) +int EvidentObjectiveSetup::OnPosSpecialNA(MM::PropertyBase* pProp, MM::ActionType eAct, long position) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(specialNA_[position - 1]); + } + else if (eAct == MM::AfterSet) + { + pProp->Get(specialNA_[position - 1]); + } + return DEVICE_OK; +} + +int EvidentObjectiveSetup::OnPosSpecialMagnification(MM::PropertyBase* pProp, MM::ActionType eAct, long position) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(specialMagnification_[position - 1]); + } + else if (eAct == MM::AfterSet) + { + pProp->Get(specialMagnification_[position - 1]); + } + return DEVICE_OK; +} + +int EvidentObjectiveSetup::OnPosSpecialImmersion(MM::PropertyBase* pProp, MM::ActionType eAct, long position) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(specialImmersion_[position - 1].c_str()); + } + else if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + specialImmersion_[position - 1] = value; + } + return DEVICE_OK; +} + +int EvidentObjectiveSetup::OnPosSpecialSendToSDK(MM::PropertyBase* pProp, MM::ActionType eAct, long position) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set("Press to send"); + } + else if (eAct == MM::AfterSet) + { + pProp->Set("Sending..."); + + int ret = SendSpecialObjectiveToSDK(position); + + std::ostringstream statusMsg; + if (ret == DEVICE_OK) + statusMsg << "Position " << position << ": Special objective sent successfully"; + else + statusMsg << "Position " << position << ": Failed to send special objective"; + + SetProperty("Last-Status", statusMsg.str().c_str()); + LogMessage(statusMsg.str().c_str()); + + pProp->Set("Press to send"); + } + return DEVICE_OK; +} diff --git a/DeviceAdapters/EvidentIX85Win/EvidentObjectiveSetup.h b/DeviceAdapters/EvidentIX85Win/EvidentObjectiveSetup.h new file mode 100644 index 000000000..750acc7e0 --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentObjectiveSetup.h @@ -0,0 +1,104 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentObjectiveSetup.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 objective setup device - sends full objective +// specifications to SDK using S_SOB command +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include "DeviceBase.h" +#include "EvidentHubWin.h" +#include "EvidentModelWin.h" +#include "EvidentProtocolWin.h" +#include "EvidentLensDatabase.h" + +////////////////////////////////////////////////////////////////////////////// +// Objective Setup Device (for one-time microscope configuration) +////////////////////////////////////////////////////////////////////////////// + +class EvidentObjectiveSetup : public CGenericBase +{ +public: + EvidentObjectiveSetup(); + ~EvidentObjectiveSetup(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + // Action interface - Parameterized handlers + int OnPosDetectedName(MM::PropertyBase* pProp, MM::ActionType eAct, long position); + int OnPosDetectedSpecs(MM::PropertyBase* pProp, MM::ActionType eAct, long position); + int OnPosDatabaseSelection(MM::PropertyBase* pProp, MM::ActionType eAct, long position); + int OnPosSendToSDK(MM::PropertyBase* pProp, MM::ActionType eAct, long position); + + // Action interface - Special objective handlers (parameterized) + int OnPosSpecialNA(MM::PropertyBase* pProp, MM::ActionType eAct, long position); + int OnPosSpecialMagnification(MM::PropertyBase* pProp, MM::ActionType eAct, long position); + int OnPosSpecialImmersion(MM::PropertyBase* pProp, MM::ActionType eAct, long position); + int OnPosSpecialSendToSDK(MM::PropertyBase* pProp, MM::ActionType eAct, long position); + + // Action interface - Global controls + int OnFilterMagnification(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnFilterImmersion(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + EvidentHubWin* GetHub(); + + // Helper functions + int QueryObjectiveAtPosition(int position); + int SendObjectiveToSDK(int position); + int SendSpecialObjectiveToSDK(int position); + int ConvertImmersionToMediumCode(EvidentLens::ImmersionType immersion); + int ConvertImmersionStringToMediumCode(const std::string& immersion); + std::string FormatLensForDropdown(const EvidentLens::LensInfo* lens); + void GetEffectiveObjectiveSpecs(int position, double& na, double& mag, int& medium); + int UpdateDatabaseDropdown(int position); + std::string FormatSpecsString(double na, double mag, int medium); + + // Member variables + bool initialized_; + std::string name_; + + // Detected objective information from GOB queries (6 positions) + struct DetectedObjective + { + std::string name; + double na; + double magnification; + int medium; // 1=Dry, 2=Water, 3=Oil, 4=Silicon, 5=Gel + bool detected; + }; + DetectedObjective detectedObjectives_[6]; + + // User override selections (6 positions) + std::string selectedLensModel_[6]; // Model name from database, or "NONE" to clear position + + // Special/custom objective specifications (6 positions) + double specialNA_[6]; // NA value (0.04-2.00) + double specialMagnification_[6]; // Magnification (0.01-150) + std::string specialImmersion_[6]; // Immersion type string + + // Filter settings for database dropdown + std::string filterMagnification_; // "All" or specific mag value + std::string filterImmersion_; // "All" or specific immersion type +}; diff --git a/DeviceAdapters/EvidentIX85Win/EvidentProtocolWin.h b/DeviceAdapters/EvidentIX85Win/EvidentProtocolWin.h new file mode 100644 index 000000000..7d6e300be --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentProtocolWin.h @@ -0,0 +1,472 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentProtocolWin.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85Win (SDK) microscope protocol definitions +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include +#include +#include +#include + +namespace EvidentIX85Win { + +// Protocol constants +const char* const TERMINATOR = "\r\n"; +const char TAG_DELIMITER = ' '; +const char DATA_DELIMITER = ','; +const char POSITIVE_ACK = '+'; +const char NEGATIVE_ACK = '!'; +const char UNKNOWN_RESPONSE = 'X'; + +const int MAX_COMMAND_LENGTH = 128; +const long ANSWER_TIMEOUT_MS = 6000; + +// Serial port settings +const int BAUD_RATE = 115200; +const int DATA_BITS = 8; +const char PARITY = 'E'; // Even +const int STOP_BITS = 2; + +// Command tags - System commands +const char* const CMD_LOGIN = "L"; +const char* const CMD_UNIT = "U"; +const char* const CMD_ERROR = "ER"; +const char* const CMD_OPERATION_MODE = "OPE"; // Setting mode control (0=normal, 1=setting) + +// Command tags - Focus +const char* const CMD_FOCUS_GOTO = "FG"; +const char* const CMD_FOCUS_MOVE = "FM"; +const char* const CMD_FOCUS_STOP = "FSTP"; +const char* const CMD_FOCUS_POSITION = "FP"; +const char* const CMD_FOCUS_NOTIFY = "NFP"; +const char* const CMD_FOCUS_SPEED = "FSPD"; +const char* const CMD_FOCUS_NEAR_LIMIT = "NL"; +const char* const CMD_FOCUS_FAR_LIMIT = "FL"; +const char* const CMD_PARFOCAL = "PF"; +const char* const CMD_ENABLE_PARFOCAL = "ENPF"; +const char* const CMD_FOCUS_ESCAPE = "ESC2"; + +// Command tags - Nosepiece (Objective Turret) +const char* const CMD_NOSEPIECE = "OB"; +const char* const CMD_NOSEPIECE_NOTIFY = "NOB"; +const char* const CMD_NOSEPIECE_SEQ = "OBSEQ"; // Safe/sequential objective switch +const char* const CMD_NOSEPIECE_REQUEST_NOTIFY = "NROB"; // MCZ dial request notification +const char* const CMD_GET_OBJECTIVE = "GOB"; // Get objective lens information + +// Command tags - Magnification Changer +const char* const CMD_MAGNIFICATION = "CA"; +const char* const CMD_MAGNIFICATION_NOTIFY = "NCA"; + +// Command tags - Light Path +const char* const CMD_LIGHT_PATH = "BIL"; + +// Command tags - Condenser +const char* const CMD_CONDENSER_TURRET = "TR"; +const char* const CMD_CONDENSER_TURRET_NOTIFY = "NTR"; +const char* const CMD_CONDENSER_CONTROL = "CD"; +const char* const CMD_CONDENSER_NOTIFY = "NCD"; +const char* const CMD_CONDENSER_SWITCH = "S1"; + +// Command tags - DIA (Transmitted Light) +const char* const CMD_DIA_APERTURE = "DAS"; +const char* const CMD_DIA_APERTURE_NOTIFY = "NDAS"; +const char* const CMD_DIA_APERTURE_STOP = "DASSTP"; +const char* const CMD_DIA_SHUTTER = "DSH"; +const char* const CMD_DIA_ILLUMINATION = "DIL"; +const char* const CMD_DIA_ILLUMINATION_NOTIFY = "NDIL"; + +// Command tags - Polarizer +const char* const CMD_POLARIZER = "PO"; +const char* const CMD_POLARIZER_NOTIFY = "NPO"; + +// Command tags - DIC +const char* const CMD_DIC_PRISM = "DIC"; +const char* const CMD_DIC_RETARDATION = "DICR"; +const char* const CMD_DIC_RETARDATION_NOTIFY = "NDICR"; +const char* const CMD_DIC_LOCALIZED_NOTIFY = "NLC"; +const char* const CMD_DIC_LOCALIZED = "LC"; + +// Command tags - EPI (Reflected Light) +const char* const CMD_EPI_SHUTTER1 = "ESH1"; +const char* const CMD_EPI_SHUTTER2 = "ESH2"; +const char* const CMD_MIRROR_UNIT1 = "MU1"; +const char* const CMD_MIRROR_UNIT2 = "MU2"; +const char* const CMD_MIRROR_UNIT_NOTIFY1 = "NMUINIT1"; +const char* const CMD_MIRROR_UNIT_NOTIFY2 = "NMUINIT2"; +const char* const CMD_MIRROR_REQUEST_NOTIFY = "NRMU"; // MCZ dial request notification +const char* const CMD_COVER_SWITCH1 = "C1"; +const char* const CMD_COVER_SWITCH2 = "C2"; +const char* const CMD_EPI_ND = "END"; + +// Command tags - Right Port +const char* const CMD_RIGHT_PORT = "BIR"; +const char* const CMD_RIGHT_PORT_NOTIFY = "NBIR"; + +// Command tags - Correction Collar +const char* const CMD_CORRECTION_COLLAR = "CC"; +const char* const CMD_CORRECTION_COLLAR_LINK = "CCL"; +const char* const CMD_CORRECTION_COLLAR_INIT = "CCINIT"; + +// Command tags - Autofocus (ZDC) +const char* const CMD_AF_START_STOP = "AF"; +const char* const CMD_AF_STOP = "AFSTP"; +const char* const CMD_AF_STATUS = "AFST"; +const char* const CMD_AF_LIMIT = "AFL"; +const char* const CMD_AF_TABLE = "AFTBL"; +const char* const CMD_AF_PARAMETER = "AFP"; +const char* const CMD_AF_GET_PARAMETER = "GAFP"; +const char* const CMD_AF_NEAR_LIMIT = "AFNL"; +const char* const CMD_AF_FAR_LIMIT = "AFFL"; +const char* const CMD_AF_BUZZER = "AFBZ"; +const char* const CMD_AF_APERTURE = "AFAS"; +const char* const CMD_AF_DICHROIC = "AFDM"; +const char* const CMD_AF_DICHROIC_MOVE = "AFDMG"; +const char* const CMD_AF_DIC = "AFDIC"; +const char* const CMD_COVERSLIP_TYPE = "CST"; +const char* const CMD_COVERSLIP_THICKNESS = "CS2"; +const char* const CMD_AF_SET_OBJECTIVE = "S_OB"; +const char* const CMD_AF_SET_OBJECTIVE_FULL = "S_SOB"; // Set objective with full specs (NA, mag, medium) + +// Command tags - Offset Lens (part of ZDC) +const char* const CMD_OFFSET_LENS_GOTO = "ABG"; +const char* const CMD_OFFSET_LENS_MOVE = "ABM"; +const char* const CMD_OFFSET_LENS_STOP = "ABSTP"; +const char* const CMD_OFFSET_LENS_POSITION = "ABP"; +const char* const CMD_OFFSET_LENS_NOTIFY = "NABP"; +const char* const CMD_OFFSET_LENS_RANGE = "ABRANGE"; +const char* const CMD_OFFSET_LENS_LIMIT = "ABLMT"; +const char* const CMD_OFFSET_LENS_LOST_MOTION = "ABLM"; +const char* const CMD_OFFSET_LENS_BASE_POSITION = "ABBP"; + +// Command tags - MCZ (Manual Control Unit) +const char* const CMD_JOG = "JG"; +const char* const CMD_JOG_SENSITIVITY_FINE = "JGSF"; +const char* const CMD_JOG_SENSITIVITY_COARSE = "JGSC"; +const char* const CMD_JOG_DIRECTION = "JGDR"; // Jog direction (0=Reverse, 1=Default) +const char* const CMD_JOG_LIMIT = "JGL"; +const char* const CMD_OFFSET_LENS_SENSITIVITY_FINE = "ABJGSF"; +const char* const CMD_OFFSET_LENS_SENSITIVITY_COARSE = "ABJGSC"; + +// Command tags - SD Magnification Changer +const char* const CMD_SD_MAGNIFICATION = "SDCA"; + +// Command tags - Indicators and Encoders +const char* const CMD_INDICATOR_CONTROL = "I"; +const char* const CMD_INDICATOR1 = "I1"; +const char* const CMD_INDICATOR2 = "I2"; +const char* const CMD_INDICATOR3 = "I3"; +const char* const CMD_INDICATOR4 = "I4"; +const char* const CMD_INDICATOR5 = "I5"; +const char* const CMD_INDICATOR_SYNC = "ISYNC"; // Synchronize indicators with actual positions +const char* const CMD_ENCODER1 = "E1"; +const char* const CMD_ENCODER2 = "E2"; +const char* const CMD_ENCODER3 = "E3"; +const char* const CMD_DIL_ENCODER_CONTROL = "DILE"; +const char* const CMD_MCZ_SWITCH = "S2"; + +// Device limits and constants +const long FOCUS_MIN_POS = 0; +const long FOCUS_MAX_POS = 1050000; // 10.5mm in 10nm units +const double FOCUS_STEP_SIZE_UM = 0.01; // 10nm = 0.01um +const long FOCUS_POSITION_TOLERANCE = 10; // 10 steps = 100nm tolerance for "at position" detection + +const int NOSEPIECE_MIN_POS = 1; +const int NOSEPIECE_MAX_POS = 6; + +const int MAGNIFICATION_MIN_POS = 1; +const int MAGNIFICATION_MAX_POS = 3; + +// Most turrets and state devices have up to 6 positions +const int CONDENSER_TURRET_MAX_POS = 6; +const int MIRROR_UNIT_MAX_POS = 8; +const int POLARIZER_MAX_POS = 2; // Out (0) and In (1) +const int DIC_PRISM_MAX_POS = 6; +const int EPIND_MAX_POS = 6; + +const int LIGHT_PATH_LEFT_PORT = 1; +const int LIGHT_PATH_BI_50_50 = 2; +const int LIGHT_PATH_BI_100 = 3; +const int LIGHT_PATH_RIGHT_PORT = 4; + +// Correction Collar +const long CORRECTION_COLLAR_MIN_POS = -3200; +const long CORRECTION_COLLAR_MAX_POS = 3200; +const double CORRECTION_COLLAR_STEP_SIZE_UM = 1.0; // 1 step = 1 µm + +// Offset Lens (ZDC) +const long OFFSET_LENS_MIN_POS = -10000; +const long OFFSET_LENS_MAX_POS = 50000; +const double OFFSET_LENS_STEP_SIZE_UM = 0.5; // 1 step = 0.5 µm + +// Manual Control Unit (MCU) 7-segment display codes +// These hex codes drive the 7-segment displays on the MCU indicators +const int SEG7_0 = 0xEE; +const int SEG7_1 = 0x28; +const int SEG7_2 = 0xCD; +const int SEG7_3 = 0x6D; +const int SEG7_4 = 0x2B; +const int SEG7_5 = 0x67; +const int SEG7_6 = 0xE7; +const int SEG7_7 = 0x2E; +const int SEG7_8 = 0xEF; +const int SEG7_9 = 0x6F; +const int SEG7_DASH = 0x01; + +// Helper function to get 7-segment code for a digit +inline int Get7SegmentCode(int digit) +{ + switch (digit) + { + case 0: return SEG7_0; + case 1: return SEG7_1; + case 2: return SEG7_2; + case 3: return SEG7_3; + case 4: return SEG7_4; + case 5: return SEG7_5; + case 6: return SEG7_6; + case 7: return SEG7_7; + case 8: return SEG7_8; + case 9: return SEG7_9; + default: return SEG7_DASH; // Return dash for invalid digits + } +} + +// Error codes (Evident specific, starting at 10100) +const int ERR_EVIDENT_OFFSET = 10100; +const int ERR_COMMAND_TIMEOUT = ERR_EVIDENT_OFFSET + 1; +const int ERR_NEGATIVE_ACK = ERR_EVIDENT_OFFSET + 2; +const int ERR_INVALID_RESPONSE = ERR_EVIDENT_OFFSET + 3; +const int ERR_NOT_IN_REMOTE_MODE = ERR_EVIDENT_OFFSET + 4; +const int ERR_DEVICE_NOT_AVAILABLE = ERR_EVIDENT_OFFSET + 5; +const int ERR_POSITION_UNKNOWN = ERR_EVIDENT_OFFSET + 6; +const int ERR_MONITOR_THREAD_FAILED = ERR_EVIDENT_OFFSET + 7; +const int ERR_PORT_NOT_SET = ERR_EVIDENT_OFFSET + 8; +const int ERR_PORT_CHANGE_FORBIDDEN = ERR_EVIDENT_OFFSET + 9; +const int ERR_CORRECTION_COLLAR_NOT_LINKED = ERR_EVIDENT_OFFSET + 10; +const int ERR_CORRECTION_COLLAR_LINK_FAILED = ERR_EVIDENT_OFFSET + 11; +const int ERR_POSITION_OUT_OF_RANGE = ERR_EVIDENT_OFFSET + 12; +const int ERR_INVALID_PARAMETER = ERR_EVIDENT_OFFSET + 13; + +// Helper functions +inline std::string BuildCommand(const char* tag) +{ + std::ostringstream cmd; + cmd << tag; // Don't add TERMINATOR - SendSerialCommand adds it + return cmd.str(); +} + +inline std::string BuildCommand(const char* tag, int param1) +{ + std::ostringstream cmd; + cmd << tag << TAG_DELIMITER << param1; + return cmd.str(); +} + +inline std::string BuildCommand(const char* tag, int param1, int param2) +{ + std::ostringstream cmd; + cmd << tag << TAG_DELIMITER << param1 << DATA_DELIMITER << param2; + return cmd.str(); +} + +inline std::string BuildCommand(const char* tag, int param1, int param2, int param3) +{ + std::ostringstream cmd; + cmd << tag << TAG_DELIMITER << param1 << DATA_DELIMITER << param2 << DATA_DELIMITER << param3; + return cmd.str(); +} + +inline std::string BuildQuery(const char* tag) +{ + std::ostringstream cmd; + cmd << tag << "?"; + return cmd.str(); +} + +// Parse response helpers +inline bool IsPositiveAck(const std::string& response, const char* tag) +{ + std::string expected = std::string(tag) + " +"; + return response.find(expected) == 0; +} + +inline bool IsValidAnswer(const std::string& response, const char* tag) +{ + if (response.length() < 3) + return false; + if (response.substr(0, strlen(tag)) != tag) + return false; + if (response.substr(3, 1) == "!") + return false; + return true; +} + +inline bool IsNegativeAck(const std::string& response, const char* tag) +{ + std::string expected = std::string(tag) + " !"; + return response.find(expected) == 0; +} + +// Check if response contains position out of range error (E0B500020) +inline bool IsPositionOutOfRangeError(const std::string& response) +{ + return response.find("E0B500020") != std::string::npos; +} + +// Check if response contains device busy error (error code ending in 70) +inline bool IsDeviceBusyError(const std::string& response) +{ + if (response.size() < 3) + return false; + // Error code ending in "70" indicates device is busy + // Response format: "ESH1 !,E0BB00070" or similar + return response.find("!") != std::string::npos + && response.substr(response.size() - 2, 2) == "70"; +} + +// Check if response is empty (device not ready) +inline bool IsEmptyResponse(const std::string& response) +{ + return response.empty(); +} + +inline bool IsUnknown(const std::string& response) +{ + return response.find(" X") != std::string::npos; +} + +inline std::string ExtractTag(const std::string& response) +{ + size_t pos = response.find(TAG_DELIMITER); + if (pos == std::string::npos) + { + // No delimiter, might be tag-only response or tag with '?' + pos = response.find('?'); + if (pos == std::string::npos) + return response; + else + return response.substr(0, pos); + } + return response.substr(0, pos); +} + +inline std::vector ParseParameters(const std::string& response) +{ + std::vector params; + size_t tagEnd = response.find(TAG_DELIMITER); + if (tagEnd == std::string::npos) + return params; + + std::string dataStr = response.substr(tagEnd + 1); + std::istringstream iss(dataStr); + std::string param; + + while (std::getline(iss, param, DATA_DELIMITER)) + { + // Trim whitespace + size_t start = param.find_first_not_of(" \t\r\n"); + size_t end = param.find_last_not_of(" \t\r\n"); + if (start != std::string::npos && end != std::string::npos) + params.push_back(param.substr(start, end - start + 1)); + else if (start != std::string::npos) + params.push_back(param.substr(start)); + } + + return params; +} + +inline int ParseIntParameter(const std::string& param) +{ + // Handle empty string + if (param.empty()) + return -1; + + // Handle unknown/not-present indicators + if (param == "X" || param == "x") + return -1; + + // Handle acknowledgment characters (shouldn't be parsed as numbers) + if (param == "+" || param == "!") + return -1; + + // Try to parse as integer with exception handling + try + { + return std::stoi(param); + } + catch (const std::invalid_argument&) + { + // Not a valid integer + return -1; + } + catch (const std::out_of_range&) + { + // Number too large for int + return -1; + } +} + +inline long ParseLongParameter(const std::string& param) +{ + // Handle empty string + if (param.empty()) + return -1; + + // Handle unknown/not-present indicators + if (param == "X" || param == "x") + return -1; + + // Handle acknowledgment characters (shouldn't be parsed as numbers) + if (param == "+" || param == "!") + return -1; + + // Try to parse as long with exception handling + try + { + return std::stol(param); + } + catch (const std::invalid_argument&) + { + // Not a valid long + return -1; + } + catch (const std::out_of_range&) + { + // Number too large for long + return -1; + } +} + +// Helper function to check if position is within tolerance of target +inline bool IsAtTargetPosition(long currentPos, long targetPos, long tolerance) +{ + if (targetPos < 0) + return false; // No target set + + long diff = currentPos - targetPos; + if (diff < 0) + diff = -diff; // Absolute value + + return diff <= tolerance; +} + +} // namespace EvidentIX85Win diff --git a/DeviceAdapters/EvidentIX85Win/EvidentSDK.h b/DeviceAdapters/EvidentIX85Win/EvidentSDK.h new file mode 100644 index 000000000..eb9d673cc --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/EvidentSDK.h @@ -0,0 +1,127 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentSDK.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX5 SDK wrapper for DLL integration +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include +#include + +// Define DLL_IMPORT as empty - we load the DLL dynamically +#ifndef DLL_IMPORT +#define DLL_IMPORT +#endif + +// Forward declare types used in mdk_if.h that we don't need +typedef enum { e_DISerial = 2 } eDeviceInterface; +typedef enum { e_DTUnknown = 0 } eDeviceType; + +// Define constants from mdk_def.h +#define MAX_STRING 512 +#ifndef IN +#define IN +#endif +#ifndef OUT +#define OUT +#endif + +// Define callback type from gt_type.h +typedef int (__stdcall *GT_CALLBACK_ENTRY)( + ULONG MsgId, + ULONG wParam, + ULONG lParam, + PVOID pv, + PVOID pContext, + PVOID pCaller +); +typedef GT_CALLBACK_ENTRY GT_MDK_CALLBACK; + +// Include the actual SDK header to ensure structure matches exactly +#include "mdk_if.h" + +namespace EvidentSDK { + +// Re-export SDK types for convenience (macros like MAX_COMMAND_SIZE are already global) +using ::MDK_MSL_CMD; +using ::eMDK_CmdStatus; + +// Use function pointer types from mdk_if.h (they're defined at global scope) +using ::fn_MSL_PM_Initialize; +using ::fn_MSL_PM_EnumInterface; +using ::fn_MSL_PM_GetInterfaceInfo; +using ::fn_MSL_PM_GetPortName; +using ::fn_MSL_PM_OpenInterface; +using ::fn_MSL_PM_CloseInterface; +using ::fn_MSL_PM_SendCommand; +using ::fn_MSL_PM_RegisterCallback; + +// SDK Error Codes +const int SDK_ERR_OFFSET = 10300; +const int SDK_ERR_DLL_NOT_FOUND = SDK_ERR_OFFSET + 1; +const int SDK_ERR_DLL_INIT_FAILED = SDK_ERR_OFFSET + 2; +const int SDK_ERR_FUNCTION_NOT_FOUND = SDK_ERR_OFFSET + 3; +const int SDK_ERR_NO_INTERFACE = SDK_ERR_OFFSET + 4; +const int SDK_ERR_OPEN_FAILED = SDK_ERR_OFFSET + 5; +const int SDK_ERR_SEND_FAILED = SDK_ERR_OFFSET + 6; +const int SDK_ERR_CALLBACK_FAILED = SDK_ERR_OFFSET + 7; + +// Helper functions +inline void InitCommand(MDK_MSL_CMD& cmd) +{ + memset(&cmd, 0x00, sizeof(MDK_MSL_CMD)); + cmd.m_Signature = GT_MDK_CMD_SIGNATURE; // Required signature + cmd.m_Timeout = 50000; // 50 seconds default + cmd.m_Sync = FALSE; // Asynchronous +} + +inline void SetCommandString(MDK_MSL_CMD& cmd, const std::string& command) +{ + // SDK expects command string to include \r\n terminator + std::string cmdWithTerminator = command + "\r\n"; + + size_t len = cmdWithTerminator.length(); + if (len >= MAX_COMMAND_SIZE) + len = MAX_COMMAND_SIZE - 1; + + memcpy(cmd.m_Cmd, cmdWithTerminator.c_str(), len); + cmd.m_CmdSize = (DWORD)len; +} + +inline std::string GetResponseString(const MDK_MSL_CMD& cmd) +{ + if (cmd.m_RspSize > 0 && cmd.m_RspSize <= MAX_RESPONSE_SIZE) + return std::string((const char*)cmd.m_Rsp, cmd.m_RspSize); + return ""; +} + +// For notifications, the SDK may use m_Cmd instead of m_Rsp +inline std::string GetNotificationString(const MDK_MSL_CMD& cmd) +{ + // Try response field first + if (cmd.m_RspSize > 0 && cmd.m_RspSize <= MAX_RESPONSE_SIZE) + return std::string((const char*)cmd.m_Rsp, cmd.m_RspSize); + // Fall back to command field for notifications + if (cmd.m_CmdSize > 0 && cmd.m_CmdSize <= MAX_COMMAND_SIZE) + return std::string((const char*)cmd.m_Cmd, cmd.m_CmdSize); + return ""; +} + +} // namespace EvidentSDK diff --git a/DeviceAdapters/EvidentIX85Win/Makefile.am b/DeviceAdapters/EvidentIX85Win/Makefile.am new file mode 100644 index 000000000..107c7f8b6 --- /dev/null +++ b/DeviceAdapters/EvidentIX85Win/Makefile.am @@ -0,0 +1,12 @@ + +AM_CXXFLAGS = $(MMDEVAPI_CXXFLAGS) $(BOOST_CPPFLAGS) -std=c++17 +deviceadapter_LTLIBRARIES = libmmgr_dal_EvidentIX85.la +libmmgr_dal_EvidentIX85_la_SOURCES = EvidentHub.cpp EvidentHub.h \ + EvidentIX85.cpp EvidentIX85.h \ + EvidentModel.cpp EvidentModel.h \ + EvidentProtocol.h \ + ../../MMDevice/MMDevice.h +libmmgr_dal_EvidentIX85_la_LDFLAGS = $(MMDEVAPI_LDFLAGS) +libmmgr_dal_EvidentIX85_la_LIBADD = $(MMDEVAPI_LIBADD) + +EXTRA_DIST = EvidentIX85.vcxproj diff --git a/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStage.cpp b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStage.cpp new file mode 100644 index 000000000..4c67c9cfc --- /dev/null +++ b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStage.cpp @@ -0,0 +1,1082 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentIX85XYStage.cpp +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 XY Stage device implementation (IX5-SSA hardware) +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#include "EvidentIX85XYStage.h" +#include "EvidentIX85XYStageProtocol.h" +#include "EvidentIX85XYStageModel.h" +#include "ModuleInterface.h" +#include +#include +#include + +using namespace IX85XYStage; + +const char* g_DeviceName = "IX85_XYStage"; + +// Property names +const char* g_PropertyPort = "Port"; +const char* g_PropertySpeed = "Speed"; +const char* g_PropertyJogEnable = "JOG Enable"; +const char* g_PropertyJogSensitivity = "JOG Sensitivity"; +const char* g_PropertyJogDirectionX = "JOG Direction X"; +const char* g_PropertyJogDirectionY = "JOG Direction Y"; + +// Property values +const char* g_Yes = "Yes"; +const char* g_No = "No"; +const char* g_Normal = "Normal"; +const char* g_Reverse = "Reverse"; +/////////////////////////////////////////////////////////////////////////////// +// Exported MMDevice API +/////////////////////////////////////////////////////////////////////////////// + +MODULE_API void InitializeModuleData() +{ + RegisterDevice(g_DeviceName, MM::XYStageDevice, "Evident IX85 XY Stage"); +} + +MODULE_API MM::Device* CreateDevice(const char* deviceName) +{ + if (deviceName == nullptr) + return nullptr; + + if (strcmp(deviceName, g_DeviceName) == 0) + return new EvidentIX85XYStage(); + + return nullptr; +} + +MODULE_API void DeleteDevice(MM::Device* pDevice) +{ + delete pDevice; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage Constructor +/////////////////////////////////////////////////////////////////////////////// + +EvidentIX85XYStage::EvidentIX85XYStage() : + initialized_(false), + port_("Undefined"), + name_(g_DeviceName), + stepSizeXUm_(0.078125), // 78.125nm per step + stepSizeYUm_(0.078125), + stopMonitoring_(false), + responseReady_(false) +{ + InitializeDefaultErrorMessages(); + + // Add custom error messages + SetErrorText(ERR_PORT_NOT_SET, "Port not set. Use the Port property to select a serial port."); + SetErrorText(ERR_COMMAND_TIMEOUT, "Command timeout - no response from device"); + SetErrorText(ERR_NEGATIVE_ACK, "Device returned negative acknowledgment"); + SetErrorText(ERR_INVALID_RESPONSE, "Invalid response from device"); + + // Pre-initialization property: Port + CPropertyAction* pAct = new CPropertyAction(this, &EvidentIX85XYStage::OnPort); + CreateProperty(g_PropertyPort, "Undefined", MM::String, false, pAct, true); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage Destructor +/////////////////////////////////////////////////////////////////////////////// + +EvidentIX85XYStage::~EvidentIX85XYStage() +{ + Shutdown(); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage::GetName +/////////////////////////////////////////////////////////////////////////////// + +void EvidentIX85XYStage::GetName(char* pszName) const +{ + CDeviceUtils::CopyLimitedString(pszName, name_.c_str()); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage::Initialize +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::Initialize() +{ + if (initialized_) + return DEVICE_OK; + + // Check if port is set + if (port_ == "Undefined") + return ERR_PORT_NOT_SET; + + // Purge any old data from the port + PurgeComPort(port_.c_str()); + + // CRITICAL: Start monitoring thread BEFORE sending any commands + StartMonitoring(); + + // Login to remote mode + int ret = LoginToRemoteMode(); + if (ret != DEVICE_OK) + { + StopMonitoring(); + return ret; + } + + // Query version + ret = QueryVersion(); + if (ret != DEVICE_OK) + { + StopMonitoring(); + return ret; + } + + // Verify this is an IX5-SSA device + ret = VerifyDevice(); + if (ret != DEVICE_OK) + { + StopMonitoring(); + return ret; + } + + // Initialize the stage (home it) + ret = InitializeStage(); + if (ret != DEVICE_OK) + { + StopMonitoring(); + return ret; + } + + // Enable XY Stage JOG operation + ret = EnableJog(true); + if (ret != DEVICE_OK) + { + StopMonitoring(); + return ret; + } + + // Enable position notifications + ret = EnableNotifications(true); + if (ret != DEVICE_OK) + { + StopMonitoring(); + return ret; + } + + // Set speed + long initial, high, accel; + model_.GetSpeed(initial, high, accel); + std::string cmd = BuildCommand(CMD_XY_SPEED, (int)initial, (int)high, (int)accel); + std::string response; + ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + StopMonitoring(); + return ret; + } + + // Create post-initialization properties + CPropertyAction* pAct = new CPropertyAction(this, &EvidentIX85XYStage::OnSpeed); + ret = CreateProperty(g_PropertySpeed, "256000", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + SetPropertyLimits(g_PropertySpeed, 0, 512000); + + pAct = new CPropertyAction(this, &EvidentIX85XYStage::OnJogEnable); + ret = CreateProperty(g_PropertyJogEnable, g_No, MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue(g_PropertyJogEnable, g_Yes); + AddAllowedValue(g_PropertyJogEnable, g_No); + + pAct = new CPropertyAction(this, &EvidentIX85XYStage::OnJogSensitivity); + ret = CreateProperty(g_PropertyJogSensitivity, "8", MM::Integer, false, pAct); + if (ret != DEVICE_OK) + return ret; + SetPropertyLimits(g_PropertyJogSensitivity, 1, 16); + + pAct = new CPropertyAction(this, &EvidentIX85XYStage::OnJogDirectionX); + ret = CreateProperty(g_PropertyJogDirectionX, g_Normal, MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue(g_PropertyJogDirectionX, g_Normal); + AddAllowedValue(g_PropertyJogDirectionX, g_Reverse); + + pAct = new CPropertyAction(this, &EvidentIX85XYStage::OnJogDirectionY); + ret = CreateProperty(g_PropertyJogDirectionY, g_Normal, MM::String, false, pAct); + if (ret != DEVICE_OK) + return ret; + AddAllowedValue(g_PropertyJogDirectionY, g_Normal); + AddAllowedValue(g_PropertyJogDirectionY, g_Reverse); + + initialized_ = true; + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage::Shutdown +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::Shutdown() +{ + if (!initialized_) + return DEVICE_OK; + + // Disable position notifications first + EnableNotifications(false); + + // Disable jog if enabled + if (model_.IsJogEnabled()) + { + EnableJog(false); + } + + // Return to local mode + std::string cmd = BuildCommand(CMD_LOGIN, 0); + std::string response; + ExecuteCommand(cmd, response); + + // Stop monitoring thread LAST (after all commands sent) + StopMonitoring(); + + initialized_ = false; + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage::Busy +/////////////////////////////////////////////////////////////////////////////// + +bool EvidentIX85XYStage::Busy() +{ + // Check model state (updated by position notifications) + return model_.IsBusy(); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage::SetPositionSteps +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::SetPositionSteps(long x, long y) +{ + // Set target in model first + model_.SetTarget(x, y); + + // Check if we're already at the target + if (model_.IsAtTarget(XY_STAGE_POSITION_TOLERANCE)) + { + // Already at target, no need to move + model_.SetBusy(false); + return DEVICE_OK; + } + + // Build XYG command for absolute positioning + std::string cmd = BuildCommand(CMD_XY_GOTO, x, y); + + model_.SetBusy(true); + + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + model_.SetBusy(false); + return ret; + } + + // Check for positive acknowledgment + if (!IsPositiveAck(response, CMD_XY_GOTO)) + { + model_.SetBusy(false); + if (IsNegativeAck(response, CMD_XY_GOTO)) + return ERR_NEGATIVE_ACK; + return ERR_INVALID_RESPONSE; + } + + // Check again if already at target (for very short moves that complete instantly) + if (model_.IsAtTarget(XY_STAGE_POSITION_TOLERANCE)) + { + model_.SetBusy(false); + } + + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage::GetPositionSteps +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::GetPositionSteps(long& x, long& y) +{ + // Get position from model (updated by position notifications) + model_.GetPosition(x, y); + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage::SetRelativePositionSteps +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::SetRelativePositionSteps(long x, long y) +{ + // Get current position from model + long currentX, currentY; + model_.GetPosition(currentX, currentY); + + // Calculate absolute target position + long targetX = currentX + x; + long targetY = currentY + y; + + // Set target in model + model_.SetTarget(targetX, targetY); + + // Check if we're already at the target (e.g., relative move of 0,0) + if (model_.IsAtTarget(XY_STAGE_POSITION_TOLERANCE)) + { + // Already at target, no need to move + model_.SetBusy(false); + return DEVICE_OK; + } + + // Use XYG (absolute positioning) for relative moves + // Note: XYM on IX5-SSA appears to do absolute positioning, not relative + std::string cmd = BuildCommand(CMD_XY_GOTO, targetX, targetY); + + model_.SetBusy(true); + + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + { + model_.SetBusy(false); + return ret; + } + + // Check for positive acknowledgment + if (!IsPositiveAck(response, CMD_XY_GOTO)) + { + model_.SetBusy(false); + if (IsNegativeAck(response, CMD_XY_GOTO)) + return ERR_NEGATIVE_ACK; + return ERR_INVALID_RESPONSE; + } + + // Check again if already at target (for very short moves that complete instantly) + if (model_.IsAtTarget(XY_STAGE_POSITION_TOLERANCE)) + { + model_.SetBusy(false); + } + + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage::Home +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::Home() +{ + return InitializeStage(); +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage::Stop +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::Stop() +{ + std::string response; + int ret = ExecuteCommand(CMD_XY_STOP, response); + if (ret == DEVICE_OK) + { + model_.SetBusy(false); + } + return ret; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage::SetOrigin +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::SetOrigin() +{ + // The SSA doesn't have a "set origin" command, so we just note the current + // position as 0,0 in software. This would require tracking an offset. + // For now, return not supported. + return DEVICE_UNSUPPORTED_COMMAND; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage::GetLimitsUm +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::GetLimitsUm(double& xMin, double& xMax, double& yMin, double& yMax) +{ + std::pair xy = ConvertPositionStepsToUm(XY_STAGE_MIN_POS_X, XY_STAGE_MIN_POS_Y); + xMin = xy.first; + yMin = xy.second; + xy = ConvertPositionStepsToUm(XY_STAGE_MAX_POS_X, XY_STAGE_MAX_POS_Y); + xMax = xy.first; + yMax = xy.second; + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage::GetStepLimits +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::GetStepLimits(long& xMin, long& xMax, long& yMin, long& yMax) +{ + xMin = XY_STAGE_MIN_POS_X; + xMax = XY_STAGE_MAX_POS_X; + yMin = XY_STAGE_MIN_POS_Y; + yMax = XY_STAGE_MAX_POS_Y; + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// Property Handlers +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::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 EvidentIX85XYStage::OnSpeed(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + long initial, high, accel; + model_.GetSpeed(initial, high, accel); + pProp->Set(high); + } + else if (eAct == MM::AfterSet) + { + long speed; + pProp->Get(speed); + + long initial, high, accel; + model_.GetSpeed(initial, high, accel); + high = speed; + model_.SetSpeed(initial, high, accel); + + // Send XYSPD command + std::string cmd = BuildCommand(CMD_XY_SPEED, (int)initial, (int)high, (int)accel); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_XY_SPEED)) + { + if (IsNegativeAck(response, CMD_XY_SPEED)) + return ERR_NEGATIVE_ACK; + return ERR_INVALID_RESPONSE; + } + } + return DEVICE_OK; +} + +int EvidentIX85XYStage::OnJogEnable(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + bool enabled = model_.IsJogEnabled(); + pProp->Set(enabled ? g_Yes : g_No); + } + else if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + bool enabled = (value == g_Yes); + + // Enable/disable jog + int ret = EnableJog(enabled); + if (ret != DEVICE_OK) + return ret; + + // Enable/disable encoders + std::string cmd; + std::string response; + if (enabled) + { + // Enable encoder 1 (X) + cmd = BuildCommand(CMD_ENCODER_1, 1); + ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Enable encoder 2 (Y) + cmd = BuildCommand(CMD_ENCODER_2, 1); + ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + } + else + { + // Disable encoders + cmd = BuildCommand(CMD_ENCODER_1, 0); + ExecuteCommand(cmd, response); + + cmd = BuildCommand(CMD_ENCODER_2, 0); + ExecuteCommand(cmd, response); + } + } + return DEVICE_OK; +} + +int EvidentIX85XYStage::OnJogSensitivity(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + int sensitivity = model_.GetJogSensitivity(); + pProp->Set((long)sensitivity); + } + else if (eAct == MM::AfterSet) + { + long sensitivity; + pProp->Get(sensitivity); + model_.SetJogSensitivity((int)sensitivity); + + std::string cmd = BuildCommand(CMD_XY_JOG_SENSITIVITY, (int)sensitivity); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_XY_JOG_SENSITIVITY)) + { + if (IsNegativeAck(response, CMD_XY_JOG_SENSITIVITY)) + return ERR_NEGATIVE_ACK; + return ERR_INVALID_RESPONSE; + } + } + return DEVICE_OK; +} + +int EvidentIX85XYStage::OnJogDirectionX(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + bool reverse = model_.GetJogDirectionX(); + pProp->Set(reverse ? g_Reverse : g_Normal); + } + else if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + bool reverse = (value == g_Reverse); + model_.SetJogDirectionX(reverse); + + int direction = reverse ? 0 : 1; + std::string cmd = BuildCommand(CMD_XY_JOG_DIR_X, direction); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_XY_JOG_DIR_X)) + { + if (IsNegativeAck(response, CMD_XY_JOG_DIR_X)) + return ERR_NEGATIVE_ACK; + return ERR_INVALID_RESPONSE; + } + } + return DEVICE_OK; +} + +int EvidentIX85XYStage::OnJogDirectionY(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + bool reverse = model_.GetJogDirectionY(); + pProp->Set(reverse ? g_Reverse : g_Normal); + } + else if (eAct == MM::AfterSet) + { + std::string value; + pProp->Get(value); + bool reverse = (value == g_Reverse); + model_.SetJogDirectionY(reverse); + + int direction = reverse ? 0 : 1; + std::string cmd = BuildCommand(CMD_XY_JOG_DIR_Y, direction); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_XY_JOG_DIR_Y)) + { + if (IsNegativeAck(response, CMD_XY_JOG_DIR_Y)) + return ERR_NEGATIVE_ACK; + return ERR_INVALID_RESPONSE; + } + } + return DEVICE_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// Threading Methods +/////////////////////////////////////////////////////////////////////////////// + +void EvidentIX85XYStage::StartMonitoring() +{ + stopMonitoring_ = false; + monitorThread_ = std::thread(&EvidentIX85XYStage::MonitorThreadFunc, this); +} + +void EvidentIX85XYStage::StopMonitoring() +{ + stopMonitoring_ = true; + if (monitorThread_.joinable()) + monitorThread_.join(); +} + +void EvidentIX85XYStage::MonitorThreadFunc() +{ + std::string message; + + while (!stopMonitoring_) + { + // Read one byte at a time from serial port + unsigned char byte; + unsigned long bytesRead = 0; + int ret = ReadFromComPort(port_.c_str(), &byte, 1, bytesRead); + + if (ret != DEVICE_OK || bytesRead == 0) + { + // No data available, sleep briefly to avoid busy waiting + CDeviceUtils::SleepMs(10); + continue; + } + + // Build message until we get a newline + if (byte == '\n') + { + // Remove any trailing \r + if (!message.empty() && message[message.length() - 1] == '\r') + message.erase(message.length() - 1); + + if (!message.empty()) + { + // Check if this is a notification or a command response + if (IsNotificationTag(message)) + { + // Process notification directly + ProcessNotification(message); + } + else + { + // Signal waiting command thread + { + std::lock_guard lock(responseMutex_); + pendingResponse_ = message; + responseReady_ = true; + } + responseCV_.notify_one(); + } + + message.clear(); + } + } + else if (byte != '\r') + { + message += static_cast(byte); + } + } +} + +void EvidentIX85XYStage::ProcessNotification(const std::string& notification) +{ + std::vector params = ParseParameters(notification); + + // Handle position notification (NXYP) + if (notification.find("NXYP ") == 0) + { + if (params.size() >= 2) + { + // Parse position values + // Note: Negative positions are valid (stage range is -743680 to 743680) + // ParseLongParameter returns -1 for invalid/unparseable values + std::string xStr = params[0]; + std::string yStr = params[1]; + + // Check for invalid position markers (X, x, +, !, etc.) + if (xStr.empty() || yStr.empty() || + xStr == "X" || xStr == "x" || xStr == "+" || xStr == "!" || + yStr == "X" || yStr == "x" || yStr == "+" || yStr == "!") + { + return; // Invalid position, skip + } + + long x = ParseLongParameter(xStr); + long y = ParseLongParameter(yStr); + + model_.SetPosition(x, y); + + // Check if we've reached the target + if (model_.IsAtTarget(XY_STAGE_POSITION_TOLERANCE)) + { + model_.SetBusy(false); + } + + // Notify core of position change + std::pair xy = ConvertPositionStepsToUm(x, y); + OnXYStagePositionChanged(xy.first, xy.second); + } + return; + } + + // Handle encoder 1 notification (E1) + if (notification.find("E1 ") == 0) + { + if (params.size() >= 1) + { + int delta = ParseIntParameter(params[0]); + if (delta != 0) + { + // Calculate new encoder position with wrapping + long currentPos = model_.GetEncoderPosition(1); + long newPos = currentPos + delta; + + // Wrap encoder position (assuming 0-99 range) + while (newPos < 0) + newPos += 100; + while (newPos >= 100) + newPos -= 100; + + model_.SetEncoderPosition(1, newPos); + + // Move stage in X direction based on delta and jog settings + int sensitivity = model_.GetJogSensitivity(); + bool reverse = model_.GetJogDirectionX(); + long moveAmount = delta * sensitivity; + if (reverse) + moveAmount = -moveAmount; + + // Get current position and calculate new position + long x, y; + model_.GetPosition(x, y); + long newX = x + moveAmount; + + // Clamp to limits + long minX, maxX; + model_.GetLimitsX(minX, maxX); + if (newX < minX) newX = minX; + if (newX > maxX) newX = maxX; + + // Send move command (don't wait for response to avoid blocking monitor thread) + std::string cmd = BuildCommand(CMD_XY_GOTO, newX, y); + SendCommand(cmd); + model_.SetTarget(newX, y); + model_.SetBusy(true); + } + } + return; + } + + // Handle encoder 2 notification (E2) + if (notification.find("E2 ") == 0) + { + if (params.size() >= 1) + { + int delta = ParseIntParameter(params[0]); + if (delta != 0) + { + // Calculate new encoder position with wrapping + long currentPos = model_.GetEncoderPosition(2); + long newPos = currentPos + delta; + + // Wrap encoder position (assuming 0-99 range) + while (newPos < 0) + newPos += 100; + while (newPos >= 100) + newPos -= 100; + + model_.SetEncoderPosition(2, newPos); + + // Move stage in Y direction based on delta and jog settings + int sensitivity = model_.GetJogSensitivity(); + bool reverse = model_.GetJogDirectionY(); + long moveAmount = delta * sensitivity; + if (reverse) + moveAmount = -moveAmount; + + // Get current position and calculate new position + long x, y; + model_.GetPosition(x, y); + long newY = y + moveAmount; + + // Clamp to limits + long minY, maxY; + model_.GetLimitsY(minY, maxY); + if (newY < minY) newY = minY; + if (newY > maxY) newY = maxY; + + // Send move command (don't wait for response to avoid blocking monitor thread) + std::string cmd = BuildCommand(CMD_XY_GOTO, x, newY); + SendCommand(cmd); + model_.SetTarget(x, newY); + model_.SetBusy(true); + } + } + return; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Command/Response Pattern +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::ExecuteCommand(const std::string& cmd, std::string& response, long timeoutMs) +{ + // Lock to ensure only one command at a time + std::lock_guard cmdLock(commandMutex_); + + // Clear any pending response + { + std::lock_guard respLock(responseMutex_); + responseReady_ = false; + pendingResponse_.clear(); + } + + // Send command + int ret = SendCommand(cmd); + if (ret != DEVICE_OK) + return ret; + + // Wait for response + ret = GetResponse(response, timeoutMs); + return ret; +} + +/////////////////////////////////////////////////////////////////////////////// +// Private Helper Methods +/////////////////////////////////////////////////////////////////////////////// + +int EvidentIX85XYStage::SendCommand(const std::string& cmd) +{ + int ret = SendSerialCommand(port_.c_str(), cmd.c_str(), TERMINATOR); + if (ret != DEVICE_OK) + return ret; + + return DEVICE_OK; +} + +int EvidentIX85XYStage::GetResponse(std::string& response, long timeoutMs) +{ + std::unique_lock lock(responseMutex_); + + // Wait for response with timeout + bool received = responseCV_.wait_for(lock, std::chrono::milliseconds(timeoutMs), + [this] { return responseReady_; }); + + if (!received || !responseReady_) + return ERR_COMMAND_TIMEOUT; + + response = pendingResponse_; + responseReady_ = false; + pendingResponse_.clear(); + + return DEVICE_OK; +} + +MM::DeviceDetectionStatus EvidentIX85XYStage::DetectDevice(void) +{ + if (initialized_) + return MM::CanCommunicate; + + // All conditions must be satisfied... + MM::DeviceDetectionStatus result = MM::Misconfigured; + char answerTO[MM::MaxStrLength]; + + try + { + std::string portLowerCase = port_; + for (std::string::iterator its = portLowerCase.begin(); its != portLowerCase.end(); ++its) + { + *its = (char)tolower(*its); + } + if (0 < portLowerCase.length() && 0 != portLowerCase.compare("undefined") && 0 != portLowerCase.compare("unknown")) + { + result = MM::CanNotCommunicate; + // Record current port settings + GetCoreCallback()->GetDeviceProperty(port_.c_str(), "AnswerTimeout", answerTO); + + // Device specific default communication parameters + GetCoreCallback()->SetDeviceProperty(port_.c_str(), MM::g_Keyword_BaudRate, "115200"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), MM::g_Keyword_StopBits, "2"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), MM::g_Keyword_Parity, "Even"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "Verbose", "0"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "AnswerTimeout", "5000.0"); + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "DelayBetweenCharsMs", "0"); + MM::Device* pS = GetCoreCallback()->GetDevice(this, port_.c_str()); + pS->Initialize(); + + // Try to query the unit name + PurgeComPort(port_.c_str()); + std::string response; + std::string cmd = "U?"; + int ret = SendSerialCommand(port_.c_str(), cmd.c_str(), TERMINATOR); + if (ret == DEVICE_OK) + { + ret = GetSerialAnswer(port_.c_str(), TERMINATOR, response); + if (ret == DEVICE_OK && response.find("IX5-SSA") != std::string::npos) + { + result = MM::CanCommunicate; + } + } + + pS->Shutdown(); + // Always restore the AnswerTimeout to the default + GetCoreCallback()->SetDeviceProperty(port_.c_str(), "AnswerTimeout", answerTO); + } + } + catch (...) + { + LogMessage("Exception in DetectDevice!", false); + } + + return result; +} + +int EvidentIX85XYStage::LoginToRemoteMode() +{ + std::string cmd = BuildCommand(CMD_LOGIN, 1); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_LOGIN)) + { + if (IsNegativeAck(response, CMD_LOGIN)) + return ERR_NEGATIVE_ACK; + return ERR_INVALID_RESPONSE; + } + + return DEVICE_OK; +} + +int EvidentIX85XYStage::QueryVersion() +{ + std::string cmd = BuildCommand(CMD_VERSION, 1); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Parse version from response + std::vector params = ParseParameters(response); + if (params.size() > 0) + { + model_.SetVersion(params[0]); + } + + return DEVICE_OK; +} + +int EvidentIX85XYStage::VerifyDevice() +{ + std::string cmd = BuildCommand(CMD_UNIT); + cmd += "?"; + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + // Response should contain "IX5-SSA" + if (response.find("IX5-SSA") == std::string::npos) + return ERR_DEVICE_NOT_AVAILABLE; + + return DEVICE_OK; +} + +int EvidentIX85XYStage::InitializeStage() +{ + // Use XYINIT to initialize the stage + // This can take a long time (several seconds), so use longer timeout + std::string response; + const long initTimeout = 30000; // 30 seconds + int ret = ExecuteCommand(CMD_XY_INIT, response, initTimeout); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_XY_INIT)) + { + if (IsNegativeAck(response, CMD_XY_INIT)) + return ERR_NEGATIVE_ACK; + return ERR_INVALID_RESPONSE; + } + + return DEVICE_OK; +} + +int EvidentIX85XYStage::EnableJog(bool enable) +{ + std::string cmd = BuildCommand(CMD_XY_JOG, enable ? 1 : 0); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_XY_JOG)) + { + if (IsNegativeAck(response, CMD_XY_JOG)) + return ERR_NEGATIVE_ACK; + return ERR_INVALID_RESPONSE; + } + + model_.SetJogEnabled(enable); + return DEVICE_OK; +} + +int EvidentIX85XYStage::EnableNotifications(bool enable) +{ + std::string cmd = BuildCommand(CMD_XY_POSITION_NOTIFY, enable ? 1 : 0); + std::string response; + int ret = ExecuteCommand(cmd, response); + if (ret != DEVICE_OK) + return ret; + + if (!IsPositiveAck(response, CMD_XY_POSITION_NOTIFY)) + { + if (IsNegativeAck(response, CMD_XY_POSITION_NOTIFY)) + return ERR_NEGATIVE_ACK; + return ERR_INVALID_RESPONSE; + } + + return DEVICE_OK; +} diff --git a/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStage.h b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStage.h new file mode 100644 index 000000000..7941fe1d5 --- /dev/null +++ b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStage.h @@ -0,0 +1,118 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentIX85XYStage.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Evident IX85 XY Stage device adapter (IX5-SSA hardware) +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include "DeviceBase.h" +#include "EvidentIX85XYStageProtocol.h" +#include "EvidentIX85XYStageModel.h" +#include +#include +#include +#include +#include + +// Use protocol and model from separate files + +////////////////////////////////////////////////////////////////////////////// +// EvidentIX85XYStage - IX85 XY Stage Controller (IX5-SSA hardware) +////////////////////////////////////////////////////////////////////////////// + +class EvidentIX85XYStage : public CXYStageBase +{ +public: + EvidentIX85XYStage(); + ~EvidentIX85XYStage(); + + // MMDevice API + int Initialize(); + int Shutdown(); + void GetName(char* pszName) const; + bool Busy(); + + bool SupportsDeviceDetection(void) { return true; } + MM::DeviceDetectionStatus DetectDevice(void); + + // XYStage API + int SetPositionSteps(long x, long y); + int GetPositionSteps(long& x, long& y); + int SetRelativePositionSteps(long x, long y); + int Home(); + int Stop(); + int SetOrigin(); + int GetLimitsUm(double& xMin, double& xMax, double& yMin, double& yMax); + int GetStepLimits(long& xMin, long& xMax, long& yMin, long& yMax); + double GetStepSizeXUm() { return stepSizeXUm_; } + double GetStepSizeYUm() { return stepSizeYUm_; } + int IsXYStageSequenceable(bool& isSequenceable) const { isSequenceable = false; return DEVICE_OK; } + + // Action interface + int OnPort(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnSpeed(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnJogEnable(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnJogSensitivity(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnJogDirectionX(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnJogDirectionY(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + // Threading + void MonitorThreadFunc(); + void StartMonitoring(); + void StopMonitoring(); + + // Command/response pattern + int ExecuteCommand(const std::string& cmd, std::string& response, long timeoutMs = 4000); + int SendCommand(const std::string& cmd); + int GetResponse(std::string& response, long timeoutMs = 4000); + + // Notification processing + void ProcessNotification(const std::string& notification); + + // Helper methods + int LoginToRemoteMode(); + int VerifyDevice(); + int QueryVersion(); + int InitializeStage(); + int EnableJog(bool enable); + int EnableNotifications(bool enable); + + // State + bool initialized_; + std::string port_; + std::string name_; + double stepSizeXUm_; + double stepSizeYUm_; + + // Model - thread-safe state storage + IX85XYStage::StageModel model_; + + // Threading + std::thread monitorThread_; + std::atomic stopMonitoring_; + + // Command synchronization + mutable std::mutex commandMutex_; // Protects command sending + std::mutex responseMutex_; // Protects response handling + std::condition_variable responseCV_; + std::string pendingResponse_; + bool responseReady_; +}; diff --git a/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStage.vcxproj b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStage.vcxproj new file mode 100644 index 000000000..2bf0b1132 --- /dev/null +++ b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStage.vcxproj @@ -0,0 +1,103 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {A7F8E9C3-5D4B-4E2F-9A1C-3B7D8E6F4A2C} + EvidentIX85XYStage + 10.0 + + + + DynamicLibrary + MultiByte + true + v143 + false + + + DynamicLibrary + MultiByte + v143 + true + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + true + false + + + + X64 + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + EnableFastChecks + 4290;%(DisableSpecificWarnings) + + + %(AdditionalLibraryDirectories) + Windows + + + + + X64 + + + MaxSpeed + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + 4290;%(DisableSpecificWarnings) + + + %(AdditionalLibraryDirectories) + Windows + true + true + + + + + + + + + + + + + + {b8c95f39-54bf-40a9-807b-598df2821d55} + + + + + + \ No newline at end of file diff --git a/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStage.vcxproj.filters b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStage.vcxproj.filters new file mode 100644 index 000000000..8e577dc33 --- /dev/null +++ b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStage.vcxproj.filters @@ -0,0 +1,30 @@ + + + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + + + {50b74e54-a977-4d12-ba69-087fcdda093b} + + + {1ef5367e-c881-4f98-927f-aee360056218} + + + \ No newline at end of file diff --git a/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStageModel.cpp b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStageModel.cpp new file mode 100644 index 000000000..71cd674c8 --- /dev/null +++ b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStageModel.cpp @@ -0,0 +1,313 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentIX85XYStageModel.cpp +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: State model implementation for Evident IX85 XY Stage +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#include "EvidentIX85XYStageModel.h" +#include "EvidentIX85XYStageProtocol.h" +#include + +using namespace IX85XYStage; + +StageModel::StageModel() : + posX_(0), + posY_(0), + targetX_(-1), + targetY_(-1), + busy_(false), + version_(""), + minX_(XY_STAGE_MIN_POS_X), + maxX_(XY_STAGE_MAX_POS_X), + minY_(XY_STAGE_MIN_POS_Y), + maxY_(XY_STAGE_MAX_POS_Y), + speedInitial_(0), + speedHigh_(256000), + speedAccel_(2560000), + jogEnabled_(false), + jogSensitivity_(8), + jogReverseX_(false), + jogReverseY_(false), + encoderPos1_(0), + encoderPos2_(0) +{ +} + +StageModel::~StageModel() +{ +} + +/////////////////////////////////////////////////////////////////////////////// +// Position management +/////////////////////////////////////////////////////////////////////////////// + +void StageModel::SetPositionX(long pos) +{ + std::lock_guard lock(mutex_); + posX_ = pos; +} + +void StageModel::SetPositionY(long pos) +{ + std::lock_guard lock(mutex_); + posY_ = pos; +} + +void StageModel::SetPosition(long x, long y) +{ + std::lock_guard lock(mutex_); + posX_ = x; + posY_ = y; +} + +long StageModel::GetPositionX() const +{ + std::lock_guard lock(mutex_); + return posX_; +} + +long StageModel::GetPositionY() const +{ + std::lock_guard lock(mutex_); + return posY_; +} + +void StageModel::GetPosition(long& x, long& y) const +{ + std::lock_guard lock(mutex_); + x = posX_; + y = posY_; +} + +/////////////////////////////////////////////////////////////////////////////// +// Target position management +/////////////////////////////////////////////////////////////////////////////// + +void StageModel::SetTargetX(long pos) +{ + std::lock_guard lock(mutex_); + targetX_ = pos; +} + +void StageModel::SetTargetY(long pos) +{ + std::lock_guard lock(mutex_); + targetY_ = pos; +} + +void StageModel::SetTarget(long x, long y) +{ + std::lock_guard lock(mutex_); + targetX_ = x; + targetY_ = y; +} + +long StageModel::GetTargetX() const +{ + std::lock_guard lock(mutex_); + return targetX_; +} + +long StageModel::GetTargetY() const +{ + std::lock_guard lock(mutex_); + return targetY_; +} + +void StageModel::GetTarget(long& x, long& y) const +{ + std::lock_guard lock(mutex_); + x = targetX_; + y = targetY_; +} + +/////////////////////////////////////////////////////////////////////////////// +// Busy state +/////////////////////////////////////////////////////////////////////////////// + +void StageModel::SetBusy(bool busy) +{ + std::lock_guard lock(mutex_); + busy_ = busy; +} + +bool StageModel::IsBusy() const +{ + std::lock_guard lock(mutex_); + return busy_; +} + +bool StageModel::IsAtTarget(long tolerance) const +{ + std::lock_guard lock(mutex_); + + // Calculate absolute differences + long diffX = posX_ - targetX_; + if (diffX < 0) + diffX = -diffX; + + long diffY = posY_ - targetY_; + if (diffY < 0) + diffY = -diffY; + + return (diffX <= tolerance) && (diffY <= tolerance); +} + +/////////////////////////////////////////////////////////////////////////////// +// Version and device info +/////////////////////////////////////////////////////////////////////////////// + +void StageModel::SetVersion(const std::string& version) +{ + std::lock_guard lock(mutex_); + version_ = version; +} + +std::string StageModel::GetVersion() const +{ + std::lock_guard lock(mutex_); + return version_; +} + +/////////////////////////////////////////////////////////////////////////////// +// Limits +/////////////////////////////////////////////////////////////////////////////// + +void StageModel::SetLimitsX(long min, long max) +{ + std::lock_guard lock(mutex_); + minX_ = min; + maxX_ = max; +} + +void StageModel::SetLimitsY(long min, long max) +{ + std::lock_guard lock(mutex_); + minY_ = min; + maxY_ = max; +} + +void StageModel::GetLimitsX(long& min, long& max) const +{ + std::lock_guard lock(mutex_); + min = minX_; + max = maxX_; +} + +void StageModel::GetLimitsY(long& min, long& max) const +{ + std::lock_guard lock(mutex_); + min = minY_; + max = maxY_; +} + +/////////////////////////////////////////////////////////////////////////////// +// Speed settings +/////////////////////////////////////////////////////////////////////////////// + +void StageModel::SetSpeed(long initial, long high, long accel) +{ + std::lock_guard lock(mutex_); + speedInitial_ = initial; + speedHigh_ = high; + speedAccel_ = accel; +} + +void StageModel::GetSpeed(long& initial, long& high, long& accel) const +{ + std::lock_guard lock(mutex_); + initial = speedInitial_; + high = speedHigh_; + accel = speedAccel_; +} + +/////////////////////////////////////////////////////////////////////////////// +// Jog settings +/////////////////////////////////////////////////////////////////////////////// + +void StageModel::SetJogEnabled(bool enabled) +{ + std::lock_guard lock(mutex_); + jogEnabled_ = enabled; +} + +bool StageModel::IsJogEnabled() const +{ + std::lock_guard lock(mutex_); + return jogEnabled_; +} + +void StageModel::SetJogSensitivity(int sensitivity) +{ + std::lock_guard lock(mutex_); + jogSensitivity_ = sensitivity; +} + +int StageModel::GetJogSensitivity() const +{ + std::lock_guard lock(mutex_); + return jogSensitivity_; +} + +void StageModel::SetJogDirectionX(bool reverse) +{ + std::lock_guard lock(mutex_); + jogReverseX_ = reverse; +} + +void StageModel::SetJogDirectionY(bool reverse) +{ + std::lock_guard lock(mutex_); + jogReverseY_ = reverse; +} + +bool StageModel::GetJogDirectionX() const +{ + std::lock_guard lock(mutex_); + return jogReverseX_; +} + +bool StageModel::GetJogDirectionY() const +{ + std::lock_guard lock(mutex_); + return jogReverseY_; +} + +/////////////////////////////////////////////////////////////////////////////// +// Encoder settings +/////////////////////////////////////////////////////////////////////////////// + +void StageModel::SetEncoderPosition(int encoder, long position) +{ + std::lock_guard lock(mutex_); + if (encoder == 1) + encoderPos1_ = position; + else if (encoder == 2) + encoderPos2_ = position; +} + +long StageModel::GetEncoderPosition(int encoder) const +{ + std::lock_guard lock(mutex_); + if (encoder == 1) + return encoderPos1_; + else if (encoder == 2) + return encoderPos2_; + return 0; +} diff --git a/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStageModel.h b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStageModel.h new file mode 100644 index 000000000..1ad98643b --- /dev/null +++ b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStageModel.h @@ -0,0 +1,124 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentIX85XYStageModel.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: State model for Evident IX85 XY Stage +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include +#include + +namespace IX85XYStage { + +/////////////////////////////////////////////////////////////////////////////// +// StageModel - Thread-safe state management for IX85 XY Stage +/////////////////////////////////////////////////////////////////////////////// + +class StageModel +{ +public: + StageModel(); + ~StageModel(); + + // Position management + void SetPositionX(long pos); + void SetPositionY(long pos); + void SetPosition(long x, long y); + long GetPositionX() const; + long GetPositionY() const; + void GetPosition(long& x, long& y) const; + + // Target position management + void SetTargetX(long pos); + void SetTargetY(long pos); + void SetTarget(long x, long y); + long GetTargetX() const; + long GetTargetY() const; + void GetTarget(long& x, long& y) const; + + // Busy state + void SetBusy(bool busy); + bool IsBusy() const; + bool IsAtTarget(long tolerance) const; + + // Version and device info + void SetVersion(const std::string& version); + std::string GetVersion() const; + + // Limits + void SetLimitsX(long min, long max); + void SetLimitsY(long min, long max); + void GetLimitsX(long& min, long& max) const; + void GetLimitsY(long& min, long& max) const; + + // Speed settings + void SetSpeed(long initial, long high, long accel); + void GetSpeed(long& initial, long& high, long& accel) const; + + // Jog settings + void SetJogEnabled(bool enabled); + bool IsJogEnabled() const; + void SetJogSensitivity(int sensitivity); + int GetJogSensitivity() const; + void SetJogDirectionX(bool reverse); + void SetJogDirectionY(bool reverse); + bool GetJogDirectionX() const; + bool GetJogDirectionY() const; + + // Encoder settings + void SetEncoderPosition(int encoder, long position); + long GetEncoderPosition(int encoder) const; + +private: + mutable std::mutex mutex_; + + // Position state + long posX_; + long posY_; + long targetX_; + long targetY_; + bool busy_; + + // Device info + std::string version_; + + // Limits + long minX_; + long maxX_; + long minY_; + long maxY_; + + // Speed + long speedInitial_; + long speedHigh_; + long speedAccel_; + + // Jog + bool jogEnabled_; + int jogSensitivity_; + bool jogReverseX_; + bool jogReverseY_; + + // Encoder positions + long encoderPos1_; + long encoderPos2_; +}; + +} // namespace IX85XYStage diff --git a/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStageProtocol.h b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStageProtocol.h new file mode 100644 index 000000000..b6e1225fb --- /dev/null +++ b/DeviceAdapters/EvidentIX85XYStage/EvidentIX85XYStageProtocol.h @@ -0,0 +1,234 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: EvidentIX85XYStageProtocol.h +// PROJECT: Micro-Manager +// SUBSYSTEM: DeviceAdapters +//----------------------------------------------------------------------------- +// DESCRIPTION: Protocol constants and helpers for Evident IX85 XY Stage +// +// COPYRIGHT: University of California, San Francisco, 2025 +// +// LICENSE: This file is distributed under the BSD license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Nico Stuurman, 2025 + +#pragma once + +#include +#include +#include + +// Protocol constants +namespace IX85XYStage { + +const char* const TERMINATOR = "\r\n"; +const char TAG_DELIMITER = ' '; +const char DATA_DELIMITER = ','; +const char POSITIVE_ACK = '+'; +const char NEGATIVE_ACK = '!'; + +// Serial port settings +const int BAUD_RATE = 115200; +const int STOP_BITS = 2; +const char* const PARITY = "Even"; + +// Command tags +const char* const CMD_LOGIN = "L"; +const char* const CMD_VERSION = "V"; +const char* const CMD_UNIT = "U"; +const char* const CMD_XY_INIT = "XYINIT"; +const char* const CMD_XY_GOTO = "XYG"; +const char* const CMD_XY_MOVE = "XYM"; +const char* const CMD_XY_STOP = "XYSTP"; +const char* const CMD_XY_POSITION = "XYP"; +const char* const CMD_XY_POSITION_NOTIFY = "NXYP"; +const char* const CMD_XY_SPEED = "XYSPD"; +const char* const CMD_XY_JOG = "XYJG"; +const char* const CMD_XY_JOG_SENSITIVITY = "XYJGS"; +const char* const CMD_XY_JOG_DIR_X = "XJGDR"; +const char* const CMD_XY_JOG_DIR_Y = "YJGDR"; +const char* const CMD_ENCODER_1 = "E1"; +const char* const CMD_ENCODER_2 = "E2"; + +// Device limits +const long XY_STAGE_MIN_POS_X = -743680; // X position range in 78.125nm units +const long XY_STAGE_MAX_POS_X = 743680; // ~58mm range in X +const long XY_STAGE_MIN_POS_Y = -500480; // Y position range in 78.125nm units +const long XY_STAGE_MAX_POS_Y = 500480; // ~39mm range in Y +const double XY_STAGE_STEP_SIZE_UM = 0.078125; // 78.125nm = 0.078125µm per step +const long XY_STAGE_POSITION_TOLERANCE = 10; // 10 steps tolerance + +// Error codes +const int ERR_OFFSET = 10200; +const int ERR_COMMAND_TIMEOUT = ERR_OFFSET + 1; +const int ERR_NEGATIVE_ACK = ERR_OFFSET + 2; +const int ERR_INVALID_RESPONSE = ERR_OFFSET + 3; +const int ERR_PORT_NOT_SET = ERR_OFFSET + 4; +const int ERR_PORT_CHANGE_FORBIDDEN = ERR_OFFSET + 5; +const int ERR_DEVICE_NOT_AVAILABLE = ERR_OFFSET + 6; +const int ERR_POSITION_UNKNOWN = ERR_OFFSET + 7; + +// Helper functions +inline std::string BuildCommand(const char* tag) +{ + return std::string(tag); +} + +inline std::string BuildCommand(const char* tag, int param) +{ + std::ostringstream cmd; + cmd << tag << TAG_DELIMITER << param; + return cmd.str(); +} + +inline std::string BuildCommand(const char* tag, long param1, long param2) +{ + std::ostringstream cmd; + cmd << tag << TAG_DELIMITER << param1 << DATA_DELIMITER << param2; + return cmd.str(); +} + +inline std::string BuildCommand(const char* tag, int param1, int param2, int param3) +{ + std::ostringstream cmd; + cmd << tag << TAG_DELIMITER << param1 << DATA_DELIMITER << param2 << DATA_DELIMITER << param3; + return cmd.str(); +} + +inline bool IsPositiveAck(const std::string& response, const char* tag) +{ + std::string expected = std::string(tag) + " +"; + return response.find(expected) == 0; +} + +inline bool IsNegativeAck(const std::string& response, const char* tag) +{ + std::string expected = std::string(tag) + " !"; + return response.find(expected) == 0; +} + +inline bool IsNotificationTag(const std::string& response) +{ + // Notification tags that can come asynchronously + // Need to distinguish notifications from command acknowledgments + + // Position notification: "NXYP x,y" vs acknowledgment: "NXYP +" or "NXYP !" + if (response.find("NXYP ") == 0) + { + size_t spacePos = response.find(' '); + if (spacePos != std::string::npos && spacePos + 1 < response.length()) + { + char nextChar = response[spacePos + 1]; + // Notification has numeric value or 'X', acknowledgment has '+' or '!' + return (nextChar != '+' && nextChar != '!'); + } + } + + // Encoder notifications (only when they contain delta values, not acknowledgments) + if (response.find("E1 ") == 0) + { + // Check if it's a notification (has numeric value) vs acknowledgment (+ or !) + size_t spacePos = response.find(' '); + if (spacePos != std::string::npos && spacePos + 1 < response.length()) + { + char nextChar = response[spacePos + 1]; + return (nextChar != '+' && nextChar != '!'); + } + } + if (response.find("E2 ") == 0) + { + size_t spacePos = response.find(' '); + if (spacePos != std::string::npos && spacePos + 1 < response.length()) + { + char nextChar = response[spacePos + 1]; + return (nextChar != '+' && nextChar != '!'); + } + } + + return false; +} + +inline std::vector ParseParameters(const std::string& response) +{ + std::vector params; + size_t tagEnd = response.find(TAG_DELIMITER); + if (tagEnd == std::string::npos) + return params; + + std::string dataStr = response.substr(tagEnd + 1); + std::istringstream iss(dataStr); + std::string param; + + while (std::getline(iss, param, DATA_DELIMITER)) + { + // Trim whitespace + size_t start = param.find_first_not_of(" \t\r\n"); + size_t end = param.find_last_not_of(" \t\r\n"); + if (start != std::string::npos && end != std::string::npos) + params.push_back(param.substr(start, end - start + 1)); + else if (start != std::string::npos) + params.push_back(param.substr(start)); + } + + return params; +} + +inline long ParseLongParameter(const std::string& param) +{ + if (param.empty() || param == "X" || param == "x" || param == "+" || param == "!") + return -1; + + try + { + return std::stol(param); + } + catch (...) + { + return -1; + } +} + +inline int ParseIntParameter(const std::string& param) +{ + if (param.empty() || param == "X" || param == "x" || param == "+" || param == "!") + return -1; + + try + { + return std::stoi(param); + } + catch (...) + { + return -1; + } +} + +inline bool IsAtTargetPosition(long currentPos, long targetPos, long tolerance) +{ + if (targetPos < 0) + return false; + + long diff = currentPos - targetPos; + if (diff < 0) + diff = -diff; + + return diff <= tolerance; +} + +inline std::string ExtractTag(const std::string& response) +{ + size_t delimPos = response.find(TAG_DELIMITER); + if (delimPos == std::string::npos) + return response; + return response.substr(0, delimPos); +} + +} // namespace IX85XYStage diff --git a/DeviceAdapters/PVCAM/StreamWriter.cpp b/DeviceAdapters/PVCAM/StreamWriter.cpp index 8c8aa3918..de9d0a7a0 100644 --- a/DeviceAdapters/PVCAM/StreamWriter.cpp +++ b/DeviceAdapters/PVCAM/StreamWriter.cpp @@ -200,6 +200,7 @@ int StreamWriter::Start() camera_->LogAdapterMessage(std::string("Started streaming to '") + path_ + "'", false); return DEVICE_OK; + } void StreamWriter::Stop() @@ -509,6 +510,7 @@ int StreamWriter::SaveSummary(const std::string& fileName) const sum << "All " << totalFrameCount << " frames saved.\n"; } else + { sum << "Saved " << totalFrameCount << " frames, " << totalFramesLost_ << " frames lost.\n" << totalSummary_; diff --git a/DeviceAdapters/configure.ac b/DeviceAdapters/configure.ac index c275cf178..da11a51a2 100644 --- a/DeviceAdapters/configure.ac +++ b/DeviceAdapters/configure.ac @@ -583,6 +583,7 @@ m4_define([device_adapter_dirs], [m4_strip([ DTOpenLayer DemoCamera Diskovery + EvidentIX85 FakeCamera FocalPoint FreeSerialPort diff --git a/micromanager.sln b/micromanager.sln index 2045a9331..20ff4e6c6 100644 --- a/micromanager.sln +++ b/micromanager.sln @@ -522,7 +522,13 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "openUC2", "DeviceAdapters\O EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PriorPureFocus", "DeviceAdapters\PriorPureFocus\PriorPureFocus.vcxproj", "{BF5D938C-60E9-400E-8BBB-D9D2C058B4FC}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CairnOptoSpinUCSF", "DeviceAdapters\CairnOptoSpinUCSF\CairnOptoSpinUCSF.vcxproj", "{ed59ab6e-b1bc-40c5-bffe-7a79c0f15834}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CairnOptoSpinUCSF", "DeviceAdapters\CairnOptoSpinUCSF\CairnOptoSpinUCSF.vcxproj", "{ED59AB6E-B1BC-40C5-BFFE-7A79C0F15834}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EvidentIX85", "DeviceAdapters\EvidentIX85\EvidentIX85.vcxproj", "{E8A5D9F1-4B2C-4E8F-9C1D-6F3A8B7E2D4C}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EvidentIX85Win", "DeviceAdapters\EvidentIX85Win\EvidentIX85Win.vcxproj", "{F2B9E7A6-3C5D-4F1E-8D2A-9B4C7E6F3A5D}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EvidentIX85XYStage", "DeviceAdapters\EvidentIX85XYStage\EvidentIX85XYStage.vcxproj", "{A7F8E9C3-5D4B-4E2F-9A1C-3B7D8E6F4A2C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1570,10 +1576,22 @@ Global {BF5D938C-60E9-400E-8BBB-D9D2C058B4FC}.Debug|x64.Build.0 = Debug|x64 {BF5D938C-60E9-400E-8BBB-D9D2C058B4FC}.Release|x64.ActiveCfg = Release|x64 {BF5D938C-60E9-400E-8BBB-D9D2C058B4FC}.Release|x64.Build.0 = Release|x64 - {ed59ab6e-b1bc-40c5-bffe-7a79c0f15834}.Debug|x64.ActiveCfg = Debug|x64 - {ed59ab6e-b1bc-40c5-bffe-7a79c0f15834}.Debug|x64.Build.0 = Debug|x64 - {ed59ab6e-b1bc-40c5-bffe-7a79c0f15834}.Release|x64.ActiveCfg = Release|x64 - {ed59ab6e-b1bc-40c5-bffe-7a79c0f15834}.Release|x64.Build.0 = Release|x64 + {ED59AB6E-B1BC-40C5-BFFE-7A79C0F15834}.Debug|x64.ActiveCfg = Debug|x64 + {ED59AB6E-B1BC-40C5-BFFE-7A79C0F15834}.Debug|x64.Build.0 = Debug|x64 + {ED59AB6E-B1BC-40C5-BFFE-7A79C0F15834}.Release|x64.ActiveCfg = Release|x64 + {ED59AB6E-B1BC-40C5-BFFE-7A79C0F15834}.Release|x64.Build.0 = Release|x64 + {E8A5D9F1-4B2C-4E8F-9C1D-6F3A8B7E2D4C}.Debug|x64.ActiveCfg = Debug|x64 + {E8A5D9F1-4B2C-4E8F-9C1D-6F3A8B7E2D4C}.Debug|x64.Build.0 = Debug|x64 + {E8A5D9F1-4B2C-4E8F-9C1D-6F3A8B7E2D4C}.Release|x64.ActiveCfg = Release|x64 + {E8A5D9F1-4B2C-4E8F-9C1D-6F3A8B7E2D4C}.Release|x64.Build.0 = Release|x64 + {F2B9E7A6-3C5D-4F1E-8D2A-9B4C7E6F3A5D}.Debug|x64.ActiveCfg = Debug|x64 + {F2B9E7A6-3C5D-4F1E-8D2A-9B4C7E6F3A5D}.Debug|x64.Build.0 = Debug|x64 + {F2B9E7A6-3C5D-4F1E-8D2A-9B4C7E6F3A5D}.Release|x64.ActiveCfg = Release|x64 + {F2B9E7A6-3C5D-4F1E-8D2A-9B4C7E6F3A5D}.Release|x64.Build.0 = Release|x64 + {A7F8E9C3-5D4B-4E2F-9A1C-3B7D8E6F4A2C}.Debug|x64.ActiveCfg = Debug|x64 + {A7F8E9C3-5D4B-4E2F-9A1C-3B7D8E6F4A2C}.Debug|x64.Build.0 = Debug|x64 + {A7F8E9C3-5D4B-4E2F-9A1C-3B7D8E6F4A2C}.Release|x64.ActiveCfg = Release|x64 + {A7F8E9C3-5D4B-4E2F-9A1C-3B7D8E6F4A2C}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE