diff --git a/DeviceAdapters/PicardUSB/PicardUSB.cpp b/DeviceAdapters/PicardUSB/PicardUSB.cpp new file mode 100644 index 000000000..2c6eecab9 --- /dev/null +++ b/DeviceAdapters/PicardUSB/PicardUSB.cpp @@ -0,0 +1,383 @@ +// Picard Industries USB devices (PiUsbSDK) +// +// AUTHOR: Mark A. Tsuchida +// +// COPYRIGHT: 2026 Board of Regents of the University of Wisconsin System +// +// 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. + +#include "PicardUSB.h" + +#include "DeviceBase.h" +#include "ModuleInterface.h" + +#include "PiUsb.h" + +#include +#include +#include +#include +#include + +const char* g_SideFlipperName = "SideFlipper"; +const char* g_GradientWheelName = "GradientWheel"; +const char* g_PropSerialNumber = "Serial Number"; +const char* g_PropPosition = "Position"; +const char* g_PropState = "State"; +const char* g_PropOpenPosition = "Open Position"; +const char* g_OpenPositionExtended = "Extended"; +const char* g_OpenPositionRetracted = "Retracted"; + +// SDK error n (1..9) maps to ERR_PI_BASE + n so each gets its own SetErrorText. +constexpr int ERR_PI_BASE = 100; // 101..109 = PiUsb errors 1..9 +constexpr int ERR_CONNECT_FAILED = 111; // piConnect returned NULL without error code +constexpr int ERR_SERIAL_NOT_SET = 112; // Serial Number property still 0 + +// Tweakables: +constexpr long flipTimeoutMs = 1000; +constexpr long gradientWheelMoveTimeoutMs = 5000; +constexpr int gradientWheelPositionToleranceCounts = 0; + +MODULE_API void InitializeModuleData() +{ + RegisterDevice(g_SideFlipperName, MM::ShutterDevice, "Picard USB Side Flipper"); + RegisterDevice(g_GradientWheelName, MM::GenericDevice, "Picard USB Gradient Wheel"); +} + +MODULE_API MM::Device* CreateDevice(const char* deviceName) +{ + if (deviceName == nullptr) + return 0; + if (std::strcmp(deviceName, g_SideFlipperName) == 0) + return new SideFlipper(); + if (std::strcmp(deviceName, g_GradientWheelName) == 0) + return new GradientWheel(); + return nullptr; +} + +MODULE_API void DeleteDevice(MM::Device* pDevice) +{ + delete pDevice; +} + +namespace { + +typedef int(__stdcall* FindFunc)(int*, int*, int); + +std::vector FindSerials(FindFunc find) +{ + int count = 0; + int serials[256]; + int err = find(&count, serials, 256); + if (err != PI_NO_ERROR) + return std::vector(); + return std::vector(serials, serials + count); +} + +int TranslatePiError(int piErrNo) +{ + if (piErrNo == PI_NO_ERROR) + return DEVICE_OK; + return ERR_PI_BASE + piErrNo; +} + +std::vector> PiErrorTexts() +{ + return { + { ERR_PI_BASE + 1, "PiUsb: device not found or disconnected" }, + { ERR_PI_BASE + 2, "PiUsb: device handle does not exist" }, + { ERR_PI_BASE + 3, "PiUsb: cannot create device object" }, + { ERR_PI_BASE + 4, "PiUsb: invalid device handle" }, + { ERR_PI_BASE + 5, "PiUsb: read timeout (device may be disconnected)" }, + { ERR_PI_BASE + 6, "PiUsb: read thread abandoned; reconnect the device" }, + { ERR_PI_BASE + 7, "PiUsb: read failed; reconnect the device" }, + { ERR_PI_BASE + 8, "PiUsb: invalid parameter" }, + { ERR_PI_BASE + 9, "PiUsb: write failed; reconnect the device" }, + { ERR_CONNECT_FAILED, + "Could not connect to the device with the given serial number; " + "check that it is connected and not in use" }, + { ERR_SERIAL_NOT_SET, + "Serial Number must be set before initialization" }, + }; +} + +} // namespace + +SideFlipper::SideFlipper() +{ + InitializeDefaultErrorMessages(); + for (const auto& e : PiErrorTexts()) + SetErrorText(e.first, e.second.c_str()); + + CreateIntegerProperty(g_PropSerialNumber, 0, false, + new CPropertyAction(this, &SideFlipper::OnSerialNumber), true); + std::vector serials = FindSerials(piFindFlippers); + for (int s : serials) + AddAllowedValue(g_PropSerialNumber, CDeviceUtils::ConvertToString(s)); + + CreateStringProperty(g_PropOpenPosition, g_OpenPositionExtended, false, + new CPropertyAction(this, &SideFlipper::OnOpenPosition), true); + AddAllowedValue(g_PropOpenPosition, g_OpenPositionExtended); + AddAllowedValue(g_PropOpenPosition, g_OpenPositionRetracted); +} + +SideFlipper::~SideFlipper() +{ + Shutdown(); +} + +int SideFlipper::Initialize() +{ + if (handle_) + return DEVICE_OK; + + if (serial_ <= 0) + return ERR_SERIAL_NOT_SET; + + int e = 0; + handle_ = piConnectFlipper(&e, static_cast(serial_)); + if (!handle_) + return e ? TranslatePiError(e) : ERR_CONNECT_FAILED; + + int state = PI_FLIPPER_RETRACTED; + int err = piGetFlipperState(&state, handle_); + if (err != PI_NO_ERROR) + { + piDisconnectFlipper(handle_); + handle_ = nullptr; + return TranslatePiError(err); + } + commandedState_ = state; + + CreateIntegerProperty(g_PropState, 0, false, + new CPropertyAction(this, &SideFlipper::OnState)); + AddAllowedValue(g_PropState, "0"); + AddAllowedValue(g_PropState, "1"); + + return DEVICE_OK; +} + +int SideFlipper::Shutdown() +{ + if (handle_) + { + piDisconnectFlipper(handle_); + handle_ = nullptr; + } + return DEVICE_OK; +} + +void SideFlipper::GetName(char* name) const +{ + CDeviceUtils::CopyLimitedString(name, g_SideFlipperName); +} + +bool SideFlipper::Busy() +{ + if (!handle_) + return false; + // Bound by a settle timeout so a physically blocked flipper cannot hang MM. + if ((GetCurrentMMTime() - lastCommandTime_).getMsec() > flipTimeoutMs) + return false; + // TODO: We probably want to add an extra wait after state changes + int state; + if (piGetFlipperState(&state, handle_) != PI_NO_ERROR) + return false; + return state != commandedState_; +} + +int SideFlipper::SetOpen(bool open) +{ + int state = (open == openIsExtended_) ? PI_FLIPPER_EXTENDED : PI_FLIPPER_RETRACTED; + int err = piSetFlipperState(state, handle_); + if (err != PI_NO_ERROR) + return TranslatePiError(err); + commandedState_ = state; + lastCommandTime_ = GetCurrentMMTime(); + return DEVICE_OK; +} + +int SideFlipper::GetOpen(bool& open) +{ + int state; + int err = piGetFlipperState(&state, handle_); + if (err != PI_NO_ERROR) + return TranslatePiError(err); + open = ((state == PI_FLIPPER_EXTENDED) == openIsExtended_); + return DEVICE_OK; +} + +int SideFlipper::OnSerialNumber(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(serial_); + } + else if (eAct == MM::AfterSet) + { + pProp->Get(serial_); + } + return DEVICE_OK; +} + +int SideFlipper::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + bool open; + int err = GetOpen(open); + if (err != DEVICE_OK) + return err; + pProp->Set(open ? 1L : 0L); + } + else if (eAct == MM::AfterSet) + { + long v; + pProp->Get(v); + return SetOpen(v != 0); + } + return DEVICE_OK; +} + +int SideFlipper::OnOpenPosition(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(openIsExtended_ ? g_OpenPositionExtended : g_OpenPositionRetracted); + } + else if (eAct == MM::AfterSet) + { + std::string v; + pProp->Get(v); + openIsExtended_ = (v == g_OpenPositionExtended); + } + return DEVICE_OK; +} + +GradientWheel::GradientWheel() +{ + InitializeDefaultErrorMessages(); + for (const auto& e : PiErrorTexts()) + SetErrorText(e.first, e.second.c_str()); + + CreateIntegerProperty(g_PropSerialNumber, 0, false, + new CPropertyAction(this, &GradientWheel::OnSerialNumber), true); + std::vector serials = FindSerials(piFindGWheels); + for (int s : serials) + { + AddAllowedValue(g_PropSerialNumber, CDeviceUtils::ConvertToString(s)); + } +} + +GradientWheel::~GradientWheel() +{ + Shutdown(); +} + +int GradientWheel::Initialize() +{ + if (!handle_) + return DEVICE_OK; + + if (serial_ <= 0) + return ERR_SERIAL_NOT_SET; + + int e = 0; + handle_ = piConnectGWheel(&e, static_cast(serial_)); + if (!handle_) + return e ? TranslatePiError(e) : ERR_CONNECT_FAILED; + + int pos = 1; + int err = piGetGWheelPosition(&pos, handle_); + if (err != PI_NO_ERROR) + { + piDisconnectGWheel(handle_); + handle_ = nullptr; + return TranslatePiError(err); + } + if (pos < 1) + pos = 1; + else if (pos > 1023) + pos = 1023; + targetPosition_ = pos; + + CreateIntegerProperty(g_PropPosition, targetPosition_, false, + new CPropertyAction(this, &GradientWheel::OnPosition)); + SetPropertyLimits(g_PropPosition, 1, 1023); + + return DEVICE_OK; +} + +int GradientWheel::Shutdown() +{ + if (handle_) + { + piDisconnectGWheel(handle_); + handle_ = nullptr; + } + return DEVICE_OK; +} + +void GradientWheel::GetName(char* name) const +{ + CDeviceUtils::CopyLimitedString(name, g_GradientWheelName); +} + +bool GradientWheel::Busy() +{ + if (!handle_) + return false; + // No moving-status API; compare readback to target, bounded by a timeout. + if ((GetCurrentMMTime() - moveStartTime_).getMsec() > gradientWheelMoveTimeoutMs) + return false; + // TODO We might want to add an extra wait after within tolerance + int pos; + if (piGetGWheelPosition(&pos, handle_) != PI_NO_ERROR) + return false; + return std::abs(pos - targetPosition_) > gradientWheelPositionToleranceCounts; +} + +int GradientWheel::OnSerialNumber(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(serial_); + } + else if (eAct == MM::AfterSet) + { + pProp->Get(serial_); + } + return DEVICE_OK; +} + +int GradientWheel::OnPosition(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + int pos; + int err = piGetGWheelPosition(&pos, handle_); + if (err != PI_NO_ERROR) + return TranslatePiError(err); + pProp->Set(static_cast(pos)); + } + else if (eAct == MM::AfterSet) + { + long pos; + pProp->Get(pos); + int err = piSetGWheelPosition(static_cast(pos), handle_); + if (err != PI_NO_ERROR) + return TranslatePiError(err); + targetPosition_ = static_cast(pos); + moveStartTime_ = GetCurrentMMTime(); + } + return DEVICE_OK; +} diff --git a/DeviceAdapters/PicardUSB/PicardUSB.h b/DeviceAdapters/PicardUSB/PicardUSB.h new file mode 100644 index 000000000..1d9b74bfd --- /dev/null +++ b/DeviceAdapters/PicardUSB/PicardUSB.h @@ -0,0 +1,75 @@ +// Picard Industries USB devices (PiUsbSDK) +// +// AUTHOR: Mark A. Tsuchida +// +// COPYRIGHT: 2026 Board of Regents of the University of Wisconsin System +// +// 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. + +#pragma once + +#include "DeviceBase.h" + +#include "PiUsb.h" + +class SideFlipper : public CShutterBase +{ +public: + SideFlipper(); + ~SideFlipper() override; + + // MMDevice API + int Initialize() override; + int Shutdown() override; + void GetName(char* name) const override; + bool Busy() override; + + // Shutter API + int SetOpen(bool open = true) override; + int GetOpen(bool& open) override; + int Fire(double /*deltaT*/) override { return DEVICE_UNSUPPORTED_COMMAND; } + +private: + // Action handlers + int OnSerialNumber(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnOpenPosition(MM::PropertyBase* pProp, MM::ActionType eAct); + + PIHANDLE handle_{}; + long serial_{}; + int commandedState_ = PI_FLIPPER_RETRACTED; + bool openIsExtended_ = true; + MM::MMTime lastCommandTime_; +}; + +class GradientWheel : public CGenericBase +{ +public: + GradientWheel(); + ~GradientWheel() override; + + // MMDevice API + int Initialize() override; + int Shutdown() override; + void GetName(char* name) const override; + bool Busy() override; + +private: + // Action handlers + int OnSerialNumber(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnPosition(MM::PropertyBase* pProp, MM::ActionType eAct); + + PIHANDLE handle_{}; + long serial_{}; + int targetPosition_ = 1; + MM::MMTime moveStartTime_; +}; diff --git a/DeviceAdapters/PicardUSB/PicardUSB.vcxproj b/DeviceAdapters/PicardUSB/PicardUSB.vcxproj new file mode 100644 index 000000000..36f0ada86 --- /dev/null +++ b/DeviceAdapters/PicardUSB/PicardUSB.vcxproj @@ -0,0 +1,96 @@ + + + + + Debug + x64 + + + Release + x64 + + + + + + + + + + + {b8c95f39-54bf-40a9-807b-598df2821d55} + + + + 17.0 + Win32Proj + {9c82367b-6fe2-4173-ab4d-41f17738cc7d} + PicardUSB + 10.0 + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + true + _DEBUG;PICARDUSB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + $(MM_3RDPARTYPUBLIC)\Picard\PiUsbSDK-2.3\include;%(AdditionalIncludeDirectories) + + + Windows + true + false + $(MM_3RDPARTYPUBLIC)\Picard\PiUsbSDK-2.3\lib\$(Platform);%(AdditionalLibraryDirectories) + PiUsb.lib;%(AdditionalDependencies) + + + + + true + true + true + NDEBUG;PICARDUSB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + $(MM_3RDPARTYPUBLIC)\Picard\PiUsbSDK-2.3\include;%(AdditionalIncludeDirectories) + + + Windows + true + false + $(MM_3RDPARTYPUBLIC)\Picard\PiUsbSDK-2.3\lib\$(Platform);%(AdditionalLibraryDirectories) + PiUsb.lib;%(AdditionalDependencies) + + + + + + \ No newline at end of file diff --git a/DeviceAdapters/PicardUSB/PicardUSB.vcxproj.filters b/DeviceAdapters/PicardUSB/PicardUSB.vcxproj.filters new file mode 100644 index 000000000..510805eb5 --- /dev/null +++ b/DeviceAdapters/PicardUSB/PicardUSB.vcxproj.filters @@ -0,0 +1,27 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + + + Source Files + + + \ No newline at end of file diff --git a/DeviceAdapters/PicardUSB/build.xml b/DeviceAdapters/PicardUSB/build.xml new file mode 100644 index 000000000..772e26b00 --- /dev/null +++ b/DeviceAdapters/PicardUSB/build.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/micromanager.sln b/micromanager.sln index d04cd3d15..e595e5c37 100644 --- a/micromanager.sln +++ b/micromanager.sln @@ -546,6 +546,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "3Z_Optics", "DeviceAdapters EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NikonAZ100SDK", "SecretDeviceAdapters\NikonAZ100SDK\NikonAZ100SDK.vcxproj", "{F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PicardUSB", "DeviceAdapters\PicardUSB\PicardUSB.vcxproj", "{9C82367B-6FE2-4173-AB4D-41F17738CC7D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -1640,6 +1642,10 @@ Global {F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}.Debug|x64.Build.0 = Debug|x64 {F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}.Release|x64.ActiveCfg = Release|x64 {F3A2D5B7-8C91-4E6A-B4D0-7E2F1A9C3D5E}.Release|x64.Build.0 = Release|x64 + {9C82367B-6FE2-4173-AB4D-41F17738CC7D}.Debug|x64.ActiveCfg = Debug|x64 + {9C82367B-6FE2-4173-AB4D-41F17738CC7D}.Debug|x64.Build.0 = Debug|x64 + {9C82367B-6FE2-4173-AB4D-41F17738CC7D}.Release|x64.ActiveCfg = Release|x64 + {9C82367B-6FE2-4173-AB4D-41F17738CC7D}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE