Skip to content

Evident IX85 - DeviceAdapters for the XY stage (COM) and Hub (both through COM and SDK)#780

Merged
marktsuchida merged 70 commits intomicro-manager:mainfrom
nicost:EvidentIX85
Feb 19, 2026
Merged

Evident IX85 - DeviceAdapters for the XY stage (COM) and Hub (both through COM and SDK)#780
marktsuchida merged 70 commits intomicro-manager:mainfrom
nicost:EvidentIX85

Conversation

@nicost
Copy link
Copy Markdown
Member

@nicost nicost commented Dec 1, 2025

No description provided.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

@marktsuchida marktsuchida left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! I took a quick look focusing on organization and added some comments.

Comment thread DeviceAdapters/EvidentIX85Win/fix_race_condition.patch Outdated
Comment thread DeviceAdapters/EvidentIX85Win/mdk_if.h Outdated
Comment thread DeviceAdapters/EvidentIX85Win/.claude/settings.local.json Outdated
Comment thread DeviceAdapters/EvidentIX85Win/EvidentHubWin.cpp.backup Outdated
Comment thread DeviceAdapters/PVCAM/StreamWriter.cpp Outdated
Comment thread DeviceAdapters/EvidentIX85Win/Makefile.am Outdated
Comment thread DeviceAdapters/EvidentIX85Win/EvidentHubWin.cpp Outdated
Comment thread DeviceAdapters/EvidentIX85Win/EvidentObjectiveSetup.cpp Outdated
Comment thread DeviceAdapters/EvidentIX85Win/EvidentHubWin.h
};

// Complete lens database from SDK LensInfo.unit file
static const LensInfo LENS_DATABASE[] = {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, it is unclear at the moment how to do this nicely. We can revisit this if important.

@marktsuchida marktsuchida changed the title Evident IX85 - DeviceAdapters for the XY stage(COM) and Hub (both through COM and SDK( Evident IX85 - DeviceAdapters for the XY stage (COM) and Hub (both through COM and SDK) Dec 3, 2025
@nicost
Copy link
Copy Markdown
Member Author

nicost commented Feb 18, 2026

Commits to be cleaned up:

67a26d5
.claude/settings.local.json
DeviceAdapters/EvidentIX85/.claude/settings.local.json
99d6cab

a7093f9
CLAUDE.md
ff15b3b

d5e956e
DeviceAdapters/EvidentIX85/.claude/settings.local.json
d3a413b

a4e1997
.claude
CLAUDE.md
746fe17

462e3c3
DeviceAdapters/PVCAM/StreamWriter.cpp
95427b1

@nicost nicost force-pushed the EvidentIX85 branch 4 times, most recently from d73384c to 8d40c26 Compare February 18, 2026 22:32
@marktsuchida
Copy link
Copy Markdown
Member

@nicost Let me see if I can fix this. Ok to merge once fixed and I check the build?

nicost and others added 17 commits February 18, 2026 18:48
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>
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>
nicost and others added 25 commits February 18, 2026 18:48
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.
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,
@marktsuchida
Copy link
Copy Markdown
Member

marktsuchida commented Feb 19, 2026

  • Take history from before today's force pushes (016d67e)
  • Cherry-pick today's commit "EvidentIX85 Addresses most PR comments."
  • Remove micromanager.sln changes from branch (keeping base)
  • Remove PVCAM change from branch (keeping base)
  • Rebase onto main (no conflicts)
  • New commit: Add EvidentIX85, EvidentIX85XYStage, EvidentIX85Win to micromanager.sln
  • Builds on Windows
  • New commit: Add to DeviceAdapters/Makefile.am
  • Builds on macOS

@marktsuchida marktsuchida merged commit 51507e4 into micro-manager:main Feb 19, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants