On the original Nikon Ti (NikonTI), the software view of whether the PFS is on drifts out of sync with the hardware. isContinuousFocusEnabled() seems to reflect reality only right after the config loads and then freezes, so it misses both hardware PFS-button presses and software changes. Separately, enableContinuousFocus(true/false) only reaches the hardware the first time per session; later calls are dropped. The only in-session value that stays truthful is the AutoFocus device's Status property, and only when the device object is reconstructed. This looks like the same missing-callback family as #45 (no OnStagePositionChanged) and #826 (XY Busy returns too soon), but for the PFS enabled/status parameter.
Symptom 1: isContinuousFocusEnabled() goes stale
- Load the config with the PFS off.
isContinuousFocusEnabled() -> false (correct)
- Press the PFS button on the microscope body to lock focus.
- Call
isContinuousFocusEnabled() again -> still false (incorrect)
The same staleness applies to the AutoFocus State property. It never updates in-session, even across an unload/reload of the AutoFocus device, whereas the Status property does update after a reload (see workaround). So the boolean enabled flag and the Status string disagree about the same hardware.
Symptom 2: enableContinuousFocus() only lands once per session
- From PFS off,
enableContinuousFocus(true) -> PFS engages (correct)
enableContinuousFocus(false) -> PFS disengages (correct)
enableContinuousFocus(true) again -> PFS stays off (incorrect)
Only the first enable and the first disable of a session seem to reach the hardware. Behaves as if the adapter compares the request against its cached enabled flag and then skips the write when they match; since that cached flag is the stale value from Symptom 1, the comparison is against a value that no longer reflects the hardware.
Expected: every enableContinuousFocus() call is honoured, or at least re-checked against the live state rather than a stale cache.
Related: Z moves stall ~10 s when the adapter thinks the PFS is engaged
With the PFS believed to be on, TIZDrive::SetPosition turns the PFS off and then waits ~10 s for a confirmation that never arrives, so every Z-carrying move costs ~10 s. Mentioning it here because a fix to the notification path would likely address all three.
Workarounds
Every symptom points to the PFS enabled/status value being served from a cache that is only updated by a device-change notification, and that notification not being delivered in-process for this param (similar to #45 and #826), reconstructing the device object reads the value fresh, which is why a reload refreshes Status.
Was able to confirm this from the adapter side: With a small binary patch to the adapter .dll that makes the enabled read return the live PFS focus status instead of the cached flag, both symptoms go away: isContinuousFocusEnabled() tracks the hardware, repeated enableContinuousFocus() calls all take effect, as the write-skip is now comparing against a live value.
Without patching the .dll, unloading + and loading the AutoFocus device and then read its Status (Locked in focus / Focusing = engaged). takes around ~60 ms. The stale isContinuousFocusEnabled() / State do not recover even after the reload however, so one solution is to ignore them and rely on Status. Enable/disable latch (Symptom 2) cannot be solved like this however.
Potential solution
Environment
- Microscope: Nikon Ti-E with PFS,
NikonTI device adapter.
- MMCore version 12.2.2, Device API version 75, Module API version 10. Driven via
pymmcore 12.2.2.75.0 and pymmcore-plus 0.18.1; reproduces from plain MMCore.
- Adapter:
mmgr_dal_NikonTI.dll from the Micro-Manager 2.0 api75 nightly, dated 2026-03-22 (seems there were no changes to PFS since then)
Happy to run tests on this setup!
On the original Nikon Ti (
NikonTI), the software view of whether the PFS is on drifts out of sync with the hardware.isContinuousFocusEnabled()seems to reflect reality only right after the config loads and then freezes, so it misses both hardware PFS-button presses and software changes. Separately,enableContinuousFocus(true/false)only reaches the hardware the first time per session; later calls are dropped. The only in-session value that stays truthful is the AutoFocus device'sStatusproperty, and only when the device object is reconstructed. This looks like the same missing-callback family as #45 (noOnStagePositionChanged) and #826 (XYBusyreturns too soon), but for the PFS enabled/status parameter.Symptom 1:
isContinuousFocusEnabled()goes staleisContinuousFocusEnabled()->false(correct)isContinuousFocusEnabled()again -> stillfalse(incorrect)The same staleness applies to the AutoFocus
Stateproperty. It never updates in-session, even across an unload/reload of the AutoFocus device, whereas theStatusproperty does update after a reload (see workaround). So the boolean enabled flag and theStatusstring disagree about the same hardware.Symptom 2:
enableContinuousFocus()only lands once per sessionenableContinuousFocus(true)-> PFS engages (correct)enableContinuousFocus(false)-> PFS disengages (correct)enableContinuousFocus(true)again -> PFS stays off (incorrect)Only the first enable and the first disable of a session seem to reach the hardware. Behaves as if the adapter compares the request against its cached enabled flag and then skips the write when they match; since that cached flag is the stale value from Symptom 1, the comparison is against a value that no longer reflects the hardware.
Expected: every
enableContinuousFocus()call is honoured, or at least re-checked against the live state rather than a stale cache.Related: Z moves stall ~10 s when the adapter thinks the PFS is engaged
With the PFS believed to be on,
TIZDrive::SetPositionturns the PFS off and then waits ~10 s for a confirmation that never arrives, so every Z-carrying move costs ~10 s. Mentioning it here because a fix to the notification path would likely address all three.Workarounds
Every symptom points to the PFS enabled/status value being served from a cache that is only updated by a device-change notification, and that notification not being delivered in-process for this param (similar to #45 and #826), reconstructing the device object reads the value fresh, which is why a reload refreshes
Status.Was able to confirm this from the adapter side: With a small binary patch to the adapter .dll that makes the enabled read return the live PFS focus status instead of the cached flag, both symptoms go away:
isContinuousFocusEnabled()tracks the hardware, repeatedenableContinuousFocus()calls all take effect, as the write-skip is now comparing against a live value.Without patching the .dll, unloading + and loading the AutoFocus device and then read its
Status(Locked in focus/Focusing= engaged). takes around ~60 ms. The staleisContinuousFocusEnabled()/Statedo not recover even after the reload however, so one solution is to ignore them and rely onStatus. Enable/disable latch (Symptom 2) cannot be solved like this however.Potential solution
enableContinuousFocus) rather than trusting a cached flag, so a front-panel change and repeated software toggles are both seen; and/orEnvironment
NikonTIdevice adapter.pymmcore12.2.2.75.0 andpymmcore-plus0.18.1; reproduces from plain MMCore.mmgr_dal_NikonTI.dllfrom the Micro-Manager 2.0api75nightly, dated 2026-03-22 (seems there were no changes to PFS since then)Happy to run tests on this setup!