diff --git a/DeviceAdapters/HamiltonRNO/HamiltonMVP.h b/DeviceAdapters/HamiltonRNO/HamiltonMVP.h new file mode 100755 index 000000000..72727d41e --- /dev/null +++ b/DeviceAdapters/HamiltonRNO/HamiltonMVP.h @@ -0,0 +1,166 @@ +/* + * Micro-Manager device adapter for Hamilton Modular Valve Positioner + * + * Author: Mark A. Tsuchida + * + * Copyright (C) 2018 Applied Materials, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + + +enum MVPValveType { + ValveTypeUnknown = 0, + ValveTypeMin = 2, + ValveTypeMax = 7, + + // The following match the values returned by the device + ValveType8Ports = 2, + ValveType6Ports = 3, + ValveType3Ports = 4, + ValveType2Ports180DegreesApart = 5, + ValveType2Ports90DegreesApart = 6, + ValveType4Ports = 7, +}; + + +inline int GetValveNumberOfPositions(MVPValveType vt) +{ + switch (vt) + { + case ValveType8Ports: + return 8; + case ValveType6Ports: + return 6; + case ValveType3Ports: + return 3; + case ValveType2Ports180DegreesApart: + case ValveType2Ports90DegreesApart: + return 2; + case ValveType4Ports: + return 4; + default: + return 0; + } +} + + +inline std::string GetValveTypeName(MVPValveType vt) +{ + switch (vt) + { + case ValveType8Ports: + return "8 ports"; + case ValveType6Ports: + return "6 ports"; + case ValveType3Ports: + return "3 ports"; + case ValveType2Ports180DegreesApart: + return "2 ports 180 degrees apart"; + case ValveType2Ports90DegreesApart: + return "2 ports 90 degrees apart"; + case ValveType4Ports: + return "4 ports"; + default: + return "Unknown"; + } +} + + +inline MVPValveType GetValveTypeFromString(const std::string& s) +{ + if (s == "8 ports") return ValveType8Ports; + if (s == "6 ports") return ValveType6Ports; + if (s == "3 ports") return ValveType3Ports; + if (s == "2 ports 180 degrees apart") return ValveType2Ports180DegreesApart; + if (s == "2 ports 90 degrees apart") return ValveType2Ports90DegreesApart; + if (s == "4 ports") return ValveType4Ports; + return ValveTypeUnknown; +} + + +inline std::vector GetAllValveTypeNames() +{ + std::vector ret; + for (int t = ValveTypeMin; t < ValveTypeMax; ++t) + { + ret.push_back(GetValveTypeName(MVPValveType(t))); + } + return ret; +} + + +inline int GetValveRotationAngle_Equal(int nPositions, bool ccw, int startPos, int destPos) +{ + int increment = 360 / nPositions; + int startAngle = increment * startPos; + int destAngle = increment * destPos; + int deltaAngle = destAngle - startAngle; + if (ccw) + deltaAngle = -deltaAngle; + if (deltaAngle >= 0) + return deltaAngle; + return 360 + deltaAngle; +} + + +inline int GetValveRotationAngle(MVPValveType vt, bool ccw, int startPos, int destPos) +{ + switch (vt) + { + case ValveType8Ports: + return GetValveRotationAngle_Equal(8, ccw, startPos, destPos); + case ValveType6Ports: + return GetValveRotationAngle_Equal(6, ccw, startPos, destPos); + case ValveType3Ports: + { + const int angle[3][3] = { + { 0, 270, 180, }, + { 90, 0, 270 }, + { 180, 90, 0, }, + }; + return ccw ? angle[startPos][destPos] : angle[destPos][startPos]; + } + case ValveType2Ports180DegreesApart: + return GetValveRotationAngle_Equal(2, ccw, startPos, destPos); + case ValveType2Ports90DegreesApart: + { + const int angle[2][2] = { + { 0, 270, }, + { 90, 0, }, + }; + return ccw ? angle[startPos][destPos] : angle[destPos][startPos]; + } + case ValveType4Ports: + return GetValveRotationAngle_Equal(4, ccw, startPos, destPos); + default: + return 0; + } +} diff --git a/DeviceAdapters/HamiltonRNO/HamiltonPSD.h b/DeviceAdapters/HamiltonRNO/HamiltonPSD.h new file mode 100755 index 000000000..9af48dd6a --- /dev/null +++ b/DeviceAdapters/HamiltonRNO/HamiltonPSD.h @@ -0,0 +1,143 @@ +/* + * Micro-Manager device adapter for Hamilton devices that use the RNO protocol + * + * Author: Mark A. Tsuchida for the original MVP code + * Egor Zindy for the PSD additions + * + * Copyright (C) 2018 Applied Materials, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#define RESOLUTION_HALF 0 +#define RESOLUTION_FULL 1 + +enum DeviceType { + DeviceTypeUnknown = 0, + DeviceTypeMVP, + DeviceTypePSD2, + DeviceTypePSD3, //FIXME No firmware string + DeviceTypePSD4, //FIXME No firmware string + DeviceTypePSD6, //FIXME No firmware string + DeviceTypePSD8, //FIXME No firmware string + DeviceTypeML500A, + DeviceTypeML500B, + DeviceTypeML500C, + DeviceTypeML600, + DeviceTypeML700, //FIXME No firmware string + DeviceTypeML900, +}; + +inline DeviceType GetDeviceTypeFromFirmware(std::string firmware) +{ + if (firmware.length() < 4) + return DeviceTypeUnknown; + + // What I found: + // AV07 = ML900, original + // AV08 = ML900, PSD/2 valve + // AV09 = ML900, ML900 valve + // BV01 = ML500A, original + // BV02 = ML500A, current + // CV01 = ML500B and ML500C + // DV01 = ML500C, OEM + // NV01 = ML600, OEM + // OM01 = PSD/2 + // MV = MVP + + if (firmware.rfind("MV",0) == 0) + return DeviceTypeMVP; + + firmware = firmware.substr(0,4); + + if (firmware == "BV01" || firmware == "BV02") + return DeviceTypeML500A; + if (firmware == "BV01" || firmware == "BV02") + return DeviceTypeML500A; + else if (firmware == "CV01") + return DeviceTypeML500B; + else if (firmware == "DV01") + return DeviceTypeML500C; + else if (firmware == "NV01") + return DeviceTypeML600; + else if (firmware == "OM01") + return DeviceTypePSD2; + else + return DeviceTypeUnknown; +} + +inline std::string GetDeviceTypeName(DeviceType dt) +{ + switch (dt) + { + case DeviceTypeMVP: + return "Modular Valve Positioner"; + case DeviceTypePSD2: + return "PSD/2 Full-Height Syringe Pump"; + case DeviceTypePSD3: + return "PSD/3 Half-Height Syringe Pump"; + case DeviceTypePSD4: + return "PSD/4 Half-Height Syringe Pump"; + case DeviceTypePSD6: + return "PSD/6 Full-Height Syringe Pump"; + case DeviceTypePSD8: + return "PSD/8 Full-Height Syringe Pump"; + case DeviceTypeML500A: + return "Microlab 500A"; + case DeviceTypeML500B: + return "Microlab 500B"; + case DeviceTypeML500C: + return "Microlab 500C"; + case DeviceTypeML600: + return "Microlab 600"; + case DeviceTypeML700: + return "Microlab 700"; + default: + return "Unknown Device Device"; + } +} + +inline long GetSyringeThrowSteps(DeviceType dt, int resolution) +{ + switch (dt) + { + case DeviceTypePSD3: //FIXME make sure we have the right values here + return (resolution == RESOLUTION_HALF)?30000:30000; + case DeviceTypePSD4: case DeviceTypePSD8: + return (resolution == RESOLUTION_HALF)?3000:24000; + case DeviceTypePSD6: + return (resolution == RESOLUTION_HALF)?6000:48000; + case DeviceTypeML600: //FIXME make sure we have the right values here + return 52800; + default: + // PDS2, FIXME Check if not valid for ML500(A,B,C) + return (resolution == RESOLUTION_HALF)?1000:2000; + } +} diff --git a/DeviceAdapters/HamiltonRNO/HamiltonRNO.cpp b/DeviceAdapters/HamiltonRNO/HamiltonRNO.cpp new file mode 100755 index 000000000..bf2319c55 --- /dev/null +++ b/DeviceAdapters/HamiltonRNO/HamiltonRNO.cpp @@ -0,0 +1,1557 @@ +/* + * Micro-Manager device adapter for Hamilton devices that use the RNO protocol + * + * Author: Mark A. Tsuchida for the original MVP code + * Egor Zindy for the PSD additions + * + * Copyright (C) 2018 Applied Materials, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "RNOCommands.h" + +#include "DeviceBase.h" +#include "DeviceThreads.h" +#include "ModuleInterface.h" + +#include +#include + + +////////////////////////////////////////////////////////////////////////////// +// Error codes +// +#define ERR_UNKNOWN_MODE 102 +#define ERR_UNKNOWN_POSITION 103 +#define ERR_IN_SEQUENCE 104 +#define ERR_SEQUENCE_INACTIVE 105 +#define ERR_STAGE_MOVING 106 +#define HUB_NOT_AVAILABLE 107 + +const char* const DEVICE_NAME_HUB = "HamiltonChain"; +const char* const DEVICE_NAME_MVP_PREFIX = "HamiltonMVP-"; +const char* const DEVICE_NAME_PSD_PREFIX = "HamiltonPSD-"; +const char* const DEVICE_NAME_MVP_FULLNAME = "Modular Valve Positioner"; +const char* const DEVICE_NAME_PSD_FULLNAME = "Precision Syringe Drive"; + +const char* const g_Keyword_BackOffSteps = "BackOffSteps"; +const char* const g_Keyword_ReturnSteps = "ReturnSteps"; +const char* const g_Keyword_FullStrokeSteps = "FullStrokeSteps"; +const char* const g_Keyword_HalfResolution = "Half Resolution"; +const char* const g_Keyword_FullResolution = "Full Resolution"; +const char* const g_Keyword_FullResolutionDisabled = "Full Res No Overload Detection"; + +enum { + ERR_UNKNOWN_VALVE_TYPE = 21001, + ERR_INITIALIZATION_TIMED_OUT, +}; + + +// Treat a chain of MVPs and PSDs on the same serial port as a "hub" +class MVPChain : public HubBase +{ + std::string port_; + char maxAddr_; + +public: + MVPChain(); + virtual ~MVPChain(); + + virtual void GetName(char* name) const; + virtual int Initialize(); + virtual int Shutdown(); + virtual bool Busy(); + + virtual int DetectInstalledDevices(); + +private: // Property handlers + int OnPort(MM::PropertyBase *pProp, MM::ActionType eAct); + +public: // For access by peripherals + int SendRecv(HamiltonCommand& cmd); + int SelectChannel(char address, int index); +}; + + +class MVP : public CStateDeviceBase +{ + const char address_; + const std::string firmware_; + int index_channel_; + int index_E2_; + DeviceType deviceType_; + + MVPValveType valveType_; + + enum RotationDirection { + CLOCKWISE, + COUNTERCLOCKWISE, + LEAST_ANGLE, + } rotationDirection_; + + MMThreadLock lock_; + + +public: + MVP(char address, int index); + virtual ~MVP(); + + virtual void GetName(char* name) const; + virtual int Initialize(); + virtual int Shutdown(); + virtual bool Busy(); + + virtual int GetPosition(long& pos); + virtual int SetPosition(long pos); + + virtual unsigned long GetNumberOfPositions() const; + +private: // Property handlers + int OnState(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnRotationDirection(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + int SendRecv(HamiltonCommand& cmd); + int SelectChannel(); + + bool ShouldRotateCCW(int curPos, int newPos); + static std::string RotationDirectionToString(RotationDirection rd); + static RotationDirection RotationDirectionFromString(const std::string& s); + +private: + MVP& operator=(const MVP&); // = delete +}; + + +class PSD : public CVolumetricPumpBase +{ + const char address_; + const std::string firmware_; + int index_channel_; + int index_E2_; + + double flowrate_ul_per_sec_; + int speed_; + long back_off_steps_; + long return_steps_; + DeviceType deviceType_; + + // full stroke is device dependant and cannot be queried. + long full_stroke_steps_; + + // current Volume + double volume_ul_; + // max Volume + double volumeMax_ul_; + + bool busy_; + bool initialized_; + + enum DriveResolution { + HALFRESOLUTION=0, + FULLRESOLUTION = 1, + FULLRESOLUTION_OVERLOADDISABLED = 2, + } driveResolution_; + + MMThreadLock lock_; + +public: + PSD(char address, int index); + virtual ~PSD(); + + virtual void GetName(char* name) const; + virtual int Initialize(); + virtual int Shutdown(); + virtual bool Busy(); + + /* + virtual int Aspirate(); + virtual int AspirateVolumeUl(double volUl); + virtual int Dispense(); + virtual int DispenseVolumeUl(double volUl); + */ + virtual int Home(); + virtual int Stop(); + //virtual bool RequiresHoming(); + //virtual int InvertDirection(bool state); + //virtual int IsDirectionInverted(bool& state); + //virtual int SetVolumeUl(double volUl); + //virtual int GetVolumeUl(double& volUl); + //virtual int SetMaxVolumeUl(double volUl); + //virtual int GetMaxVolumeUl(double& volUl); + //virtual int SetFlowrateUlPerSecond(double flowrate); + //virtual int GetFlowrateUlPerSecond(double& flowrate); + virtual int Start(); + virtual int DispenseDurationSeconds(double durSec); + virtual int DispenseVolumeUl(double volUl); + + bool RequiresHoming() + { + return false; + } + int InvertDirection(bool state) + { + return DEVICE_OK; + } + int IsDirectionInverted(bool& state) + { + return false; + } + + int SetFlowrateUlPerSecond(double flowrate) + { + flowrate_ul_per_sec_ = flowrate; + speed_ = (int)(volumeMax_ul_ / flowrate_ul_per_sec_); + return DEVICE_OK; + } + + int GetFlowrateUlPerSecond(double& flowrate) + { + flowrate = flowrate_ul_per_sec_ ; + LogMessage("Reporting flowrate (ul/sec)", true); + return DEVICE_OK; + } + + int SetVolumeUl(double volUl) + { + volume_ul_ = volUl; + return DEVICE_OK; + } + + int GetVolumeUl(double& volUl) + { + int err; + MMThreadGuard myLock(lock_); + { + err = SelectChannel(); + SyringePositionRequest spReq(address_); + + if (err == DEVICE_OK) + err = SendRecv(spReq); + + if (err == DEVICE_OK) + volume_ul_ = spReq.GetPosition() * volumeMax_ul_ / full_stroke_steps_; + } + if (err == DEVICE_OK) + volUl = volume_ul_; + + return err; + } + + int SetMaxVolumeUl(double volUl) + { + volumeMax_ul_ = volUl; + speed_ = (int)(volumeMax_ul_ / flowrate_ul_per_sec_); + + //get and set volume_ul_ + int err = GetVolumeUl(volUl); + return err; + } + + int GetMaxVolumeUl(double& volUl) + { + volUl = volumeMax_ul_; + return DEVICE_OK; + } + +private: + int SetBackOffSteps(int steps); + int SetReturnSteps(int steps); + int SetDriveResolution(int resolution); + int SendRecv(HamiltonCommand& cmd); + int SelectChannel(); + static std::string DriveResolutionToString(DriveResolution rd); + static DriveResolution DriveResolutionFromString(const std::string& s); + +private: // Property handlers + int OnDriveResolution(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnFullStrokeSteps(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnFlowrate(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnVolume(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnMaxVolume(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnReturnSteps(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnBackOffSteps(MM::PropertyBase* pProp, MM::ActionType eAct); + +private: + PSD& operator=(const PSD&); // = delete +}; + + +MODULE_API void InitializeModuleData() +{ + RegisterDevice(DEVICE_NAME_HUB, MM::HubDevice, + "Hamilton RNO adapter"); +} + + +MODULE_API MM::Device* CreateDevice(const char* name) +{ + //The pump or valve index (to account for multi-valve / multi-pump systems + int index; + char address; + char* description; + MM::Device* device; + + if (!name) + return 0; + + // Create a Hub device + if (strcmp(name, DEVICE_NAME_HUB) == 0) + return new MVPChain(); + + // Create a MVP device: + // + // NOTE: Some may have multiple valves and pumps per device, + // hence the need for a channel index. + // The index is used to determine the position of the E2 information byte. + // + // Index values can be: 0 (one device, no index selection), 1 (left) or 2 (right). + // The MVP or PSD would use 0 as the index, the Microlab uses 1 and 2, so we know we + // need to select left or right devices before issuing a command. + // + // The name generated also depends on the index value: + // * With 0, just do a prefix-device + // * With 1 or mode, do prefix-deviceIndex where device is [a-z] and index is [1-4] + // + if (strncmp(name, DEVICE_NAME_MVP_PREFIX, strlen(DEVICE_NAME_MVP_PREFIX)) == 0) + { + // Address must be 1 or 2 char + if (strlen(name) - strlen(DEVICE_NAME_MVP_PREFIX) > 2 || strlen(name) - strlen(DEVICE_NAME_MVP_PREFIX) == 0) + return 0; + + if (strlen(name) - strlen(DEVICE_NAME_MVP_PREFIX) == 2) + { + address = name[strlen(name) - 2]; + index = (int)(name[strlen(name) - 1]) - '0'; + } + else + { + address = name[strlen(name) - 1]; + index = 0; + } + + device = new MVP(address, index); + RegisterDevice(name, MM::StateDevice, DEVICE_NAME_MVP_FULLNAME); + return device; + } + + // Create a PSD device + if (strncmp(name, DEVICE_NAME_PSD_PREFIX, strlen(DEVICE_NAME_PSD_PREFIX)) == 0) + { + // Address must be 1 or 2 char + if (strlen(name) - strlen(DEVICE_NAME_PSD_PREFIX) > 2 || strlen(name) - strlen(DEVICE_NAME_PSD_PREFIX) == 0) + return 0; + + if (strlen(name) - strlen(DEVICE_NAME_PSD_PREFIX) == 2) + { + address = name[strlen(name) - 2]; + index = (int)(name[strlen(name) - 1]) - '0'; + } + else + { + address = name[strlen(name) - 1]; + index = 0; + } + + device = new PSD(address, index); + RegisterDevice(name, MM::VolumetricPumpDevice, DEVICE_NAME_PSD_FULLNAME); + return device; + } + + return 0; +} + + +MODULE_API void DeleteDevice(MM::Device* device) +{ + delete device; +} + + +/////////////////////////////////////////////////////////////////////////////// +// Hamilton Hub +/////////////////////////////////////////////////////////////////////////////// + +MVPChain::MVPChain() : + port_("Undefined"), + maxAddr_('a') +{ + CreateStringProperty("Port", port_.c_str(), false, + new CPropertyAction(this, &MVPChain::OnPort), true); +} + + +MVPChain::~MVPChain() +{ +} + + +void +MVPChain::GetName(char* name) const +{ + CDeviceUtils::CopyLimitedString(name, DEVICE_NAME_HUB); +} + + +int +MVPChain::Initialize() +{ + int err; + + //Send a global reset, no answer to be expected. + err = SendSerialCommand(port_.c_str(), ":!", RNO_TERM); + if (err != DEVICE_OK) + return err; + + //Try 5 times + for (int i=0; i < 5; i++) + { + + CDeviceUtils::SleepMs(500); + std::string answer; + err = GetSerialAnswer(port_.c_str(), RNO_TERM, answer); + if (err == DEVICE_OK) + break; + } + + AutoAddressingCommand autoaddr; + err = SendRecv(autoaddr); + if (err != DEVICE_OK) + return err; + + if (autoaddr.HasMaxAddr()) + { + maxAddr_ = autoaddr.GetMaxAddr(); + } + else + { + // Autoaddressing did not happen, presumably because the MVPs have + // already been assigned addresses. + // In this case, we test each address. + char addr; + for (addr = 'a'; addr < 'z'; ++addr) + { + FirmwareVersionRequest req(addr); + err = SendRecv(req); + if (err) + { + if (addr == 'a') + return err; + break; + } + } + maxAddr_ = addr - 1; + } + + LogMessage(("Last address in chain is '" + std::string(1, maxAddr_) + "'").c_str()); + + return DEVICE_OK; +} + + +int +MVPChain::Shutdown() +{ + return DEVICE_OK; +} + + +bool +MVPChain::Busy() +{ + return false; +} + + +int +MVPChain::DetectInstalledDevices() +{ + ClearInstalledDevices(); + + int err = DEVICE_OK; + std::string firmware; + std::string deviceName; + + for (char addr = 'a'; addr <= maxAddr_; ++addr) + { + MM::Device* device; + + FirmwareVersionRequest fvReq(addr); + err = SendRecv(fvReq); + if (err != DEVICE_OK) + break; + + firmware = fvReq.GetFirmwareVersion(); + DeviceType deviceType = GetDeviceTypeFromFirmware(firmware); + deviceName = GetDeviceTypeName(deviceType); + LogMessage(("Detecting device '" + std::string(1, addr) + "' " + firmware + " " + deviceName).c_str(), true); + + switch (deviceType) + { + case DeviceTypeML600: case DeviceTypeML500A: case DeviceTypeML500B: case DeviceTypeML500C: case DeviceTypeML700: + // FIXME need to check these against actual hardware, I'm just assuming here they are dual pump / dual valve systems + device = new MVP(addr, 1); + if (device) + AddInstalledDevice(device); + device = new MVP(addr, 2); + if (device) + AddInstalledDevice(device); + device = new PSD(addr, 1); + if (device) + AddInstalledDevice(device); + device = new PSD(addr, 2); + if (device) + AddInstalledDevice(device); + break; + case DeviceTypeMVP: + device = new MVP(addr, 1); + if (device) + AddInstalledDevice(device); + break; + case DeviceTypePSD2: case DeviceTypePSD3: case DeviceTypePSD4: case DeviceTypePSD6: case DeviceTypePSD8: + //FIXME all these have a valve and a syringe pump. Need a way to check whether the valve exists or not. + device = new MVP(addr, 0); + if (device) + AddInstalledDevice(device); + //Add the syringe + device = new PSD(addr, 0); + if (device) + AddInstalledDevice(device); + break; + default: + err = ERR_UNKNOWN_VALVE_TYPE; + } + + } + return DEVICE_OK; +} + + +int +MVPChain::SelectChannel(char address, int index) +{ + int err = DEVICE_OK; + if (index > 0) { + ChannelSelectionCommand csCmd(address, index); + err = SendRecv(csCmd); + } + return err; +} + +int +MVPChain::OnPort(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(port_.c_str()); + } + else if (eAct == MM::AfterSet) + { + pProp->Get(port_); + } + return DEVICE_OK; +} + + +int +MVPChain::SendRecv(HamiltonCommand& cmd) +{ + int err; + + err = SendSerialCommand(port_.c_str(), cmd.Get().c_str(), RNO_TERM); + if (err != DEVICE_OK) + return err; + + bool expectsMore = true; + while (expectsMore) + { + std::string answer; + err = GetSerialAnswer(port_.c_str(), RNO_TERM, answer); + if (err != DEVICE_OK) + return err; + + err = cmd.ParseResponse(answer, expectsMore); + if (err != DEVICE_OK) + return err; + } + + return DEVICE_OK; +} + + +/////////////////////////////////////////////////////////////////////////////// +// Hamilton Valve +/////////////////////////////////////////////////////////////////////////////// + +MVP::MVP(char address, int index) : + lock_(), + address_(address), + index_channel_(index), + index_E2_(1), + valveType_(ValveTypeUnknown), + rotationDirection_(LEAST_ANGLE) +{ +} + + +MVP::~MVP() +{ +} + + +void +MVP::GetName(char* name) const +{ + std::string suffix(1, address_); + + //This ensures any previous naming still applies + //For the Microlab, this will translate to -a1 and -a2 + if (index_channel_ > 0) + suffix += std::to_string(index_channel_); + + CDeviceUtils::CopyLimitedString(name, + (DEVICE_NAME_MVP_PREFIX + suffix).c_str()); +} + + +int +MVP::Initialize() +{ + int err; + const char* description = NULL; + std::string firmware; + std::string deviceName; + + // FIXME: This here is not thread-safe, but are initializations done one at a time? Does it matter? + err = SelectChannel(); + if (err != DEVICE_OK) + return err; + + FirmwareVersionRequest fvReq(address_); + err = SendRecv(fvReq); + if (err != DEVICE_OK) + return err; + + firmware = fvReq.GetFirmwareVersion(); + deviceType_ = GetDeviceTypeFromFirmware(firmware); + deviceName = GetDeviceTypeName(deviceType_); + LogMessage(("Initializing MVP device '" + std::string(1, address_) + "' " + firmware + " " + deviceName).c_str(), true); + + err = CreateStringProperty("FirmwareVersion", + firmware.c_str(), true); + if (err != DEVICE_OK) + return err; + + switch (deviceType_) + { + case DeviceTypeML600: case DeviceTypeML500A: case DeviceTypeML500B: case DeviceTypeML500C: case DeviceTypeML700: + // FIXME need to check these against actual hardware, I'm just assuming here they are dual pump / dual valve systems + // a and c = These ASCII values show errors for the left (a) and right (c) syringes. + // b and d = These ASCII values show errors for the left (b) and right (d) valves. + if (index_channel_ == 1) index_E2_ = 1; + if (index_channel_ == 2) index_E2_ = 3; + break; + default: + index_E2_ = 1; + } + + ValveErrorRequest errReq(address_,index_E2_); + err = SendRecv(errReq); + + if (err != DEVICE_OK) + return err; + if (errReq.IsValveNotInitialized()) + { + ValveInitializationCommand initCmd(address_); + err = SendRecv(initCmd); + if (err != DEVICE_OK) + return err; + + MM::MMTime deadline = GetCurrentMMTime() + MM::MMTime(15 * 1000 * 1000); + bool busy = true; + while (busy && GetCurrentMMTime() < deadline) + { + busy = Busy(); + if (!busy) + break; + CDeviceUtils::SleepMs(200); + } + if (busy) + return ERR_INITIALIZATION_TIMED_OUT; + } + + // Description + err = CreateStringProperty(MM::g_Keyword_Description, deviceName.c_str(), true); + if (err != DEVICE_OK) + return err; + + ValveTypeRequest typeReq(address_); + err = SendRecv(typeReq); + if (err != DEVICE_OK) + return err; + valveType_ = typeReq.GetValveType(); + if (valveType_ == ValveTypeUnknown) + return ERR_UNKNOWN_VALVE_TYPE; + err = CreateStringProperty("ValveType", + GetValveTypeName(typeReq.GetValveType()).c_str(), true); + if (err != DEVICE_OK) + return err; + + long pos; + err = GetPosition(pos); + if (err != DEVICE_OK) + return err; + err = CreateIntegerProperty(MM::g_Keyword_State, pos, false, + new CPropertyAction(this, &MVP::OnState)); + if (err != DEVICE_OK) + return err; + for (unsigned long i = 0; i < GetNumberOfPositions(); ++i) + { + char s[16]; + snprintf(s, 15, "%ld", i); + AddAllowedValue(MM::g_Keyword_State, s); + } + + err = CreateStringProperty(MM::g_Keyword_Label, "Undefined", false, + new CPropertyAction(this, &CStateBase::OnLabel)); + if (err != DEVICE_OK) + return err; + for (unsigned long i = 0; i < GetNumberOfPositions(); ++i) + { + char label[32]; + snprintf(label, 31, "Position-%ld", i); + SetPositionLabel(i, label); + } + + ValveSpeedRequest speedReq(address_); + err = SendRecv(speedReq); + if (err != DEVICE_OK) + return err; + err = CreateIntegerProperty("ValveSpeedHz", + speedReq.GetSpeedHz(), true); + if (err != DEVICE_OK) + return err; + + err = CreateStringProperty("RotationDirection", + RotationDirectionToString(rotationDirection_).c_str(), false, + new CPropertyAction(this, &MVP::OnRotationDirection)); + if (err != DEVICE_OK) + return err; + AddAllowedValue("RotationDirection", "Clockwise"); + AddAllowedValue("RotationDirection", "Counterclockwise"); + AddAllowedValue("RotationDirection", "Least rotation angle"); + + return DEVICE_OK; +} + + +int +MVP::Shutdown() +{ + return DEVICE_OK; +} + + +bool +MVP::Busy() +{ + bool busy = false; + + MMThreadGuard myLock(lock_); + { + int err = SelectChannel(); + if (err == DEVICE_OK) + { + MovementFinishedRequest req(address_); + err = SendRecv(req); + busy = !req.IsMovementFinished(); + } + if (err != DEVICE_OK) + busy = false; + } + + return busy; +} + + +unsigned long +MVP::GetNumberOfPositions() const +{ + return GetValveNumberOfPositions(valveType_); +} + + +/////////////////////////////////////////////////////////////////////////////// +// Action handlers +/////////////////////////////////////////////////////////////////////////////// + +int +MVP::OnState(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + long pos; + int err = GetPosition(pos); + if (err != DEVICE_OK) + return err; + pProp->Set(pos); + } + else if (eAct == MM::AfterSet) + { + long v; + pProp->Get(v); + int err = SetPosition(v); + if (err != DEVICE_OK) + return err; + } + return DEVICE_OK; +} + + +int +MVP::OnRotationDirection(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(RotationDirectionToString(rotationDirection_).c_str()); + } + else if (eAct == MM::AfterSet) + { + std::string v; + pProp->Get(v); + rotationDirection_ = RotationDirectionFromString(v); + } + return DEVICE_OK; +} + + +int +MVP::SendRecv(HamiltonCommand& cmd) +{ + return static_cast(GetParentHub())->SendRecv(cmd); +} + + +int +MVP::SelectChannel() +{ + int err = DEVICE_OK; + if (index_channel_ > 0) + err = static_cast(GetParentHub())->SelectChannel(address_, index_channel_); + return err; +} + + +int +MVP::GetPosition(long& pos) +{ + int err; + ValvePositionRequest req(address_); + + MMThreadGuard myLock(lock_); + { + // Channel selection + err = SelectChannel(); + if (err == DEVICE_OK) + err = SendRecv(req); + } + + if (err != DEVICE_OK) + return err; + + pos = req.GetPosition(); + return DEVICE_OK; +} + + +int +MVP::SetPosition(long pos) +{ + int err; + long curPos; + + err = GetPosition(curPos); + if (err != DEVICE_OK) + return err; + + MMThreadGuard myLock(lock_); + { + // Channel selection + err = SelectChannel(); + if (err == DEVICE_OK) + { + ValvePositionCommand cmd(address_, ShouldRotateCCW(curPos, pos), pos); + err = SendRecv(cmd); + } + } + return err; +} + + +bool +MVP::ShouldRotateCCW(int curPos, int newPos) +{ + switch (rotationDirection_) + { + case CLOCKWISE: + return false; + case COUNTERCLOCKWISE: + return true; + case LEAST_ANGLE: + default: + { + int cwAngle = GetValveRotationAngle(valveType_, false, curPos, newPos); + int ccwAngle = GetValveRotationAngle(valveType_, true, curPos, newPos); + return (ccwAngle < cwAngle); + } + } +} + + +std::string +MVP::RotationDirectionToString(RotationDirection rd) +{ + switch (rd) + { + case CLOCKWISE: + return "Clockwise"; + case COUNTERCLOCKWISE: + return "Counterclockwise"; + case LEAST_ANGLE: + default: + return "Least rotation angle"; + } +} + + +MVP::RotationDirection +MVP::RotationDirectionFromString(const std::string& s) +{ + if (s == "Clockwise") + return CLOCKWISE; + if (s == "Counterclockwise") + return COUNTERCLOCKWISE; + return LEAST_ANGLE; +} + + +/////////////////////////////////////////////////////////////////////////////// +// Hamilton Syringe Pumps +/////////////////////////////////////////////////////////////////////////////// + +PSD::PSD(char address, int index) : + lock_(), + address_(address), + index_channel_(index), + index_E2_(0), + volumeMax_ul_(1000), + flowrate_ul_per_sec_(2000), + speed_(0), + back_off_steps_(0), + return_steps_(0), + volume_ul_(0), + busy_(false), + initialized_(false) +{ +} + + +PSD::~PSD() +{ +} + + +void +PSD::GetName(char* name) const +{ + std::string suffix(1, address_); + + //This ensures any previous naming still applies + //For the Microlab, this will translate to -a1 and -a2 + if (index_channel_ > 0) + suffix += std::to_string(index_channel_); + + CDeviceUtils::CopyLimitedString(name, + (DEVICE_NAME_PSD_PREFIX + suffix).c_str()); +} + + +int +PSD::Initialize() +{ + int err; + const char* description = NULL; + std::string firmware; + std::string deviceName; + + // Channel selection + err = SelectChannel(); + if (err != DEVICE_OK) + return err; + + FirmwareVersionRequest fvReq(address_); + err = SendRecv(fvReq); + if (err != DEVICE_OK) + return err; + + firmware = fvReq.GetFirmwareVersion(); + deviceType_ = GetDeviceTypeFromFirmware(firmware); + deviceName = GetDeviceTypeName(deviceType_); + LogMessage(("Initializing PSD device '" + std::string(1, address_) + "' " + firmware + " " + deviceName).c_str(), true); + + err = CreateStringProperty("FirmwareVersion", + firmware.c_str(), true); + if (err != DEVICE_OK) + return err; + + switch (deviceType_) + { + case DeviceTypeML600: case DeviceTypeML500A: case DeviceTypeML500B: case DeviceTypeML500C: case DeviceTypeML700: + // FIXME need to check these against actual hardware, I'm just assuming here they are dual pump / dual valve systems + // a and c = These ASCII values show errors for the left (a) and right (c) syringes. + // b and d = These ASCII values show errors for the left (b) and right (d) valves. + if (index_channel_ == 1) index_E2_ = 0; + if (index_channel_ == 2) index_E2_ = 2; + break; + default: + index_E2_ = 0; + } + + SyringeErrorRequest errReq(address_,index_E2_); + err = SendRecv(errReq); + + if (err != DEVICE_OK) + return err; + if (errReq.IsSyringeNotInitialized()) + { + err = Home(); + if (err != DEVICE_OK) + return err; + } + + /* Syringe parameters */ + + SyringeResolutionRequest srReq(address_); + err = SendRecv(srReq); + if (err != DEVICE_OK) + return err; + driveResolution_ = (DriveResolution)srReq.GetResolution(); + + SyringeBackOffStepsRequest bosReq(address_); + err = SendRecv(bosReq); + if (err != DEVICE_OK) + return err; + back_off_steps_ = bosReq.GetSteps(); + + SyringeReturnStepsRequest rsReq(address_); + err = SendRecv(rsReq); + if (err != DEVICE_OK) + return err; + return_steps_ = rsReq.GetSteps(); + + // Description + // ----------- + err = CreateStringProperty(MM::g_Keyword_Description, deviceName.c_str(), true); + if (err != DEVICE_OK) + return err; + + // Resolution + // ---------- + err = CreateStringProperty("DriveResolution", + DriveResolutionToString(driveResolution_).c_str(), false, + new CPropertyAction(this, &PSD::OnDriveResolution)); + if (err != DEVICE_OK) + return err; + AddAllowedValue("DriveResolution", g_Keyword_HalfResolution ); + AddAllowedValue("DriveResolution", g_Keyword_FullResolution ); + AddAllowedValue("DriveResolution", g_Keyword_FullResolutionDisabled ); + + // Full Stroke Steps + // ----------------- + full_stroke_steps_ = GetSyringeThrowSteps(deviceType_, driveResolution_); + err = CreateIntegerProperty(g_Keyword_FullStrokeSteps, full_stroke_steps_, true, + new CPropertyAction (this, &PSD::OnFullStrokeSteps)); + if (err != DEVICE_OK) + return err; + + std::ostringstream os; + os << "Resolution: " << driveResolution_ << " - Full stroke: " << full_stroke_steps_; + LogMessage(os.str().c_str()); + + // Max Volume + // ---------- + err = CreateFloatProperty(MM::g_Keyword_Max_Volume, volumeMax_ul_, false, + new CPropertyAction (this, &PSD::OnMaxVolume)); + if (err != DEVICE_OK) + return err; + + // Flow rate + // --------- + err = CreateFloatProperty(MM::g_Keyword_Flowrate, flowrate_ul_per_sec_, false, + new CPropertyAction (this, &PSD::OnFlowrate)); + if (err != DEVICE_OK) + return err; + + // Volume + // ------ + err = CreateFloatProperty(MM::g_Keyword_Current_Volume, volume_ul_, true, + new CPropertyAction (this, &PSD::OnVolume)); + if (err != DEVICE_OK) + return err; + + // Back-off Steps + // -------------- + err = CreateIntegerProperty(g_Keyword_BackOffSteps, back_off_steps_, false, + new CPropertyAction (this, &PSD::OnBackOffSteps)); + if (err != DEVICE_OK) + return err; + + // Return Steps + // ------------ + err = CreateIntegerProperty(g_Keyword_ReturnSteps, return_steps_, false, + new CPropertyAction (this, &PSD::OnReturnSteps)); + if (err != DEVICE_OK) + return err; + + initialized_ = true; + return DEVICE_OK; +} + + +int +PSD::Shutdown() +{ + Stop(); + + if (initialized_) + { + initialized_ = false; + } + return DEVICE_OK; +} + +bool +PSD::Busy() +{ + bool busy = false; + + + MMThreadGuard myLock(lock_); + { + // Channel selection + int err = SelectChannel(); + if (err == DEVICE_OK) + { + MovementFinishedRequest req(address_); + err = SendRecv(req); + busy = !req.IsMovementFinished(); + } + if (err != DEVICE_OK) + busy = false; + } + + return busy; +} + +int PSD::SetDriveResolution(int resolution) +{ + int err; + MMThreadGuard myLock(lock_); + { + err = SelectChannel(); + if (err == DEVICE_OK) + { + SyringeResolutionCommand req(address_, resolution); + err = SendRecv(req); + if (err == DEVICE_OK) + driveResolution_ = (DriveResolution)resolution; + } + } + return err; +} + +int PSD::SetReturnSteps(int steps) +{ + int err; + MMThreadGuard myLock(lock_); + { + err = SelectChannel(); + if (err == DEVICE_OK) + { + SyringeReturnStepsCommand req(address_, steps); + err = SendRecv(req); + if (err == DEVICE_OK) + return_steps_ = steps; + } + } + return err; +} + +int PSD::SetBackOffSteps(int steps) +{ + int err; + MMThreadGuard myLock(lock_); + { + err = SelectChannel(); + if (err == DEVICE_OK) + { + SyringeBackOffStepsCommand req(address_, steps); + err = SendRecv(req); + if (err == DEVICE_OK) + back_off_steps_ = steps; + } + } + return err; +} + +/* +int PSD::Aspirate() +{ + LogMessage("Filling up the syringe", true); + return AspirateVolume(volumeMax_ul_ - volume_ul_); +} + +int PSD::AspirateVolume(double volUl) +{ + if (volUl < 0 || volUl > (volumeMax_ul_ - volume_ul_)) + return ERR_UNKNOWN_POSITION; + + long steps = (long)(full_stroke_steps_ * volUl / volumeMax_ul_); + + std::ostringstream os; + os << "Aspirating : " << volUl << " uL - " << steps; + LogMessage(os.str().c_str()); + + // Channel selection + int err; + MMThreadGuard myLock(lock_); + { + err = SelectChannel(); + if (err == DEVICE_OK) + { + SyringePickupCommand pCmd(address_, steps, speed_); + err = SendRecv(pCmd); + if (err == DEVICE_OK) + volume_ul_ += volUl; + } + } + + return err; +} + +int PSD::Dispense() +{ + LogMessage("Emptying the syringe", true); + return DispenseVolume(volume_ul_); +} +*/ + +//Negative volume aspirates? Or error? Maybe a flag? +//Aspirate with invert direction? +int PSD::DispenseVolumeUl(double volUl) +{ + if (volUl < 0 || volUl > volumeMax_ul_) + return ERR_UNKNOWN_POSITION; + + long steps = (long)(full_stroke_steps_ * volUl / volumeMax_ul_); + + std::ostringstream os; + os << "Dispensing : " << volUl << " uL - " << steps; + LogMessage(os.str().c_str()); + + int err; + MMThreadGuard myLock(lock_); + { + err = SelectChannel(); + if (err == DEVICE_OK) + { + SyringeDispenseCommand dCmd(address_, steps, speed_); + err = SendRecv(dCmd); + if (err == DEVICE_OK) + volume_ul_ -= volUl; + } + } + return err; +} + +int PSD::DispenseDurationSeconds(double durSec) +{ + double volUl, flowrate; + int nRet = DEVICE_OK; + + nRet = GetFlowrateUlPerSecond(flowrate); + + if (nRet != DEVICE_OK ) + return nRet; + + volUl = durSec * flowrate; + + if (volUl < 0 || volUl > volumeMax_ul_) + return ERR_UNKNOWN_POSITION; + + return DispenseVolumeUl(volUl); +} + +int PSD::Home() +{ + LogMessage("Homing the syringe pump", true); + + int err; + MMThreadGuard myLock(lock_); + { + err = SelectChannel(); + if (err == DEVICE_OK) + { + SyringeInitializationCommand initCmd(address_); + err = SendRecv(initCmd); + } + } + if (err != DEVICE_OK) + return err; + + MM::MMTime deadline = GetCurrentMMTime() + MM::MMTime(15 * 1000 * 1000); + bool busy = true; + while (busy && GetCurrentMMTime() < deadline) + { + busy = Busy(); + if (!busy) + break; + CDeviceUtils::SleepMs(200); + } + if (busy) + return ERR_INITIALIZATION_TIMED_OUT; + + LogMessage("Did Home the syringe pump", true); + return DEVICE_OK; +} + +//FIXME I need to check this: Initiate a dispense, followed by as many start/stop as needed? Wait for start? Or Autostart? +int PSD::Start() +{ + int err; + LogMessage("Starting the pump", true); + + MMThreadGuard myLock(lock_); + { + } + + LogMessage("Did Start the pump", true); + return DEVICE_OK; +} + +int PSD::Stop() +{ + int err; + LogMessage("Stopping the pump", true); + + MMThreadGuard myLock(lock_); + { + err = SelectChannel(); + if (err == DEVICE_OK) + { + // This pauses the pump + SyringeHaltCommand haltCmd(address_); + err = SendRecv(haltCmd); + } + if (err == DEVICE_OK) + { + // This clears the pause state (so it stops) + SyringeClearPendingCommand clearCmd(address_); + err = SendRecv(clearCmd); + } + } + + if (err != DEVICE_OK) + return err; + + LogMessage("Did Stop the pump", true); + return DEVICE_OK; +} + +int +PSD::SendRecv(HamiltonCommand& cmd) +{ + return static_cast(GetParentHub())->SendRecv(cmd); +} + +int +PSD::SelectChannel() +{ + int err = DEVICE_OK; + if (index_channel_ > 0) + err = static_cast(GetParentHub())->SelectChannel(address_, index_channel_); + return err; +} + +std::string +PSD::DriveResolutionToString(DriveResolution dr) +{ + switch (dr) + { + case FULLRESOLUTION: + return g_Keyword_FullResolution; + case FULLRESOLUTION_OVERLOADDISABLED: + return g_Keyword_FullResolutionDisabled; + default: + return g_Keyword_HalfResolution; + } +} + + +PSD::DriveResolution +PSD::DriveResolutionFromString(const std::string& s) +{ + if (s == g_Keyword_FullResolution) + return FULLRESOLUTION; + if (s == g_Keyword_FullResolutionDisabled) + return FULLRESOLUTION_OVERLOADDISABLED; + return HALFRESOLUTION; +} + +/////////////////////////////////////////////////////////////////////////////// +// Action handlers +/////////////////////////////////////////////////////////////////////////////// + +int PSD::OnDriveResolution(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(DriveResolutionToString(driveResolution_).c_str()); + } + else if (eAct == MM::AfterSet) + { + std::string v; + pProp->Get(v); + driveResolution_ = DriveResolutionFromString(v); + + full_stroke_steps_ = GetSyringeThrowSteps(deviceType_, driveResolution_); + OnPropertyChanged(g_Keyword_FullStrokeSteps, CDeviceUtils::ConvertToString(full_stroke_steps_)); + } + return DEVICE_OK; +} + +int PSD::OnFullStrokeSteps(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + std::stringstream s; + s << full_stroke_steps_; + pProp->Set(s.str().c_str()); + } + return DEVICE_OK; +} + +int PSD::OnVolume(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + int nRet = DEVICE_OK; + if (eAct == MM::BeforeGet) + { + double volume; + nRet = GetVolumeUl(volume); + if (nRet != DEVICE_OK) + return nRet; + + std::stringstream s; + s << volume; + pProp->Set(s.str().c_str()); + } + return DEVICE_OK; +} + +int PSD::OnMaxVolume(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + int nRet = DEVICE_OK; + if (eAct == MM::BeforeGet) + { + std::stringstream s; + s << volumeMax_ul_; + pProp->Set(s.str().c_str()); + } + else if (eAct == MM::AfterSet) + { + double vol; + pProp->Get(vol); + nRet = SetMaxVolumeUl(vol); + } + return nRet; +} + +int PSD::OnReturnSteps(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + int nRet = DEVICE_OK; + if (eAct == MM::BeforeGet) + { + std::stringstream s; + s << return_steps_; + pProp->Set(s.str().c_str()); + } + else if (eAct == MM::AfterSet) + { + long steps; + pProp->Get(steps); + nRet = SetReturnSteps(steps); + } + return nRet; +} + +int PSD::OnBackOffSteps(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + int nRet = DEVICE_OK; + if (eAct == MM::BeforeGet) + { + std::stringstream s; + s << back_off_steps_; + pProp->Set(s.str().c_str()); + } + else if (eAct == MM::AfterSet) + { + long steps; + pProp->Get(steps); + nRet = SetBackOffSteps(steps); + } + return nRet; +} + +int PSD::OnFlowrate(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + int nRet = DEVICE_OK; + if (eAct == MM::BeforeGet) + { + std::stringstream s; + s << flowrate_ul_per_sec_; + pProp->Set(s.str().c_str()); + } + else if (eAct == MM::AfterSet) + { + double fr; + pProp->Get(fr); + nRet = SetFlowrateUlPerSecond(fr); + } + return nRet; +} + diff --git a/DeviceAdapters/HamiltonRNO/HamiltonRNO.vcxproj b/DeviceAdapters/HamiltonRNO/HamiltonRNO.vcxproj new file mode 100755 index 000000000..9a24ffec0 --- /dev/null +++ b/DeviceAdapters/HamiltonRNO/HamiltonRNO.vcxproj @@ -0,0 +1,93 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {A0676B13-4850-4BF4-88BC-F099BEE5B762} + Win32Proj + HamiltonRNO + 10.0 + + + + DynamicLibrary + true + Unicode + v142 + + + DynamicLibrary + false + true + Unicode + v142 + + + + + + + + + + + + + + + + + true + + + false + + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;HAMILTONRNO_EXPORTS;%(PreprocessorDefinitions) + + + Windows + true + + + + + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;HAMILTONRNO_EXPORTS;%(PreprocessorDefinitions) + + + Windows + true + true + true + + + + + + + + + + + + + {b8c95f39-54bf-40a9-807b-598df2821d55} + + + + + + diff --git a/DeviceAdapters/HamiltonRNO/HamiltonRNO.vcxproj.filters b/DeviceAdapters/HamiltonRNO/HamiltonRNO.vcxproj.filters new file mode 100755 index 000000000..e33d41e74 --- /dev/null +++ b/DeviceAdapters/HamiltonRNO/HamiltonRNO.vcxproj.filters @@ -0,0 +1,34 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;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;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + + Header Files + + + Header Files + + + + + Source Files + + + diff --git a/DeviceAdapters/HamiltonRNO/Makefile.am b/DeviceAdapters/HamiltonRNO/Makefile.am new file mode 100755 index 000000000..fcf9a63bd --- /dev/null +++ b/DeviceAdapters/HamiltonRNO/Makefile.am @@ -0,0 +1,14 @@ +AM_CXXFLAGS = $(MMDEVAPI_CXXFLAGS) +deviceadapter_LTLIBRARIES = libmmgr_dal_HamiltonRNO.la +libmmgr_dal_HamiltonRNO_la_SOURCES = HamiltonRNO.cpp \ + HamiltonCommands.h \ + HamiltonValves.h \ + HamiltonSyringes.h +libmmgr_dal_HamiltonRNO_la_LIBADD = $(MMDEVAPI_LIBADD) +libmmgr_dal_HamiltonRNO_la_LDFLAGS = $(MMDEVAPI_LDFLAGS) + +if BUILD_CPP_TESTS +UNITTESTS = unittest +endif + +SUBDIRS = . $(UNITTESTS) diff --git a/DeviceAdapters/HamiltonRNO/RNOCommands.h b/DeviceAdapters/HamiltonRNO/RNOCommands.h new file mode 100755 index 000000000..a0bf3fe7a --- /dev/null +++ b/DeviceAdapters/HamiltonRNO/RNOCommands.h @@ -0,0 +1,977 @@ +/* + * Micro-Manager device adapter for Hamilton devices that use the RNO protocol + * + * Author: Mark A. Tsuchida for the original MVP code + * Egor Zindy for the PSD additions + * + * Copyright (C) 2018 Applied Materials, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "HamiltonMVP.h" +#include "HamiltonPSD.h" + +#include "MMDeviceConstants.h" + +#include +#include +#include +#include + +#if defined(_MSC_VER) && _MSC_VER < 1900 +#define snprintf _snprintf +#endif + + +const char* const RNO_TERM = "\r"; +const char RNO_ACK = 6; +const char RNO_NAK = 21; + +enum { + ERR_ECHO_MISMATCH = 20001, + ERR_NAK, + ERR_UNEXPECTED_RESPONSE, + ERR_RESPONSE_PARITY, +}; + +class HamiltonCommand +{ +protected: + bool TestBit(char byte, int bit) + { + return (byte & (1 << bit)) != 0; + } + + int ParseDecimal(const std::string& s, int maxNDigits, int& result) + { + if (s.empty() || s.size() > (unsigned int) maxNDigits) + return ERR_UNEXPECTED_RESPONSE; + for (unsigned int i = 0; i < s.size(); ++i) + { + char c = s[i]; + if (c < '0' || c > '9') + return ERR_UNEXPECTED_RESPONSE; + } + result = std::atoi(s.c_str()); + return DEVICE_OK; + } + + std::string FormatDecimal(int /* nDigits */, int value) + { + // It turns out that there is no need to zero-pad to a fixed number of + // digits, despite what the manual may seem to imply. + char buf[16]; + snprintf(buf, 15, "%d", value); + return std::string(buf); + } + +public: + + virtual ~HamiltonCommand() {} + + virtual std::string Get() = 0; + + // expectsMore set to true if further (CR-deliminated) response should be parsed + virtual int ParseResponse(const std::string& response, bool& expectsMore) = 0; +}; + + +class AutoAddressingCommand : public HamiltonCommand +{ + char echoedAddr_; + int responsesParsed_; + +public: + AutoAddressingCommand() : + echoedAddr_('\0'), + responsesParsed_(0) + {} + + virtual std::string Get() { return "1a"; } + virtual int ParseResponse(const std::string& response, bool& expectsMore) + { + expectsMore = false; + if (responsesParsed_++ == 0) + { + if (response.size() != 2) + return ERR_UNEXPECTED_RESPONSE; + if (response[0] != '1') + return ERR_UNEXPECTED_RESPONSE; + echoedAddr_ = response[1]; + + if (echoedAddr_ == 'a') + { + // We get the echo '1a' when the address had already been assigned. + // In this case only, there will be a further ACK response. + expectsMore = true; + } + + return DEVICE_OK; + } + + if (response.size() != 1) + return ERR_UNEXPECTED_RESPONSE; + char ack = response[0]; + if (ack == RNO_NAK) + return ERR_NAK; + if (ack != RNO_ACK) + return ERR_UNEXPECTED_RESPONSE; + + return DEVICE_OK; + } + + bool HasMaxAddr() { return echoedAddr_ > 'a'; } + char GetMaxAddr() { return echoedAddr_ - 1; } +}; + + +class NormalCommand : public HamiltonCommand +{ + char address_; + int responsesParsed_; + +protected: + virtual std::string GetCommandString() = 0; + virtual int ParseContent(const std::string& content) = 0; + +public: + NormalCommand(char address) : + address_(address), + responsesParsed_(0) + {} + + virtual std::string Get() + { return std::string(1, address_) + GetCommandString(); } + + virtual int ParseResponse(const std::string& response, bool& expectsMore) + { + expectsMore = false; + if (responsesParsed_++ == 0) + { + // The first response should be an exact echo + if (response != Get()) + return ERR_ECHO_MISMATCH; + expectsMore = true; + return DEVICE_OK; + } + + // The second response ACK or NAK; ACK may also be followed by query + // result. + if (response.empty()) + return ERR_UNEXPECTED_RESPONSE; + char ack = response[0]; + if (ack == RNO_NAK) + return ERR_NAK; + if (ack != RNO_ACK) + return ERR_UNEXPECTED_RESPONSE; + std::string content = response.substr(1); + return ParseContent(content); + } +}; + + +// Commands whose ACK contains no data +class NonQueryCommand : public NormalCommand +{ +protected: + virtual int ParseContent(const std::string& content) + { return content.empty() ? DEVICE_OK : ERR_UNEXPECTED_RESPONSE; } + +public: + NonQueryCommand(char address) : + NormalCommand(address) + {} +}; + + +class ValveInitializationCommand : public NonQueryCommand +{ +protected: + virtual std::string GetCommandString() { return "LXR"; } + +public: + ValveInitializationCommand(char address) : + NonQueryCommand(address) + {} +}; + + +class SyringeInitializationCommand : public NonQueryCommand +{ +protected: + virtual std::string GetCommandString() { return "X1R"; } + +public: + SyringeInitializationCommand(char address) : + NonQueryCommand(address) + {} +}; + + +class SyringeHaltCommand : public NonQueryCommand +{ +protected: + virtual std::string GetCommandString() { return "K"; } + +public: + SyringeHaltCommand(char address) : + NonQueryCommand(address) + {} +}; + + +class SyringeResumeCommand : public NonQueryCommand +{ +protected: + virtual std::string GetCommandString() { return "$"; } + +public: + SyringeResumeCommand(char address) : + NonQueryCommand(address) + {} +}; + + +class SyringeClearPendingCommand : public NonQueryCommand +{ +protected: + virtual std::string GetCommandString() { return "V"; } + +public: + SyringeClearPendingCommand(char address) : + NonQueryCommand(address) + {} +}; + + +class ValvePositionCommand : public NonQueryCommand +{ + bool ccw_; + int positionOneBased_; + +protected: + virtual std::string GetCommandString() + { + return std::string("LP") + (ccw_ ? "1" : "0") + + FormatDecimal(2, positionOneBased_) + "R"; + } + +public: + ValvePositionCommand(char address, bool counterclockwise, long position) : + NonQueryCommand(address), + ccw_(counterclockwise), + positionOneBased_(position + 1) + {} +}; + + +class SyringePickupCommand : public NonQueryCommand +{ + int numsteps_; + int speed_; + +protected: + virtual std::string GetCommandString() + { + return std::string("P") + FormatDecimal(4, numsteps_) + + "S" + FormatDecimal(2, speed_) + "R"; + } + +public: + SyringePickupCommand(char address, long numsteps, int speed) : + NonQueryCommand(address), + numsteps_(numsteps), + speed_(speed) + {} +}; + + +class SyringeResolutionCommand : public NonQueryCommand +{ + int resolution_; + +protected: + virtual std::string GetCommandString() + { + return std::string("YSM") + FormatDecimal(1, resolution_); + } + +public: + SyringeResolutionCommand(char address, int resolution) : + NonQueryCommand(address), + resolution_(resolution) + {} +}; + + +class SyringeSpeedCommand : public NonQueryCommand +{ + int speed_; + +protected: + virtual std::string GetCommandString() + { + return std::string("YSS") + FormatDecimal(2, speed_); + } + +public: + SyringeSpeedCommand(char address, int speed) : + NonQueryCommand(address), + speed_(speed) + {} +}; + + +class SyringeReturnStepsCommand : public NonQueryCommand +{ + int return_steps_; + +protected: + virtual std::string GetCommandString() + { + return std::string("YSN") + FormatDecimal(4, return_steps_); + } + +public: + SyringeReturnStepsCommand(char address, int return_steps) : + NonQueryCommand(address), + return_steps_(return_steps) + {} +}; + + +class SyringeBackOffStepsCommand : public NonQueryCommand +{ + int back_off_steps_; + +protected: + virtual std::string GetCommandString() + { + return std::string("YSB") + FormatDecimal(4,back_off_steps_); + } + +public: + SyringeBackOffStepsCommand(char address, int back_off_steps) : + NonQueryCommand(address), + back_off_steps_(back_off_steps) + {} +}; + + +class SyringeDispenseCommand : public NonQueryCommand +{ + int numsteps_; + int speed_; + +protected: + virtual std::string GetCommandString() + { + return std::string("D") + FormatDecimal(4, numsteps_) + + "S" + FormatDecimal(2, speed_) + "R"; + } + +public: + SyringeDispenseCommand(char address, long numsteps, int speed) : + NonQueryCommand(address), + numsteps_(numsteps), + speed_(speed) + {} +}; + + +class ChannelSelectionCommand : public NonQueryCommand +{ + int channel_; + +protected: + virtual std::string GetCommandString() + { + if (channel_ < 1 || channel_ > 2) + channel_ = 1; + + //char chan = 'A'+channel_; + //return std::string("")+chan; + return std::string(1, static_cast('A' + channel_)); + } + +public: + ChannelSelectionCommand(char address, int channel) : + NonQueryCommand(address), + channel_(channel) + {} +}; + + +class ResetInstrumentCommand : public NonQueryCommand +{ +protected: + virtual std::string GetCommandString() { return "!"; } +public: + ResetInstrumentCommand(char address) : + NonQueryCommand(address) + {} +}; + + +class InstrumentStatusRequest : public NormalCommand +{ + char b1_; + +protected: + virtual std::string GetCommandString() { return "E1"; } + virtual int ParseContent(const std::string& content) + { + if (content.size() != 1) + return ERR_UNEXPECTED_RESPONSE; + + b1_ = content[0]; + if (TestBit(b1_, 5)) + return ERR_UNEXPECTED_RESPONSE; + if (!TestBit(b1_, 6)) + return ERR_UNEXPECTED_RESPONSE; + return DEVICE_OK; + } + +public: + InstrumentStatusRequest(char address) : + NormalCommand(address), + b1_(0) + {} + + bool IsReceivedCommandButNotExecuted() { return TestBit(b1_, 0); } + bool IsSyringeDriveBusy() { return TestBit(b1_, 1); } + bool IsValveDriveBusy() { return TestBit(b1_, 2); } + bool IsSyntaxError() { return TestBit(b1_, 3); } + bool IsInstrumentError() { return TestBit(b1_, 4); } +}; + + +class ValveErrorRequest : public NormalCommand +{ + char b1_; + int index_; + +protected: + virtual std::string GetCommandString() { return "E2"; } + virtual int ParseContent(const std::string& content) + { + // The answer is always four bytes long + // On an MVP device, B0, B2 and B3 are 0x50 + // On a PSD/2 device, only B2 and B3 are 0x50 + // On a microlab 600, B and D are the valve values + if (content.size() != 4) + return ERR_UNEXPECTED_RESPONSE; + + b1_ = content[index_]; + + /* + if (content[0] != 0x50) + return ERR_UNEXPECTED_RESPONSE; + if (content[2] != 0x50) + return ERR_UNEXPECTED_RESPONSE; + if (content[3] != 0x50) + return ERR_UNEXPECTED_RESPONSE; + + if (TestBit(b1_, 3)) + return ERR_UNEXPECTED_RESPONSE; + if (TestBit(b1_, 4)) + return ERR_UNEXPECTED_RESPONSE; + if (TestBit(b1_, 5)) + return ERR_UNEXPECTED_RESPONSE; + if (!TestBit(b1_, 6)) + return ERR_UNEXPECTED_RESPONSE; + */ + return DEVICE_OK; + } + +public: + ValveErrorRequest(char address, int byte_index) : + NormalCommand(address), + b1_(0), + index_(byte_index) + {} + + bool IsValveNotInitialized() { return TestBit(b1_, 0); } + bool IsValveInitializationError() { return TestBit(b1_, 1); } + bool IsValveOverloadError() { return TestBit(b1_, 2); } + bool IsValveExists() { return !TestBit(b1_, 4); } // This on the MicroLab 600 +}; + +class SyringeErrorRequest : public NormalCommand +{ + char b1_; + int index_; + +protected: + virtual std::string GetCommandString() { return "E2"; } + virtual int ParseContent(const std::string& content) + { + // The answer is always four bytes long + // On an MVP device, B0, B2 and B3 are 0x50 + // On a PSD/2 device, only B2 and B3 are 0x50 + // On a microlab 600, B and D are the valve values + if (content.size() != 4) + return ERR_UNEXPECTED_RESPONSE; + + b1_ = content[index_]; + + /* + if (content[0] != 0x50) + return ERR_UNEXPECTED_RESPONSE; + if (content[2] != 0x50) + return ERR_UNEXPECTED_RESPONSE; + if (content[3] != 0x50) + return ERR_UNEXPECTED_RESPONSE; + + if (TestBit(b1_, 3)) + return ERR_UNEXPECTED_RESPONSE; + if (TestBit(b1_, 4)) + return ERR_UNEXPECTED_RESPONSE; + if (TestBit(b1_, 5)) + return ERR_UNEXPECTED_RESPONSE; + if (!TestBit(b1_, 6)) + return ERR_UNEXPECTED_RESPONSE; + */ + return DEVICE_OK; + } + +public: + SyringeErrorRequest(char address, int byte_index) : + NormalCommand(address), + b1_(0), + index_(byte_index) + {} + + bool IsSyringeNotInitialized() { return TestBit(b1_, 0); } + bool IsStrokeTooLarge() { return TestBit(b1_, 1); } // This on the MicroLab 600 + bool IsSyringeInitializationError() { return TestBit(b1_, 2); } + bool IsSyringeOverloadError() { return TestBit(b1_, 3); } + bool IsSyringeExists() { return !TestBit(b1_, 4); } // This on the MicroLab 600 +}; + + +class MiscellaneousDeviceStatusRequest : public NormalCommand +{ + char b1_; + +protected: + virtual std::string GetCommandString() { return "E3"; } + virtual int ParseContent(const std::string& content) + { + if (content.size() != 1) + return ERR_UNEXPECTED_RESPONSE; + + b1_ = content[0]; + if (TestBit(b1_, 5)) + return ERR_UNEXPECTED_RESPONSE; + if (!TestBit(b1_, 6)) + return ERR_UNEXPECTED_RESPONSE; + return DEVICE_OK; + } + +public: + MiscellaneousDeviceStatusRequest(char address) : + NormalCommand(address), + b1_(0) + {} + + bool IsTimerBusy() { return TestBit(b1_, 0); } + bool IsDiagnosticModeBusy() { return TestBit(b1_, 1); } + bool IsEEPROMBusy() { return TestBit(b1_, 2); } + bool IsI2CBusError() { return TestBit(b1_, 3); } + bool IsOverTemperatureError() { return TestBit(b1_, 4); } +}; + + +class MovementFinishedRequest : public NormalCommand +{ + char response_; + +protected: + virtual std::string GetCommandString() { return "F"; } + virtual int ParseContent(const std::string& content) + { + if (content.size() != 1) + return ERR_UNEXPECTED_RESPONSE; + response_ = content[0]; + switch (response_) + { + case 'N': + case 'Y': + case '*': + return DEVICE_OK; + default: + return ERR_UNEXPECTED_RESPONSE; + } + } + +public: + MovementFinishedRequest(char address) : + NormalCommand(address), + response_('\0') + {} + + bool IsReceivedCommandButNotExecuted() { return response_ == 'N'; } + bool IsValveDriveBusy() { return response_ == '*'; } + bool IsMovementFinished() { return response_ == 'Y'; } +}; + + +class ValveOverloadedRequest : public NormalCommand +{ + char response_; + +protected: + virtual std::string GetCommandString() { return "G"; } + virtual int ParseContent(const std::string& content) + { + if (content.size() != 1) + return ERR_UNEXPECTED_RESPONSE; + response_ = content[0]; + switch (response_) + { + case 'N': + case 'Y': + case '*': + return DEVICE_OK; + default: + return ERR_UNEXPECTED_RESPONSE; + } + } + +public: + ValveOverloadedRequest(char address) : + NormalCommand(address), + response_('\0') + {} + + bool IsValveOverload() { return response_ == 'Y'; } + bool IsValveDriveBusy() { return response_ == '*'; } + bool IsNoError() { return response_ == 'N'; } +}; + + +class InstrumentConfigurationRequest : public NormalCommand +{ + char response_; + +protected: + virtual std::string GetCommandString() { return "H"; } + virtual int ParseContent(const std::string& content) + { + if (content.size() != 1) + return ERR_UNEXPECTED_RESPONSE; + response_ = content[0]; + switch (response_) + { + case 'N': + case 'Y': + case '*': + return DEVICE_OK; + default: + return ERR_UNEXPECTED_RESPONSE; + } + } + +public: + InstrumentConfigurationRequest(char address) : + NormalCommand(address), + response_('\0') + {} + + bool IsOneSyringeOneValve() { return response_ == 'Y'; } + bool IsBusy() { return response_ == '*'; } +}; + + +class SyringeOverloadedRequest : public NormalCommand +{ + char response_; + +protected: + virtual std::string GetCommandString() { return "Z"; } + virtual int ParseContent(const std::string& content) + { + if (content.size() != 1) + return ERR_UNEXPECTED_RESPONSE; + response_ = content[0]; + switch (response_) + { + case 'N': + case 'Y': + case '*': + return DEVICE_OK; + default: + return ERR_UNEXPECTED_RESPONSE; + } + } + +public: + SyringeOverloadedRequest(char address) : + NormalCommand(address), + response_('\0') + {} + + bool IsSyringeOverload() { return response_ == 'Y'; } + bool IsSyringeDriveBusy() { return response_ == '*'; } + bool IsNoError() { return response_ == 'N'; } +}; + + +class ValvePositionRequest : public NormalCommand +{ + int positionOneBased_; + +protected: + virtual std::string GetCommandString() { return "LQP"; } + virtual int ParseContent(const std::string& content) + { return ParseDecimal(content, 2, positionOneBased_); } + +public: + ValvePositionRequest(char address) : + NormalCommand(address), + positionOneBased_(0) + {} + + long GetPosition() { return long(positionOneBased_ - 1); } +}; + + +class ValveAngleRequest : public NormalCommand +{ + int angle_; + +protected: + virtual std::string GetCommandString() { return "LQA"; } + virtual int ParseContent(const std::string& content) + { return ParseDecimal(content, 3, angle_); } + +public: + ValveAngleRequest(char address) : + NormalCommand(address), + angle_(-1) + {} + + int GetAngle() { return angle_; } // 0-359 +}; + + +class ValveTypeRequest : public NormalCommand +{ +private: + int type_; + +protected: + virtual std::string GetCommandString() { return "LQT"; } + virtual int ParseContent(const std::string& content) + { return ParseDecimal(content, 1, type_); } + +public: + ValveTypeRequest(char address) : + NormalCommand(address), + type_(ValveTypeUnknown) + {} + + MVPValveType GetValveType() { return MVPValveType(type_); } +}; + + +class ValveSpeedRequest : public NormalCommand +{ + int speed_; + +protected: + virtual std::string GetCommandString() { return "LQF"; } + virtual int ParseContent(const std::string& content) + { return ParseDecimal(content, 1, speed_); } + +public: + ValveSpeedRequest(char address) : + NormalCommand(address), + speed_(-1) + {} + + int GetSpeedHz() + { + switch (speed_) + { + case 0: return 30; + case 1: return 40; + case 2: return 50; + case 3: return 60; + case 4: return 70; + case 5: return 80; + case 6: return 90; + case 7: return 100; + case 8: return 110; + case 9: return 120; + default: return 0; + } + } +}; + + +class SyringeSpeedRequest : public NormalCommand +{ + int speed_; + +protected: + virtual std::string GetCommandString() { return "YQS"; } + virtual int ParseContent(const std::string& content) + { return ParseDecimal(content, 4, speed_); } + +public: + SyringeSpeedRequest(char address) : + NormalCommand(address), + speed_(-1) + {} + + // The number of seconds to move the whole stroke + int GetSpeedSeconds() + { + return speed_; + } +}; + + +class SyringeReturnStepsRequest : public NormalCommand +{ + int return_steps_; + +protected: + virtual std::string GetCommandString() { return "YQN"; } + virtual int ParseContent(const std::string& content) + { return ParseDecimal(content, 4, return_steps_); } + +public: + SyringeReturnStepsRequest(char address) : + NormalCommand(address), + return_steps_(-1) + {} + + //THe number of seconds to move the whole stroke + int GetSteps() + { + return return_steps_; + } +}; + + +class SyringeBackOffStepsRequest : public NormalCommand +{ + int back_off_steps_; + +protected: + virtual std::string GetCommandString() { return "YQB"; } + virtual int ParseContent(const std::string& content) + { return ParseDecimal(content, 4, back_off_steps_); } + +public: + SyringeBackOffStepsRequest(char address) : + NormalCommand(address), + back_off_steps_(-1) + {} + + // Syringe Back-off Steps Request/Response + int GetSteps() + { + return back_off_steps_; + } +}; + + +class SyringeResolutionRequest : public NormalCommand +{ + int resolution_; + +protected: + virtual std::string GetCommandString() { return "YQM"; } + virtual int ParseContent(const std::string& content) + { return ParseDecimal(content, 1, resolution_); } + +public: + SyringeResolutionRequest(char address) : + NormalCommand(address), + resolution_(-1) + {} + + // Syringe Resolution Request/Response + // x = 0 Half resolution + // x = 1 Full resolution + // x = 2 Full resolution, overload detection disabled + int GetResolution() + { + return resolution_; + } +}; + + +class SyringePositionRequest : public NormalCommand +{ + int position_; + +protected: + virtual std::string GetCommandString() { return "YQP"; } + virtual int ParseContent(const std::string& content) + { return ParseDecimal(content, 4, position_); } + +public: + SyringePositionRequest(char address) : + NormalCommand(address), + position_(-1) + {} + + // The position of the stroke in steps + int GetPosition() + { + return position_; + } +}; + + +class FirmwareVersionRequest : public NormalCommand +{ + std::string version_; + +protected: + virtual std::string GetCommandString() { return "U"; } + virtual int ParseContent(const std::string& content) + { + if (content.empty()) + return ERR_UNEXPECTED_RESPONSE; + version_ = content; + return DEVICE_OK; + } + +public: + FirmwareVersionRequest(char address) : + NormalCommand(address) + {} + + std::string GetFirmwareVersion() { return version_; } +}; diff --git a/DeviceAdapters/HamiltonRNO/unittest/Makefile.am b/DeviceAdapters/HamiltonRNO/unittest/Makefile.am new file mode 100755 index 000000000..1d39d0edc --- /dev/null +++ b/DeviceAdapters/HamiltonRNO/unittest/Makefile.am @@ -0,0 +1,7 @@ +check_PROGRAMS = \ + RNO-Tests +AM_DEFAULT_SOURCE_EXT = .cpp +AM_CPPFLAGS = $(GMOCK_CPPFLAGS) -I.. +AM_CXXFLAGS = $(MMDEVAPI_CXXFLAGS) +LDADD = ../../../testing/libgmock.la $(MMDEVAPI_LIBADD) +TESTS = $(check_PROGRAMS) diff --git a/DeviceAdapters/HamiltonRNO/unittest/RNO-Tests.cpp b/DeviceAdapters/HamiltonRNO/unittest/RNO-Tests.cpp new file mode 100755 index 000000000..72ff2e79f --- /dev/null +++ b/DeviceAdapters/HamiltonRNO/unittest/RNO-Tests.cpp @@ -0,0 +1,172 @@ +/* + * Micro-Manager device adapter for Hamilton devices that use the RNO protocol + * + * Author: Mark A. Tsuchida for the original MVP code + * Egor Zindy for the PSD additions + * + * Copyright (C) 2018 Open Imaging, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "HamiltonMVP.h" +#include "HamiltonPSD.h" + +#include + + +struct RotationAngleParams +{ + MVPValveType valveType; + bool ccw; + int startPos; + int destPos; + + RotationAngleParams() {} + + RotationAngleParams(MVPValveType vt, bool ccw, int start, int dest) : + valveType(vt), + ccw(ccw), + startPos(start), + destPos(dest) + {} + + friend std::ostream& operator<<(std::ostream& os, const RotationAngleParams& params); +}; + +std::ostream& operator<<(std::ostream& os, const RotationAngleParams& params) +{ + os << '[' << GetValveTypeName(params.valveType) << ", " << + (params.ccw ? "CCW" : "CW") << ", " << + params.startPos << "->" << params.destPos << ']'; + return os; +} + + +class ParameterizedValveRotationAngleTest : public ::testing::Test, + public ::testing::WithParamInterface< std::pair > +{ +protected: + RotationAngleParams params_; + int expected_; + + virtual void SetUp() + { + params_ = GetParam().first; + expected_ = GetParam().second; + } +}; + +TEST_P(ParameterizedValveRotationAngleTest, MatchWithExpected) +{ + ASSERT_EQ(expected_, GetValveRotationAngle(params_.valveType, + params_.ccw, params_.startPos, params_.destPos)); +} + +INSTANTIATE_TEST_CASE_P(BasicTestCase, ParameterizedValveRotationAngleTest, + ::testing::Values( + std::make_pair(RotationAngleParams( + ValveType2Ports90DegreesApart, false, 0, 0), 0), + std::make_pair(RotationAngleParams( + ValveType2Ports90DegreesApart, true, 0, 0), 0), + std::make_pair(RotationAngleParams( + ValveType2Ports90DegreesApart, false, 0, 1), 90), + std::make_pair(RotationAngleParams( + ValveType2Ports90DegreesApart, true, 0, 1), 270), + std::make_pair(RotationAngleParams( + ValveType2Ports90DegreesApart, false, 1, 0), 270), + std::make_pair(RotationAngleParams( + ValveType2Ports90DegreesApart, true, 1, 0), 90), + std::make_pair(RotationAngleParams( + ValveType2Ports90DegreesApart, false, 1, 1), 0), + std::make_pair(RotationAngleParams( + ValveType2Ports90DegreesApart, true, 1, 1), 0), + + std::make_pair(RotationAngleParams( + ValveType3Ports, false, 0, 0), 0), + std::make_pair(RotationAngleParams( + ValveType3Ports, true, 0, 0), 0), + std::make_pair(RotationAngleParams( + ValveType3Ports, false, 0, 1), 90), + std::make_pair(RotationAngleParams( + ValveType3Ports, true, 0, 1), 270), + std::make_pair(RotationAngleParams( + ValveType3Ports, false, 0, 2), 180), + std::make_pair(RotationAngleParams( + ValveType3Ports, true, 0, 2), 180), + std::make_pair(RotationAngleParams( + ValveType3Ports, false, 1, 0), 270), + std::make_pair(RotationAngleParams( + ValveType3Ports, true, 1, 0), 90), + std::make_pair(RotationAngleParams( + ValveType3Ports, false, 1, 1), 0), + std::make_pair(RotationAngleParams( + ValveType3Ports, true, 1, 1), 0), + std::make_pair(RotationAngleParams( + ValveType3Ports, false, 1, 2), 90), + std::make_pair(RotationAngleParams( + ValveType3Ports, true, 1, 2), 270), + std::make_pair(RotationAngleParams( + ValveType3Ports, false, 2, 0), 180), + std::make_pair(RotationAngleParams( + ValveType3Ports, true, 2, 0), 180), + std::make_pair(RotationAngleParams( + ValveType3Ports, false, 2, 1), 270), + std::make_pair(RotationAngleParams( + ValveType3Ports, true, 2, 1), 90), + std::make_pair(RotationAngleParams( + ValveType3Ports, false, 2, 2), 0), + std::make_pair(RotationAngleParams( + ValveType3Ports, true, 2, 2), 0), + + std::make_pair(RotationAngleParams( + ValveType8Ports, false, 0, 0), 0), + std::make_pair(RotationAngleParams( + ValveType8Ports, false, 0, 1), 45), + std::make_pair(RotationAngleParams( + ValveType8Ports, false, 0, 2), 90), + std::make_pair(RotationAngleParams( + ValveType8Ports, false, 1, 2), 45), + + std::make_pair(RotationAngleParams( + ValveType8Ports, true, 0, 0), 0), + std::make_pair(RotationAngleParams( + ValveType8Ports, true, 0, 1), 315), + std::make_pair(RotationAngleParams( + ValveType8Ports, true, 0, 2), 270), + std::make_pair(RotationAngleParams( + ValveType8Ports, true, 1, 2), 315) + )); + + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}