Evident IX85 - DeviceAdapters for the XY stage (COM) and Hub (both through COM and SDK)#780
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request adds comprehensive device adapter support for the Evident IX85 microscope system, implementing three separate adapters to support different communication methods and hardware configurations:
- EvidentIX85XYStage: COM-based XY stage controller for the IX5-SSA hardware
- EvidentIX85Win: SDK-based adapter providing full microscope control (focus, nosepiece, shutters, filters, etc.)
- EvidentIX85: COM-based adapter for general microscope control
The implementation includes protocol definitions, thread-safe state management, device detection, and proper resource handling. Additionally, a HamamatsuHamU adapter reference and documentation (CLAUDE.md) are added.
Reviewed changes
Copilot reviewed 37 out of 39 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| micromanager.sln | Adds four new project entries: HamamatsuHamU, EvidentIX85, EvidentIX85Win, and EvidentIX85XYStage with build configurations |
| DeviceAdapters/configure.ac | Adds EvidentIX85 to autotools build system |
| DeviceAdapters/PVCAM/StreamWriter.cpp | Contains extraneous blank line additions |
| DeviceAdapters/EvidentIX85XYStage/* | Complete XY stage adapter implementation with protocol, model, and device classes |
| DeviceAdapters/EvidentIX85Win/* | Full SDK-based microscope adapter with hub, multiple device types, lens database, and objective setup |
| DeviceAdapters/EvidentIX85/* | COM-based microscope adapter with hub and device implementations |
| CLAUDE.md | Repository documentation for Claude AI assistance |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| // Load the DLL | ||
| dllHandle_ = LoadLibraryExA(dllFullPath.c_str(), NULL, LOAD_WITH_ALTERED_SEARCH_PATH); |
There was a problem hiding this comment.
Untrusted DLL load via LoadLibraryExA with a relative filename and LOAD_WITH_ALTERED_SEARCH_PATH enables DLL hijacking and arbitrary code execution. An attacker who can place a crafted msl_pm_ix85.dll in the working directory, application directory, or a searched path can get it loaded instead of the intended vendor DLL. To fix: resolve and validate an absolute, trusted path to the SDK DLL (e.g., program files directory), call SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) and load with LoadLibraryExW(absPath, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR), or use AddDllDirectory to a trusted directory; avoid passing just a filename and avoid LOAD_WITH_ALTERED_SEARCH_PATH. Optionally verify the DLL’s signature before use.
marktsuchida
left a comment
There was a problem hiding this comment.
Awesome! I took a quick look focusing on organization and added some comments.
| }; | ||
|
|
||
| // Complete lens database from SDK LensInfo.unit file | ||
| static const LensInfo LENS_DATABASE[] = { |
There was a problem hiding this comment.
If this gets included in more than one .cpp file, the big static array will be duplicated. Probably won't actually get duplicated if not accessed, but I think it's better to put this (and the implementations of the functions that access it) into a .cpp file.
There was a problem hiding this comment.
Sorry, it is unclear at the moment how to do this nicely. We can revisit this if important.
1b62656 to
c647311
Compare
d73384c to
8d40c26
Compare
|
@nicost Let me see if I can fix this. Ok to merge once fixed and I check the build? |
Implements device adapter for the Evident IX85 microscope system with support for: - Focus drive (Z-stage) with position and speed control - Nosepiece (objective turret) with 6 positions - Magnification changer (1x, 1.6x, 2x) - Light path selector - Condenser turret - DIA and EPI shutters - Mirror units (filter cube turrets) - Polarizer, DIC prism, EPI ND filter - Correction collar Key features: - Hub-based architecture with automatic device discovery - C++17 std::thread for monitoring thread (not MMDeviceThreadBase) - Thread-safe state management using std::mutex - Active notifications for real-time position updates - Serial communication via USB VCOM (115200 baud, even parity) Files added: - DeviceAdapters/EvidentIX85/EvidentProtocol.h - Protocol definitions - DeviceAdapters/EvidentIX85/EvidentModel.h/.cpp - State management - DeviceAdapters/EvidentIX85/EvidentHub.h/.cpp - Hub with monitoring - DeviceAdapters/EvidentIX85/EvidentIX85.h/.cpp - Device implementations - DeviceAdapters/EvidentIX85/EvidentIX85.vcxproj - VS project (C++17) - DeviceAdapters/EvidentIX85/Makefile.am - Unix build config Build system integration: - Added to DeviceAdapters/configure.ac Also updated CLAUDE.md with repository documentation. 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Fix include: change <lock_guard> to <mutex> in EvidentModel.cpp - Add missing error codes: ERR_PORT_NOT_SET and ERR_PORT_CHANGE_FORBIDDEN - Add error text messages for new error codes 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
SharedRuntime.
Magnification device is working and updating its position automatically.
- Updated Focus notification handler to compare current position with target - When positions match, set Busy state to false in the model - Applied same pattern to Nosepiece device - This ensures Busy() returns false once movement completes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Problem: During device initialization, when enabling notifications, the microscope sends immediate notification messages (e.g., NCA 1 after enabling magnification notifications). These notifications could be read by the command thread when expecting command responses, causing ERR_NEGATIVE_ACK (10102) errors. Solution: Modified GetResponse() to skip over notification messages and only return command responses. Notifications start with 'N' prefix (NFP, NCA, NOB, etc.) and are now filtered out during command-response sequences. The monitoring thread will handle these notifications asynchronously. Changes: - GetResponse() now identifies notification tags and skips them - Logs skipped notifications for debugging - Continues reading until actual command response is found - Monitoring thread code simplified for clarity This ensures clean command-response sequences during initialization even when notifications are enabled. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Problem: GetResponse() was incorrectly filtering out command acknowledgments like "NCA +" thinking they were notifications, causing timeout errors (10101) when waiting for responses. Root cause: Notification enable commands (e.g., "NCA 1") receive TWO responses: 1. "NCA +" - acknowledgment that notifications are enabled 2. "NCA 1" - immediate notification of current state The previous code skipped both, causing timeouts. Solution: Added check to distinguish between: - Acknowledgments: "NCA +", "NFP +" (should be returned) - Notifications: "NCA 1", "NFP 3110" (should be skipped) Now only skips notifications that contain data, not ack responses. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Major architectural change: Only the monitoring thread now reads from the serial port. All messages are routed through this thread to either: - Command responses -> Passed to waiting command thread via condition variable - Notifications -> Processed directly and update model state Benefits: - Eliminates all race conditions between threads - No messages are lost or skipped - All notifications are processed regardless of timing - Cleaner separation of concerns Implementation: - Added condition_variable for command-response communication - MonitorThreadFunc() is now sole serial port reader - GetResponse() waits on condition variable instead of reading - ProcessNotification() handles all notification types - IsNotificationTag() distinguishes notifications from responses This fixes the timeout issue where notifications arriving during command execution were causing state management problems. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Problem: ParseIntParameter and ParseLongParameter would crash with C++ exceptions when encountering non-numeric strings like "+", "!", or empty strings. std::stoi/std::stol throw exceptions on invalid input. Solution: Added comprehensive defensive checks: - Handle empty strings -> return -1 - Handle "X" or "x" (unknown) -> return -1 - Handle "+" or "!" (acknowledgments) -> return -1 - Wrap std::stoi/std::stol in try-catch blocks - Catch std::invalid_argument (not a number) - Catch std::out_of_range (number too large) - Return -1 for all error cases This prevents application crashes when the microscope returns unexpected response formats or when parsing acknowledgment messages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Problem: IsNotificationTag() was identifying "NCA +" as a notification
because the tag "NCA" matches CMD_MAGNIFICATION_NOTIFY. But "NCA +" is
actually a command acknowledgment (response to "NCA 1" enable command),
not a notification. This caused the monitoring thread to process it as
a notification instead of passing it to the waiting command thread,
resulting in timeout errors (10101).
Root cause: The function only checked the tag, not the message content.
Both acknowledgments ("NCA +") and notifications ("NCA 1") have the
same tag "NCA".
Solution: Changed IsNotificationTag() to:
1. Take the full message instead of just the tag
2. Check if it's a known notification tag
3. Verify it's NOT an acknowledgment (+ or !)
4. Only return true for actual data notifications
Now:
- "NCA +" -> IsNotificationTag = false -> Passed to command thread ✓
- "NCA 1" -> IsNotificationTag = true -> Processed as notification ✓
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Problem: Focus device's Busy() function would return true indefinitely after movement because the exact position comparison (pos == targetPos) would almost never succeed. Mechanical focus drives have settling tolerance, backlash, and encoder quantization that prevent landing exactly on the requested position. Solution: Added tolerance-based comparison for continuous position devices: 1. Added FOCUS_POSITION_TOLERANCE constant (10 steps = 100nm) 2. Created IsAtTargetPosition() helper function 3. Updated ProcessNotification() to use tolerance comparison 4. Added debug logging when target is reached Now the focus is considered "at position" when within ±100nm of target, which is well within mechanical precision and allows Busy state to clear properly. For discrete state devices (Nosepiece), exact equality is still used since positions are discrete and well-defined. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Problem: Notifications could arrive before the command thread set the target position and busy state, causing race conditions: Timeline of the bug: 1. Send "FG 196000" command 2. Notification "NFP 196000" arrives (but target not set yet) 3. Acknowledgment "FG +" arrives 4. Set target=196000 and busy=true 5. No more notifications → busy stays true forever The notification arrived BEFORE the target was set, so it couldn't clear the busy state. Then after setting busy=true, no more notifications arrive to clear it. Solution: Set target position and busy state BEFORE sending command: 1. Set target=196000 and busy=true 2. Send "FG 196000" command 3. Notification "NFP 196000" arrives → clears busy (matches target) 4. Acknowledgment "FG +" arrives → command succeeded 5. Return success If command fails, clear busy state before returning error. Applied to both Focus and Nosepiece devices. This allows notifications to arrive in any order relative to acknowledgments and still work correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Changed EvidentMagnification from CStateDeviceBase to CMagnifierBase for better semantic correctness as a magnification changer device. Changes: - Inherit from CMagnifierBase<EvidentMagnification> - Implemented GetMagnification() returning actual magnification value - Changed property from "State" to "Magnification" (MM::g_Keyword_Magnification) - Property values are now 1.0, 1.6, 2.0 instead of 0, 1, 2 - Added static magnifications_[3] array with actual values - Removed GetNumberOfPositions() (not needed for CMagnifierBase) - Updated OnState() to OnMagnification() - Updated notification handler to use Magnification property Benefits: - More semantically correct device type - Properties are actual magnification values, not abstract states - Better integration with Micro-Manager's magnification system - Consistent with other magnification changer implementations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Changes made to make the code build and run correctly: 1. Added custom Magnification property constant - MM::g_Keyword_Magnification doesn't exist in MMDevice API - Defined g_Keyword_Magnification = "Magnification" in EvidentIX85.cpp - Added extern declaration in EvidentHub.cpp 2. Corrected device type registration - Changed from MM::StateDevice to MM::MagnifierDevice - Properly identifies device as magnification changer 3. Fixed GetMagnification() signature - Removed 'const' qualifier (was causing issues) - GetHub() is not const, so method can't be const 4. Updated all property references - Use g_Keyword_Magnification consistently - Applied in Initialize() and notification handler These changes ensure proper compilation and correct runtime behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Added GetCoreCallback()->OnMagnifierChanged() when magnification position changes. This is a specialized callback that notifies the Micro-Manager core when the magnification changer position changes. The callback is called in addition to OnPropertyChanged to ensure: 1. Property value updates correctly 2. Core is notified of magnification system change 3. Dependent calculations (pixel size, etc.) can be updated Called in ProcessNotification() when magnification notification arrives from the microscope. Fix: OnMagnifierChanged() requires the device parameter to identify which magnifier changed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Problem: During shutdown, each command was timing out (2 seconds each), making device unload take 8+ seconds. The issue was that StopMonitoring() was called BEFORE sending shutdown commands. Timeline of the bug: 1. StopMonitoring() called → monitoring thread stopped 2. Send "NFP 0" command → waits for response 3. No monitoring thread to read response → timeout (2s) 4. Send "NOB 0" command → timeout (2s) 5. Send "NCA 0" command → timeout (2s) 6. Send "L 0" command → timeout (2s) 7. Total: 8 seconds of timeouts Root cause: GetResponse() waits on a condition variable that's signaled by the monitoring thread. With the thread stopped, responses are never read from the serial port and never delivered to GetResponse(). Solution: Reordered shutdown sequence: 1. Send all shutdown commands (disable notifications, local mode) 2. Commands complete quickly (responses read by monitoring thread) 3. THEN stop monitoring thread 4. Total: milliseconds instead of 8 seconds Shutdown is now instant instead of 8+ seconds. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Added functionality to view and set focus near limits for each objective
position on the nosepiece:
- Added nearLimits_ member variable to store 6 near limit positions (in steps)
- Query near limits from microscope on initialization using "NL?" command
- Created "Focus-Near-Limit-um" read-only property that displays the near
limit in micrometers for the currently selected objective
- Created "Set-Focus-Near-Limit" action property with three options:
- "" (empty): Default/inactive state
- "Set": Captures current focus position as near limit for active objective
- "Clear Limit": Sets near limit to maximum (10500 um) to allow moving
focus higher and setting a new limit
- Implemented QueryNearLimits() to fetch all 6 limits from microscope
- Implemented OnNearLimit() to display current objective's limit
- Implemented OnSetNearLimit() to set or clear limits via "NL p1,p2,p3,p4,p5,p6" command
The near limit property automatically updates when the objective position
changes, showing the limit specific to each objective.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
…OBSEQ Add three new Nosepiece properties for parfocal functionality: - Parfocal-Position-um (read-only, shows position for current objective) - Set-Parfocal-Position (action property with Set/Clear options) - Parfocal-Enabled (enable/disable parfocal functionality) Add Focus-Escape-Distance property (0.0-9.0 mm) using ESC2 command. Switch objective changes from OB to OBSEQ command. The SDK now automatically handles focus escape and parfocal adjustments during objective changes. Remove SafeNosepieceChange property and method (~200 lines) since this functionality is now handled by the SDK via OBSEQ. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Add three autofocus workflow modes controlled by "AF-Workflow-Mode" property: - Measure-Offset: User focuses manually, AF 1 runs, offset calculated and stored, then returns to original position - Find-Focus-With-Offset: AF 3 runs to find focus, then applies stored offset to Focus Drive and stops - Continuous-Focus: Starts AF 2 for continuous focus tracking Add "Measured-Focus-Offset-um" property to display and set the Z-offset value. Property updates automatically when offset is measured and triggers OnPropertyChanged callbacks to notify core of changes. Remove redundant "Tracking Mode" property - workflow modes now handle all autofocus mode selection. Code simplified to always use AF mode 2 as default. Fix AF mode 1 auto-stop detection to recognize success when position changes and correct offset sign calculation to properly represent correction from ZDC's focus position to user's desired focus position. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Implemented a new device adapter for one-time objective configuration: - Created lens database with 102 Evident objectives (EvidentLensDatabase.h) - New EvidentObjectiveSetup device with per-position configuration - Auto-detects installed objectives via GOB queries - Allows database override selection with filtering by magnification/immersion - Per-position Send-To-SDK buttons for individual objective configuration - Sends objective info to SDK using S_OB command - Supports clearing positions with NONE command - Dynamic property updates after sending objectives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…onEx. Replaced 48 individual position-specific handler functions with 8 parameterized handlers using CPropertyActionEx. This reduces code duplication and improves maintainability. Changes: - Replaced OnPos1-6DetectedName/Specs/DatabaseSelection/SendToSDK with parameterized OnPos* handlers - Replaced OnPos1-6Special* handlers with parameterized OnPosSpecial* handlers - Updated property creation to use CPropertyActionEx instead of switch statements - Net reduction of ~200 lines of code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…e measured first, because we now have the offset as a property with a default value of zero. It can for instance be set as a pre-initialization property.
Ensure that OnPropertyChanged is called whenever the autofocus status changes, so that the MMCore and applications are properly notified of status changes. Changes: - UpdateAFStatus(): Now calls OnPropertyChanged when status changes (triggered by NAFST notifications from microscope) - StopAF(): Calls OnPropertyChanged when AF is stopped - IsContinuousFocusLocked(): Calls OnPropertyChanged when querying updates the status All callbacks check if the status actually changed before notifying to avoid unnecessary property change events. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
ports are offered as options in the HCW.
… current directory. This should match instructions on the website about downloading and installting the SDK zip file.
…ect position after one-shot autofocus.
…mand 20 more times at 50 ms intervals.:
asynchronously. This mechanism shoudl be used from with the Notification processing code, otherwise there will either be deadlock and timeout, and/or the command responses will not be processed leading to chaose down the line. Tested this code with 528 channel switches, autofocus and z-stacks, and no issues observed,
…. Will try relative paths again later.
8d40c26 to
ee5396c
Compare
|
No description provided.