diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index df50af9e5a57808a5f3594e3c9bddce046e991d4..fb628f78a6c5523ff07fc9e94975c5d22f9a316e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,7 +23,7 @@ stages: stage: package # make a docker daemon available for cibuildwheel to use tags: - - docker-privileged + - docker-privileged-xl services: - name: docker:dind entrypoint: ["env", "-u", "DOCKER_HOST"] diff --git a/src/libChiller/CMakeLists.txt b/src/libChiller/CMakeLists.txt index 071c840eb0057340f9568927a8b864fa3b183deb..d0b8a15907e5c970b6dbfc1253d4860645b2c731 100644 --- a/src/libChiller/CMakeLists.txt +++ b/src/libChiller/CMakeLists.txt @@ -6,6 +6,7 @@ target_sources(Chiller HuberChiller.cpp SPSRC211.cpp PolySciLM.cpp + Julabo.cpp HuberComGate.cpp ) target_link_libraries(Chiller PRIVATE Com Utils) diff --git a/src/libChiller/IChiller.cpp b/src/libChiller/IChiller.cpp index 4a839c35b69769222ea80b711284c51d7d64670e..49b2bdfe331131bc00e7e3961e38fe5e8e578462 100644 --- a/src/libChiller/IChiller.cpp +++ b/src/libChiller/IChiller.cpp @@ -26,6 +26,16 @@ float IChiller::getRampRate() { return 0.0; } +float IChiller::measurePressure() { + logger(logWARNING) << "measurePressure is not implemented in this chiller!"; + return 0.0; +} + +float IChiller::measureFlow() { + logger(logWARNING) << "measureFlow is not implemented in this chiller!"; + return 0.0; +} + float IChiller::ctof(float t) { return (t * 9.0 / 5.0) + 32.0; } float IChiller::ftoc(float t) { return (t - 32.0) * 5.0 / 9.0; } diff --git a/src/libChiller/IChiller.h b/src/libChiller/IChiller.h index 52741e972a7e512c8312da002da9c9c77905e420..1ec9f165ad89ff7c38b8ca1d481fca5f3178db37 100644 --- a/src/libChiller/IChiller.h +++ b/src/libChiller/IChiller.h @@ -53,6 +53,10 @@ class IChiller { virtual void setRampRate(float RR); //! Return the ramp rate that the chiller is set to in degree/minute virtual float getRampRate(); + //! Return the current pressure of the chiller in PSI + virtual float measurePressure(); + //! Return chiller flow in LPM + virtual float measureFlow(); //! Set the target temperature of the chiller /** * \param temp The target temperature in Celsius diff --git a/src/libChiller/Julabo.cpp b/src/libChiller/Julabo.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4293662cc161a0b8f2a88f07433703615e2f0be9 --- /dev/null +++ b/src/libChiller/Julabo.cpp @@ -0,0 +1,59 @@ +#include "Julabo.h" + +#include "ChillerRegistry.h" +REGISTER_CHILLER(Julabo) + +Julabo::Julabo(const std::string& name) : IChiller(name, {"Julabo"}) {} + +void Julabo::init() {} + +void Julabo::turnOn() { m_com->send("out_mode_05 1"); } + +void Julabo::turnOff() { m_com->send("out_mode_05 0"); } + +void Julabo::setTargetTemperature(float temp) { + std::string cmd = "out_sp_00"; + std::stringstream ss; + ss << temp; + cmd.append(" " + ss.str()); + m_com->send(cmd); +} + +float Julabo::getTargetTemperature() { + std::string response = m_com->sendreceive("in_sp_00"); + float temp; + if (response.substr(0, 1) == "-") + temp = -std::stof(response.substr(1)); + else + temp = std::stof(response); + return temp; +} + +float Julabo::measureTemperature() { + std::string response = m_com->sendreceive("in_pv_00"); + float temp; + if (response.substr(0, 1) == "-") + temp = -std::stof(response.substr(1)); + else + temp = std::stof(response); + return temp; +} + +bool Julabo::getStatus() { + std::string response = m_com->sendreceive("status"); + // Return codes: + // 01 MANUAL START --> chiller is on and can be operated manually + // 02 MANUAL STOP --> chiller is off and can be operated manually + // 03 REMOTE START --> chiller is on and can be operated remotely + // 04 REMOTE STOP --> chiller is off and can be operated remotely + if (response.find("START") != std::string::npos) + return true; + else if (response.find("STOP") != std::string::npos) + return false; + else { + logger(logDEBUG) << __PRETTY_FUNCTION__ + << " -> Unexpected response: " << response; + throw std::runtime_error("Unexpected response from chiller"); + return false; + } +} diff --git a/src/libChiller/Julabo.h b/src/libChiller/Julabo.h new file mode 100644 index 0000000000000000000000000000000000000000..cf49f90b0f1083fa663cb541f63bd1ddee2bd870 --- /dev/null +++ b/src/libChiller/Julabo.h @@ -0,0 +1,67 @@ +#ifndef Julabo_H +#define Julabo_H + +#include <memory> +#include <stdexcept> +#include <string> + +#include "IChiller.h" +#include "Logger.h" +#include "TextSerialCom.h" + +//! \brief Object to interface with a Julabo series chiller +/** + * # Example configuration for a Julabo Chiller: + * + * { + * "name": "myJulaboChiller", + * "hw-type": "Chiller", + * "hw-model": "Julabo", + * "communication": { + * "protocol": "TextSerialCom", + * "termination": "\r", + * "returnTermination": "\n", + * "baudrate": "B4800", + * "port": "/dev/ttyUSB0", + * "charsize": "CS7", + * "parityBit": false, + * "flowControl": true + * } + * } + * + * The operator's manual for these chillers can be found at + * https://www.julabo.com/en-us/products/recirculating-coolers/fl-recirculating-coolers/fl1201 + */ +class Julabo : public IChiller { + public: + /** + * \param com The serial communication interface connected + * to the chiller + */ + Julabo(const std::string& name); + ~Julabo() = default; + + //! Initialize the serial communication channel + void init(); + //! Turn the chiller on + void turnOn(); + //! Turn the chiller off + void turnOff(); + //! Set the target temperature of the chiller + /** + * \param temp The target temperature in Celsius + */ + void setTargetTemperature(float temp); + //! Return the temperature that the chiller is set to in Celsius + float getTargetTemperature(); + //! Return the current temperature of the chiller in Celsius + float measureTemperature(); + //! Get the status of the chiller + /** + * \return true if chiller is in "run" mode, and false if + * it's in "standby" mode + */ + bool getStatus(); +}; + +#endif diff --git a/src/libChiller/python.cpp b/src/libChiller/python.cpp index 49219ea453168e27d3d72b00ce830dc901482414..5fdaeecac6ddef4ee6eab7b77ec393c19fb17621 100644 --- a/src/libChiller/python.cpp +++ b/src/libChiller/python.cpp @@ -7,6 +7,7 @@ namespace py = pybind11; #include "HuberChiller.h" #include "IChiller.h" +#include "Julabo.h" #include "PolySciLM.h" class PyIChiller : public IChiller { @@ -53,6 +54,8 @@ void register_chiller(py::module& m) { .def("setTargetTemperature", &IChiller::setTargetTemperature) .def("getTargetTemperature", &IChiller::getTargetTemperature) .def("measureTemperature", &IChiller::measureTemperature) + .def("measurePressure", &IChiller::measurePressure) + .def("measureFlow", &IChiller::measureFlow) .def("getStatus", &IChiller::getStatus); py::class_<HuberChiller, IChiller, std::shared_ptr<HuberChiller>>( @@ -65,6 +68,8 @@ void register_chiller(py::module& m) { .def("setTargetTemperature", &HuberChiller::setTargetTemperature) .def("getTargetTemperature", &HuberChiller::getTargetTemperature) .def("measureTemperature", &HuberChiller::measureTemperature) + .def("measurePressure", &HuberChiller::measurePressure) + .def("measureFlow", &HuberChiller::measureFlow) .def("getStatus", &HuberChiller::getStatus) .def("getFaultStatus", &HuberChiller::getFaultStatus); @@ -80,4 +85,16 @@ void register_chiller(py::module& m) { .def("measureFlow", &PolySciLM::measureFlow) .def("getStatus", &PolySciLM::getStatus) .def("getFaultStatus", &PolySciLM::getFaultStatus); + + py::class_<Julabo, IChiller, std::shared_ptr<Julabo>>(m, "Julabo") + .def(py::init<const std::string&>()) + .def("init", &Julabo::init) + .def("turnOn", &Julabo::turnOn) + .def("turnOff", &Julabo::turnOff) + .def("setTargetTemperature", &Julabo::setTargetTemperature) + .def("getTargetTemperature", &Julabo::getTargetTemperature) + .def("measureTemperature", &Julabo::measureTemperature) + .def("measurePressure", &Julabo::measurePressure) + .def("measureFlow", &Julabo::measureFlow) + .def("getStatus", &Julabo::getStatus); } diff --git a/src/libCom/SerialCom.cpp b/src/libCom/SerialCom.cpp index 3a4aad9d089d3ac9fc3413eff8a5febeac0f302f..c7830605426c4f300f6ec6fdde02ef3ec07835e7 100644 --- a/src/libCom/SerialCom.cpp +++ b/src/libCom/SerialCom.cpp @@ -6,6 +6,7 @@ #include <termios.h> #include <unistd.h> +#include <bitset> #include <cerrno> #include <cstring> #include <stdexcept> @@ -210,9 +211,31 @@ std::string SerialCom::receive() { ScopeLock lock(this); int n_read = ::read(m_dev, m_tmpbuf, MAX_READ); - if (n_read >= 0) - return std::string(m_tmpbuf, n_read); - else + if (n_read >= 0) { + std::string response = std::string(m_tmpbuf, n_read); + + // Convert response if communication uses charsize different from 8 + if (m_charsize != mapCHARSIZE.at("CS8")) { + std::string decoded_response; + for (size_t i = 0; i < response.length(); i++) { + unsigned char original = + static_cast<unsigned char>(response[i]); + unsigned char fixed; + if (m_charsize == mapCHARSIZE.at("CS7")) + fixed = original & 0x7F; + else if (m_charsize == mapCHARSIZE.at("CS6")) + fixed = original & 0x6F; + else if (m_charsize == mapCHARSIZE.at("CS5")) + fixed = original & 0x5F; + else + throw std::runtime_error( + "CharSize not recognized! Unable to decode response"); + decoded_response.push_back(fixed); + } + response = decoded_response; + } + return response; + } else throw std::runtime_error("Error reading from " + m_port + ": " + std::strerror(errno)); } diff --git a/src/libDevCom/ChecksumException.h b/src/libDevCom/ChecksumException.h index 94eb6d109702b6c2a24677c181fa131c06783030..07e578a36b23970abc3c7d53b909e92a9b8b8d91 100644 --- a/src/libDevCom/ChecksumException.h +++ b/src/libDevCom/ChecksumException.h @@ -1,6 +1,8 @@ #ifndef CHECKSUMEXCEPTION_H #define CHECKSUMEXCEPTION_H +#include <stdint.h> + #include <iostream> #include "ComException.h" diff --git a/src/libDevCom/OutOfRangeException.h b/src/libDevCom/OutOfRangeException.h index 5a6ac1105eeaa981369af79f7173320f1c22f5b6..e2b35b45bf803082675362b6b6181e74a0ad79a6 100644 --- a/src/libDevCom/OutOfRangeException.h +++ b/src/libDevCom/OutOfRangeException.h @@ -1,6 +1,8 @@ #ifndef OUTOFRANGEEXCEPTION_H #define OUTOFRANGEEXCEPTION_H +#include <stdint.h> + #include <iostream> #include "ComException.h" diff --git a/src/libMeter/CMakeLists.txt b/src/libMeter/CMakeLists.txt index e11d2151169264f7459e1d5d1738f4b00658d9e6..3629ce973f610677f45e3d85aa8082c3bd004ba7 100644 --- a/src/libMeter/CMakeLists.txt +++ b/src/libMeter/CMakeLists.txt @@ -6,10 +6,12 @@ target_sources(Meter Keithley2000.cpp Keithley199.cpp Fluke8842.cpp + Fluke45.cpp HP3478A.cpp PM6680.cpp DMM6500.cpp KeysightDAQ970A.cpp + RigolDM30XX.cpp ) target_link_libraries(Meter PRIVATE Com Utils) target_include_directories(Meter PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/libMeter/Fluke45.cpp b/src/libMeter/Fluke45.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e5b36e84698da80258fb3abd9668fa86a3a6d081 --- /dev/null +++ b/src/libMeter/Fluke45.cpp @@ -0,0 +1,165 @@ +#include "Fluke45.h" + +#include "IMeter.h" +#include "Logger.h" +#include "MeterRegistry.h" +#include "ScopeLock.h" +REGISTER_METER(Fluke45) + +Fluke45::Fluke45(const std::string& name) : IMeter(name, {"Fluke45"}) {} + +bool Fluke45::ping(unsigned dev) { + std::string result = ""; + if (dev == 0) { + logger(logDEBUG) << "ping the multimeter....."; + result = m_com->sendreceive("*IDN?\n"); + } else { + throw std::runtime_error("Other channels not implemented! "); + } + return !result.empty(); +} + +std::string Fluke45::identify() { + std::string idn = this->sendreceive("*IDN?"); + return idn; +} + +void Fluke45::reset() { + logger(logDEBUG) << __PRETTY_FUNCTION__ << " -> Initialising: "; + this->send("*RST"); +} + +std::string Fluke45::GetMode() { return this->sendreceive("FUNC1?"); } + +void Fluke45::send(std::string cmd) { + ScopeLock lock(m_com); + m_com->send("*CLS"); + logger(logDEBUG2) << __PRETTY_FUNCTION__ << " -> Sending: " << cmd; + cmd += "\r\n"; + m_com->send(cmd); + std::this_thread::sleep_for(std::chrono::milliseconds(m_wait)); +} + +std::string Fluke45::sendreceive(std::string cmd) { + ScopeLock lock(m_com); + m_com->send("*CLS"); + logger(logDEBUG2) << __PRETTY_FUNCTION__ << " -> Sending: " << cmd; + cmd += "\r\n"; + m_com->send(cmd); + m_com->send("++read eoi\n\r"); + std::this_thread::sleep_for(std::chrono::milliseconds(m_wait)); + std::string buf = m_com->receive(); + logger(logDEBUG2) << __PRETTY_FUNCTION__ << " -> Received: " << buf; + return buf; +} + +std::string Fluke45::GetValue() { return this->sendreceive("VAL1?"); } + +double Fluke45::measureDCV(unsigned channel) { + ScopeLock lock(m_com); + std::string CurrentMode = this->GetMode(); + if (CurrentMode != "VDC") { + logger(logDEBUG2) << "Current Mode is " + CurrentMode + + ", Reset the meter to VDC"; + this->reset(); + this->SetMode(Fluke45Mode::VOLTAGEDC); + } + + if (channel > 0) // use scanner card; not implemented for Fluke 45 + std::cerr << "Unimplemented channel number: " << channel << std::endl; + + return std::stod(this->GetValue()); +} + +double Fluke45::measureDCI(unsigned channel) { + ScopeLock lock(m_com); + std::string CurrentMode = this->GetMode(); + if (CurrentMode != "ADC") { + logger(logDEBUG2) << "Current Mode is " + CurrentMode + + ", Reset the meter to ADC"; + this->reset(); + this->SetMode(Fluke45Mode::CURRENTDC); + } + + if (channel > 0) // use scanner card; not implemented for Fluke 45 + std::cerr << "Unimplemented channel number: " << channel << std::endl; + + std::string val = this->GetValue(); + return std::stod(val); +} + +double Fluke45::measureRES(unsigned channel, bool use4w) { + ScopeLock lock(m_com); + std::string CurrentMode = this->GetMode(); + if (CurrentMode != "OHMS") { + logger(logDEBUG2) << "Current Mode is " + CurrentMode + + ", Reset the meter to OHMS"; + this->reset(); + this->SetMode(Fluke45Mode::OHMS); + } + + if (channel > 0) // use scanner card; not implemented for Fluke 45 + std::cerr << "Unimplemented channel number: " << channel << std::endl; + + std::string val = this->GetValue(); + return std::stod(val); +} + +double Fluke45::measureCAP(unsigned channel) { + std::cerr << "Unable to measure capacitance by Fluke 45, exit. " + << std::endl; +} + +void Fluke45::SetMode(enum Fluke45Mode mode) { + switch (mode) { + case Fluke45Mode::VOLTAGEDC: + this->send("VDC; AUTO;"); + break; + case Fluke45Mode::VOLTAGEAC: + this->send("VAC; AUTO;"); + break; + case Fluke45Mode::CURRENTDC: + this->send("ADC; AUTO;"); + break; + case Fluke45Mode::CURRENTAC: + this->send("AAC; AUTO;"); + break; + case Fluke45Mode::OHMS: + this->send("OHMS; AUTO;"); + break; + default: + logger(logERROR) << __PRETTY_FUNCTION__ << " : Unknown mode!"; + break; + } +} + +void Fluke45::checkCompatibilityList() { + // get model connected to the meter + std::string idn = this->identify(); + + // get list of models + std::vector<std::string> models = IMeter::getListOfModels(); + + if (models.empty()) { + logger(logINFO) << "No model identifier implemented for this meter. No " + "check is performed."; + return; + } + + std::size_t pos = m_name.find("Fluke"); + std::string brand = m_name.substr(pos, pos + 5); + std::string type = m_name.substr(pos + 5, pos + 2); + + for (int i = 0; i < brand.length(); i++) { + brand[i] = toupper(brand[i]); + } + + for (const std::string& model : models) { + if (idn.find(brand) != std::string::npos && + idn.find(type) != std::string::npos) + return; + } + + logger(logERROR) << "Unknown meter: " << idn; + throw std::runtime_error("Unknown meter: " + idn); +} diff --git a/src/libMeter/Fluke45.h b/src/libMeter/Fluke45.h new file mode 100644 index 0000000000000000000000000000000000000000..1df0ec235dc0804ff4de9fba01de98ff281fef2f --- /dev/null +++ b/src/libMeter/Fluke45.h @@ -0,0 +1,67 @@ +#ifndef Fluke45_H +#define Fluke45_H +#include <chrono> +#include <iostream> +#include <string> +#include <thread> + +#include "IMeter.h" + +/* + Fluke45 multimeter + Author: Haoran Zhao & Emily Thompson + Date: Feb 2023 + Reference 1: + Fluke 45 User Manual + https://www.testequipmentdepot.com/usedequipment/pdf/45.pdf + Reference 2: + GPIB usb controller + https://prologix.biz/gpib-usb-controller.html +*/ + +enum class Fluke45Mode { VOLTAGEDC, VOLTAGEAC, CURRENTDC, CURRENTAC, OHMS }; + +class Fluke45 : public IMeter { + public: + // constructor + Fluke45(const std::string& name); + + // Reset the multimeter + void reset(); + + // Ping device + bool ping(unsigned dev = 0); + + // Ping device + std::string identify(); + + // Get the current measurement mode of the multimeter + std::string GetMode(); + + // Get value on the display + std::string GetValue(); + + // Set measurement mode (VDC, VAC, ADC, AAC) + void SetMode(enum Fluke45Mode); + + // Make measurements + double measureDCV(unsigned channel = 0); + double measureDCI(unsigned channel = 0); + double measureRES(unsigned channel = 0, bool use4w = false); + double measureCAP(unsigned channel = 0); + + // Check if device model is supported + void checkCompatibilityList(); + + private: + // Send command string to reader + void send(std::string cmd); + + // Send command string to meter and read the output + std::string sendreceive(std::string cmd); + + // Brief wait interval, could be used between two successive measurements + std::chrono::milliseconds m_wait{900}; +}; + +#endif diff --git a/src/libMeter/RigolDM30XX.cpp b/src/libMeter/RigolDM30XX.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e771c97a90090ee03363f03a59aa274dafc8ec8b --- /dev/null +++ b/src/libMeter/RigolDM30XX.cpp @@ -0,0 +1,87 @@ +#include "RigolDM30XX.h" + +#include "IMeter.h" +#include "Logger.h" +#include "MeterRegistry.h" +#include "ScopeLock.h" +#include "StringUtils.h" +REGISTER_METER(RigolDM30XX) + +RigolDM30XX::RigolDM30XX(const std::string& name) + : IMeter(name, {"DM3058", "DM3058E", "DM3068"}) {} + +bool RigolDM30XX::ping(unsigned /*dev*/) { + std::string result = ""; + logger(logDEBUG) << "ping the multimeter....."; + result = m_com->sendreceive("*IDN?"); + utils::rtrim(result); + if (result.empty()) { + throw std::runtime_error("Failed communication with the device"); + } else { + logger(logDEBUG) << result; + } + + return !result.empty(); +} + +std::string RigolDM30XX::identify() { + std::string idn = m_com->sendreceive("*IDN?"); + return idn; +} + +void RigolDM30XX::autowait() { + m_com->send("*OPC"); + int ESRValue = 0; + while ((ESRValue & 1) == 0) { + ESRValue = std::stoi(m_com->sendreceive("*ESR?")); + std::this_thread::sleep_for(std::chrono::milliseconds(m_wait)); + } +} + +void RigolDM30XX::send(const std::string& cmd) { + logger(logDEBUG) << __PRETTY_FUNCTION__ << " -> Sending: " << cmd; + m_com->send("*CLS"); + m_com->send(cmd); + this->autowait(); +} + +std::string RigolDM30XX::sendreceive(const std::string& cmd) { + m_com->send("*CLS"); + std::string buf = m_com->sendreceive(cmd); + this->autowait(); + logger(logDEBUG) << __PRETTY_FUNCTION__ << " -> Received: " << buf; + utils::rtrim(buf); + return buf; +} + +void RigolDM30XX::reset() { + logger(logDEBUG) << __PRETTY_FUNCTION__ << " -> Initialising: "; + this->send("*RST"); +} + +// measure DC voltage with high precision +// take average of 10 repeatings +double RigolDM30XX::measureDCV(unsigned channel) { + ScopeLock lock(m_com); + + return std::stod(this->sendreceive(":MEAS:VOLT:DC?")); +} + +// measure resistance (2W or 4W) +// take average of 10 repeatings +double RigolDM30XX::measureRES(unsigned channel, bool use4w) { + std::string n_func = "RES"; + if (use4w) n_func = "FRES"; + + ScopeLock lock(m_com); + + return std::stod(this->sendreceive(":MEAS:" + n_func + "?")); +} + +// measure DC current with high precision +// take average of 10 repeatings +double RigolDM30XX::measureDCI(unsigned channel) { + ScopeLock lock(m_com); + + return std::stod(this->sendreceive(":MEAS:CURR:DC?")); +} diff --git a/src/libMeter/RigolDM30XX.h b/src/libMeter/RigolDM30XX.h new file mode 100644 index 0000000000000000000000000000000000000000..d13e9a025e6fa0e78b94c11de189b3f44f3c85e5 --- /dev/null +++ b/src/libMeter/RigolDM30XX.h @@ -0,0 +1,55 @@ +#ifndef RigolDM30XX_H +#define RigolDM30XX_H +#include <chrono> +#include <iostream> +#include <string> +#include <thread> + +#include "IMeter.h" + +/* + RigolDM30XX single-channel multimeter + Author: Simon Koch + Date: Feb 2024 + Reference 1 (User Guide, DM3068): + https://www.rigol-uk.co.uk/pdf/Rigol-DM3068-User-Guide.pdf + Reference 2 (Programmers Guide, DM3058/3058E/3068): + https://beyondmeasure.rigoltech.com/acton/attachment/1579/f-003f/0/-/-/-/-/file.pdf + + Based on implementation of Keithley2000 + - channel argument included only for compatibility with IMeter interface +*/ +class RigolDM30XX : public IMeter { + public: + RigolDM30XX(const std::string& name); + + /** ping the device + */ + virtual bool ping(unsigned dev = 0); + + virtual std::string identify(); + + virtual void reset(); + + /** measure DC voltage (unit: V) + */ + virtual double measureDCV(unsigned channel = 0); + + /** measure DC current (unit: A) + */ + virtual double measureDCI(unsigned channel = 0); + + /* measure resistance (unit: Ohm) + */ + virtual double measureRES(unsigned channel = 0, bool use4w = false); + + private: + void send(const std::string& cmd); + + std::string sendreceive(const std::string& cmd); + void autowait(); + + std::chrono::milliseconds m_wait{10}; +}; + +#endif diff --git a/src/libMeter/python.cpp b/src/libMeter/python.cpp index 4af85efb1c9d21f2cd944d5aa2e82f6ea2f20f4a..21990e69ce91cdcfb2367ceade5c59ebca82ac13 100644 --- a/src/libMeter/python.cpp +++ b/src/libMeter/python.cpp @@ -9,11 +9,13 @@ // labRemote #include "DMM6500.h" +#include "Fluke45.h" #include "Fluke8842.h" #include "ICom.h" #include "IMeter.h" #include "Keithley2000.h" #include "KeysightDAQ970A.h" +#include "RigolDM30XX.h" namespace py = pybind11; @@ -152,6 +154,20 @@ void register_meter(py::module& m) { const std::vector<unsigned>&, bool)>( &Keithley2000::measureRES)); + // RigolDM30XX + py::class_<RigolDM30XX, IMeter, std::shared_ptr<RigolDM30XX>>(m, + "RigolDM30XX") + .def(py::init<const std::string&>()) + .def("ping", &RigolDM30XX::ping) + .def("identify", &RigolDM30XX::identify) + .def("reset", &RigolDM30XX::reset) + .def("measureDCV", static_cast<double (RigolDM30XX::*)(unsigned)>( + &RigolDM30XX::measureDCV)) + .def("measureDCI", static_cast<double (RigolDM30XX::*)(unsigned)>( + &RigolDM30XX::measureDCI)) + .def("measureRES", static_cast<double (RigolDM30XX::*)(unsigned, bool)>( + &RigolDM30XX::measureRES)); + // KeysightDAQ970A py::class_<KeysightDAQ970A, IMeter, std::shared_ptr<KeysightDAQ970A>> daq970a(m, "KeysightDAQ970A"); @@ -195,6 +211,19 @@ void register_meter(py::module& m) { .value("Maximum", KeysightDAQ970A::Statistic::Maximum) .value("PeakToPeak", KeysightDAQ970A::Statistic::PeakToPeak); + // Fluke45 + py::class_<Fluke45, IMeter, std::shared_ptr<Fluke45>>(m, "Fluke45") + .def(py::init<const std::string&>()) + .def("ping", &Fluke45::ping) + .def("identify", &Fluke45::identify) + .def("reset", &Fluke45::reset) + .def("measureDCV", + static_cast<double (Fluke45::*)(unsigned)>(&Fluke45::measureDCV)) + .def("measureDCI", + static_cast<double (Fluke45::*)(unsigned)>(&Fluke45::measureDCI)) + .def("measureRES", static_cast<double (Fluke45::*)(unsigned, bool)>( + &Fluke45::measureRES)); + // Fluke8842 py::class_<Fluke8842, IMeter, std::shared_ptr<Fluke8842>>(m, "Fluke8842") .def(py::init<const std::string&>()) diff --git a/src/libPS/CMakeLists.txt b/src/libPS/CMakeLists.txt index ecdf435fe8e58dfb324268fdfca1a1583e2385b5..94d8662ae16209fa0b56cbf3e3d98596360113f6 100644 --- a/src/libPS/CMakeLists.txt +++ b/src/libPS/CMakeLists.txt @@ -15,12 +15,16 @@ target_sources(PS DT54xxPs.cpp DT5471NPs.cpp DT5472NPs.cpp + DT8033NPs.cpp + IsegPs.cpp + IsegSHR20xxPs.cpp SorensenPs.cpp RigolDP832.cpp Keithley24XX.cpp Keithley22XX.cpp RS_HMP4040.cpp RS_HMP2020.cpp + RS_NGP804.cpp Tenma72133XX.cpp Tenma722XXX.cpp TTIPs.cpp diff --git a/src/libPS/DT5471NPs.cpp b/src/libPS/DT5471NPs.cpp index a883af092beb8b01f8c0c1ba5e85e4d6be1fba51..ae4962cd8b0f48d1592faaab02c91e8b8d42b944 100644 --- a/src/libPS/DT5471NPs.cpp +++ b/src/libPS/DT5471NPs.cpp @@ -11,4 +11,4 @@ REGISTER_POWERSUPPLY(DT5471NPs) DT5471NPs::DT5471NPs(const std::string& name) - : DT54xxPs(name, {"DT5471"}, Polarity::Negative, 51e-6) {} + : DT54xxPs(name, {"DT5471"}, IPowerSupply::Polarity::Negative, 51e-6) {} diff --git a/src/libPS/DT5472NPs.cpp b/src/libPS/DT5472NPs.cpp index d3edbb977fb4c98a28a984c4a8efb4a5094101b9..7bdc3e6a094115548b10c28ecc09f3ebcb1e1702 100644 --- a/src/libPS/DT5472NPs.cpp +++ b/src/libPS/DT5472NPs.cpp @@ -11,4 +11,4 @@ REGISTER_POWERSUPPLY(DT5472NPs) DT5472NPs::DT5472NPs(const std::string& name) - : DT54xxPs(name, {"DT5472"}, Polarity::Negative, 105e-6) {} + : DT54xxPs(name, {"DT5472"}, IPowerSupply::Polarity::Negative, 105e-6) {} diff --git a/src/libPS/DT54xxPs.cpp b/src/libPS/DT54xxPs.cpp index 10505b57fe34d2fafd548e8965402722340a180a..0e0e519ea192cc7a90ab085d7aad6ad4b697d126 100644 --- a/src/libPS/DT54xxPs.cpp +++ b/src/libPS/DT54xxPs.cpp @@ -12,8 +12,8 @@ REGISTER_POWERSUPPLY(DT54xxPs) DT54xxPs::DT54xxPs(const std::string& name, - const std::vector<std::string>& models, Polarity output, - double imaxl) + const std::vector<std::string>& models, + IPowerSupply::Polarity output, double imaxl) : IPowerSupply(name, models), m_output(output), m_imaxl(imaxl) {} void DT54xxPs::reset() { @@ -38,7 +38,7 @@ bool DT54xxPs::ping() { void DT54xxPs::checkCompatibilityList() { IPowerSupply::checkCompatibilityList(); - Polarity pol = polarity(); + IPowerSupply::Polarity pol = polarity(); if (pol != m_output) throw std::runtime_error("Wrong polarity detected"); } @@ -160,16 +160,16 @@ uint16_t DT54xxPs::status(unsigned channel) { return std::stoi(command("MON", "STAT")) & 0xFFFF; } -DT54xxPs::Polarity DT54xxPs::polarity(unsigned channel) { +IPowerSupply::Polarity DT54xxPs::polarity(unsigned channel) { if (channel != 1) throw std::runtime_error( "Set the channel to 1 for single channel power-supply"); std::string polstr = command("MON", "POLARITY"); if (polstr == "-") - return Polarity::Negative; + return IPowerSupply::Polarity::Negative; else - return Polarity::Positive; + return IPowerSupply::Polarity::Positive; } void DT54xxPs::setIMonRange(IMonRange range, unsigned channel) { @@ -239,11 +239,11 @@ std::string DT54xxPs::command(const std::string& cmd, const std::string& par, } double DT54xxPs::checkPolarity(double input) { - if (m_output == Polarity::Negative && input > 0) + if (m_output == IPowerSupply::Polarity::Negative && input > 0) throw std::runtime_error( "Specified positive output value for a power supply that only " "supports negative output."); - if (m_output == Polarity::Positive && input < 0) + if (m_output == IPowerSupply::Polarity::Positive && input < 0) throw std::runtime_error( "Specified negative output value for a power supply that only " "supports positive output."); @@ -252,5 +252,5 @@ double DT54xxPs::checkPolarity(double input) { } double DT54xxPs::convertPolarity(double value) { - return (m_output == Polarity::Negative) ? -value : value; + return (m_output == IPowerSupply::Polarity::Negative) ? -value : value; } diff --git a/src/libPS/DT54xxPs.h b/src/libPS/DT54xxPs.h index 55949e91951cf97ec0f37e207432f50c28f2b02a..a0350bddad530fc8a488eae497db56660f6d0b9e 100644 --- a/src/libPS/DT54xxPs.h +++ b/src/libPS/DT54xxPs.h @@ -48,11 +48,6 @@ class DT54xxPs : public IPowerSupply { CalError = (1 << 13) // 1 : Calibration Error }; - /** - * Polarity of the power supply output - */ - enum Polarity { Positive, Negative }; - /** * Range of the current monitor */ @@ -69,7 +64,8 @@ class DT54xxPs : public IPowerSupply { */ DT54xxPs(const std::string& name, const std::vector<std::string>& models = {}, - Polarity output = Polarity::Positive, double imaxl = 105e-6); + IPowerSupply::Polarity output = IPowerSupply::Polarity::Positive, + double imaxl = 105e-6); ~DT54xxPs() = default; /** \name Communication @@ -198,7 +194,7 @@ class DT54xxPs : public IPowerSupply { * * @return polarity of the channel */ - Polarity polarity(unsigned channel = 1); + IPowerSupply::Polarity polarity(unsigned channel = 1); /** * Set the current monitoring range @@ -271,7 +267,7 @@ class DT54xxPs : public IPowerSupply { //! \brief Specify whether power supply outputs a negative voltage (N vs P //! model) - Polarity m_output = Polarity::Positive; + IPowerSupply::Polarity m_output = IPowerSupply::Polarity::Positive; //! \brief Specify maximum current [A] for low IMon range double m_imaxl = 105e-6; diff --git a/src/libPS/DT8033NPs.cpp b/src/libPS/DT8033NPs.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b99f413087395cc699e2f2d8902b6e36d26044d9 --- /dev/null +++ b/src/libPS/DT8033NPs.cpp @@ -0,0 +1,175 @@ +#include "DT8033NPs.h" + +#include <algorithm> +#include <thread> + +#include "Logger.h" +#include "ScopeLock.h" +#include "TextSerialCom.h" + +// Register power supply +#include "PowerSupplyRegistry.h" + +REGISTER_POWERSUPPLY(DT8033NPs) + +DT8033NPs::DT8033NPs(const std::string& name) + : IPowerSupply(name, {"DT8033"}) {} + +void DT8033NPs::reset() { + command("SET", "OFF"); + command("SET", "BDCLR"); + + std::string result = identify(); + if (result.empty()) + throw std::runtime_error("No communication after reset."); +} + +std::string DT8033NPs::identify() { + command("SET", "BDCLR"); + std::string idn = command("MON", "BDNAME"); + return idn; +} + +bool DT8033NPs::ping() { + std::string result = command("MON", "BDNAME"); + return !result.empty(); +} + +void DT8033NPs::checkCompatibilityList() { + IPowerSupply::checkCompatibilityList(); +} + +void DT8033NPs::turnOn(unsigned channel) { + ScopeLock lock(m_com); + command("SET", "ON", channel); + waitRamp(); +} + +void DT8033NPs::turnOff(unsigned channel) { + ScopeLock lock(m_com); + command("SET", "OFF", channel); + waitRamp(); +} + +void DT8033NPs::setCurrentLevel(double cur, unsigned channel) { + command("SET", "ISET", channel, std::to_string(cur)); +} + +double DT8033NPs::getCurrentLevel(unsigned channel) { + return std::stod(command("MON", "ISET", channel)) / 1e6; +} + +void DT8033NPs::setCurrentProtect(double maxcur, unsigned channel) { + setCurrentLevel(maxcur, channel); +} + +double DT8033NPs::getCurrentProtect(unsigned channel) { + return std::fabs(getCurrentLevel(channel)); +} + +double DT8033NPs::measureCurrent(unsigned channel) { + return std::stod(command("MON", "IMON", channel)) / 1e6; +} + +void DT8033NPs::setVoltageLevel(double volt, unsigned channel) { + command("SET", "VSET", channel, std::to_string(checkPolarity(volt))); + waitRamp(); +} + +double DT8033NPs::getVoltageLevel(unsigned channel) { + return std::stod(command("MON", "VSET", channel)); +} + +void DT8033NPs::setVoltageProtect(double maxvolt, unsigned channel) { + setVoltageLevel(maxvolt, channel); +} + +double DT8033NPs::getVoltageProtect(unsigned channel) { + return std::fabs(getVoltageLevel(channel)); +} + +double DT8033NPs::measureVoltage(unsigned channel) { + return std::stod(command("MON", "VMON", channel)); +} + +uint16_t DT8033NPs::status(unsigned channel) { + return std::stoi(command("MON", "STAT", channel)) & 0xFFFF; +} + +IPowerSupply::Polarity DT8033NPs::polarity(unsigned channel) { + std::string polstr = command("MON", "POLARITY", channel); + if (polstr == "-") + return IPowerSupply::Polarity::Negative; + else + return IPowerSupply::Polarity::Positive; +} + +void DT8033NPs::setIMonRange(IMonRange range, unsigned channel) { + command("SET", "IMRANGE", channel, + (range == IMonRange::Low) ? "LOW" : "HIGH"); +} + +DT8033NPs::IMonRange DT8033NPs::getIMonRange(unsigned channel) { + return (command("MON", "IMRANGE", channel) == "LOW") ? IMonRange::Low + : IMonRange::High; +} + +void DT8033NPs::waitRamp(unsigned channel) { + do { + std::this_thread::sleep_for(std::chrono::seconds(1)); + logger(logDEBUG) << __PRETTY_FUNCTION__ + << " -> ramping: " << measureVoltage(channel) << "V"; + } while (status(channel) & (Status::RampingUp | Status::RampingDown)); +} + +std::string DT8033NPs::command(const std::string& cmd, const std::string& par, + unsigned channel, const std::string& value) { + // Build command + std::string tosend = "$CMD:" + cmd; + if (channel != 99) tosend += ",CH:" + std::to_string(channel); + tosend += ",PAR:" + par; + if (!value.empty()) tosend += ",VAL:" + value; + + // Send command and receive response + std::string resp = m_com->sendreceive(tosend); + + // Parse response + if (resp.empty()) throw "DT8033: No response :("; + + std::string retvalue; + std::string cmdvalue; + + std::string token; + std::stringstream ss(resp); + while (std::getline(ss, token, ',')) { + size_t seppos = token.find(':'); + if (seppos == std::string::npos) continue; // Not a valid part + if (token.substr(0, seppos) == "VAL") { // This is the value part! + retvalue = token.substr(seppos + 1); + } else if (token.substr(0, seppos) == + "#CMD") { // This is the value part! + cmdvalue = token.substr(seppos + 1); + } + } + + if (cmdvalue.empty()) + throw std::runtime_error("DT8033: No CMD in return statement :("); + + if (cmdvalue == "ERR") + throw std::runtime_error("DT8033: CMD shows an error :("); + + return retvalue; +} + +double DT8033NPs::checkPolarity(double input) { + if (m_output == IPowerSupply::Polarity::Negative && input > 0) + throw std::runtime_error( + "Specified positive output value for a power supply that only " + "supports negative output."); + if (m_output == IPowerSupply::Polarity::Positive && input < 0) + throw std::runtime_error( + "Specified negative output value for a power supply that only " + "supports positive output."); + + return std::fabs(input); +} diff --git a/src/libPS/DT8033NPs.h b/src/libPS/DT8033NPs.h new file mode 100644 index 0000000000000000000000000000000000000000..e80977ce083ecf784924217c3ab47df142948d0c --- /dev/null +++ b/src/libPS/DT8033NPs.h @@ -0,0 +1,257 @@ +#ifndef DT8033NPS_H +#define DT8033NPS_H + +#include <chrono> +#include <memory> +#include <string> + +#include "IPowerSupply.h" + +//! \brief Base implementation for the CAEN DT8033NPs power supplies +/** + * CAEN DT8033NPs USB High Voltage Power Supplies. + * + * Example configuration for a DT8033N PS: + * + * "name": "myDT8033NPs", + * "hw-type": "PS", + * "hw-model": "DT8033NPs", + * "communication": { + * "protocol" : "TextSerialCom", + * "termination" : "\r\n", + * "baudrate" : "B9600", + * "port": "/dev/ttyACM1" + * } + * + * [Programming Manual](https://www.caen.it/products/dt8033/) + */ +class DT8033NPs : public IPowerSupply { + public: + /** + * Interpretation of status bits. + * The value is the bitmask to select the bit in status() return value. + */ + enum Status { + On = (1 << 0), // 1 : ON 0 : OFF + RampingUp = (1 << 1), // 1 : Channel Ramping UP + RampingDown = (1 << 2), // 1 : Channel Ramping DOWN + OVC = (1 << 3), // 1 : Over current + OVV = (1 << 4), // 1 : Over voltage + UNV = (1 << 5), // 1 : Under voltage + MAXV = (1 << 6), // 1 : VOUT in MAXV protection + Trip = (1 << 7), // 1 : Current generator + OVT = (1 << 8), // 1 : Over temperature + Disabled = (1 << 10), // 1 : Ch disabled + Kill = (1 << 11), // 1 : Ch in KILL + Interlock = (1 << 12), // 1 : Ch in INTERLOCK + CalError = (1 << 13) // 1 : Calibration Error + }; + + /** + * Range of the current monitor + */ + enum IMonRange { High, Low }; + + //! \brief Constructor that configures checks for a specific collection of + //! models + /** + * @param name `IPowerSupply` instance name + * @param models List of supported model names (see + * `IPowerSupply::checkCompatibilityList`) + * @param output The output voltage is negative + * @param imaxl Maximum current [A] for low IMon range + */ + DT8033NPs(const std::string& name); + ~DT8033NPs() = default; + + /** \name Communication + * @{ + */ + + virtual bool ping(); + + virtual std::string identify(); + + /** + * In addition to the standard model check from `IPowerSupply`, this also + * checks that the `output` setting is correct. + */ + virtual void checkCompatibilityList(); + + /** @} */ + + /** \name Power Supply Control + * @{ + */ + + virtual void reset(); + + /** \brief Turn on power supply + * + * Block until power supply finishes ramping. + * + * @param channel channel, if any + */ + virtual void turnOn(unsigned channel); + + /** \brief Turn off power supply + * + * Block until power supply finishes rampdown. + * @param channel channel, if any + */ + virtual void turnOff(unsigned channel); + + /** @} */ + + /** \name Current Control and Measurement + * @{ + */ + + /** + * Calls `setCurrentProtect` with `cur` as the maximum level. + */ + virtual void setCurrentLevel(double cur, unsigned channel = 1); + virtual double getCurrentLevel(unsigned channel = 1); + + //! \brief Set current protection + /** + * The current monitor range is also automatically set to match up + * with the maximum current (`maxcur`). If `maxcur` is below the maximum + * of the low IMon range (`imaxl`, see constructor), then low Imon range + * is used. Otherwise the high IMon range is used. + * + * @param maxcur maximum current (absolute value) [A] + * @param channel channel (if any) + */ + virtual void setCurrentProtect(double maxcur, unsigned channel = 1); + + //! \brief Get current protection + /** + * @param channel channel (if any) + * @return maximum current (absolute value) [A] + */ + virtual double getCurrentProtect(unsigned channel = 1); + virtual double measureCurrent(unsigned channel = 1); + + /** @} */ + + /** \name Voltage Control and Measurement + * @{ + */ + + //! \brief Set output voltage level. + /** + * For the N-type power supplies, the voltage level should be + * specified as negative. This function uses the `output` + * property to validate the input. + * + * @param volt voltage [V] + * @param channel channel (if any) + */ + virtual void setVoltageLevel(double volt, unsigned channel = 1); + virtual double getVoltageLevel(unsigned channel = 1); + + //! \brief Set voltage protection + /** + * @param maxvolt maximum voltage (absolute value) [V] + * @param channel channel (if any) + */ + virtual void setVoltageProtect(double maxvolt, unsigned channel = 1); + + //! \brief Get voltage protection + /** + * @param channel channel (if any) + * @return maximum voltage (absolute value) [V] + */ + virtual double getVoltageProtect(unsigned channel = 1); + + virtual double measureVoltage(unsigned channel = 1); + + /** @} */ + + /** \name Model-specific functionality + * @{ + */ + + /** + * Return the status of the Power Supply. + * Use with Status enum to interpret bits. + * + * @param channel Channel to query + * + * @return status bits + */ + uint16_t status(unsigned channel = 1); + + /** + * Return the polarity of the power supply. + * + * @param channel Channel to query + * + * @return polarity of the channel + */ + IPowerSupply::Polarity polarity(unsigned channel = 1); + + /** + * Set the current monitoring range + * + * @param range to set + */ + void setIMonRange(IMonRange range = Low, unsigned channel = 1); + + /** + * Get the current monitoring range + * + * @return currently set range + */ + IMonRange getIMonRange(unsigned channel = 1); + + //! \brief Wait for voltage ramp to complete + /** + * Monitors the status of the power supply every + * second until the ramp up and down bits are zero. + * + * The monitoring starts by waiting for 1 second for + * the status register to be updated. + */ + void waitRamp(unsigned channel = 1); + + /** @} */ + + private: + //! \brief Build a command string and parse the response + /** + * Throws an exception if any of the following errors are detected: + * - no response + * - returned CMD value is ERR + * + * @param cmd CMD value + * @param par PAR value + * @param value VAL value (if empty, not appended) + * + * @return The returned VAL value. + */ + std::string command(const std::string& cmd, const std::string& par, + unsigned channel = 99, const std::string& value = ""); + + //! \brief Check that the input (voltage or current) has the right sign and + //! convert to absolute + /** + * A `std::runtime_error` is thrown if a wrong sign is supplied, as + * determine by the `m_output` setting. + * + * The conversion to absolute value is necessary for the power supplies + * command protocol. + * + * @param `input` Input value to check and convert. + * + * @return Absolute value of `input` + */ + double checkPolarity(double input); + + //! \brief Specify whether power supply outputs a negative voltage (N vs P + //! model) + IPowerSupply::Polarity m_output = IPowerSupply::Polarity::Negative; +}; + +#endif // DT8033NPS_H diff --git a/src/libPS/IPowerSupply.h b/src/libPS/IPowerSupply.h index 5cf14258beeec54ee90394e393d2e97e843729f0..d601dfa3e4872ca56c93fbf477b978c66823a806 100644 --- a/src/libPS/IPowerSupply.h +++ b/src/libPS/IPowerSupply.h @@ -2,6 +2,7 @@ #define IPOWERSUPPLY_H #include <nlohmann/json.hpp> +#include <stdexcept> #include <string> #include "ICom.h" @@ -222,6 +223,21 @@ class IPowerSupply { /** @} */ + /** An exception to be thrown when an operation is executed which + * would trigger a polarity change while to power supply is turned on. + * Implemented in order for application to explicitly catch these types + * of errors and implement fault handling. + */ + struct polarity_error : public std::runtime_error { + polarity_error(std::string const& what = "") + : std::runtime_error(what) {} + }; + + /** + * Polarity of the power supply output + */ + enum class Polarity : int { Positive, Negative }; + protected: /** Communication */ std::shared_ptr<ICom> m_com = nullptr; diff --git a/src/libPS/IsegPs.cpp b/src/libPS/IsegPs.cpp new file mode 100644 index 0000000000000000000000000000000000000000..65b3fe7da0ab0315a1ba7cb72a9c99f4fba78056 --- /dev/null +++ b/src/libPS/IsegPs.cpp @@ -0,0 +1,219 @@ +#include "IsegPs.h" + +#include <algorithm> +#include <chrono> +#include <cmath> +#include <thread> + +#include "Logger.h" +#include "ScopeLock.h" +#include "StringUtils.h" +#include "TextSerialCom.h" + +// Register power supply +#include "PowerSupplyRegistry.h" +REGISTER_POWERSUPPLY(IsegPs) + +IsegPs::IsegPs(const std::string& name, std::vector<std::string> models, + unsigned maxChannels) + : IPowerSupply(name, models), m_maxChannels(maxChannels) {} + +bool IsegPs::ping() { + std::string result = sendreceive("*IDN?"); + return !result.empty(); +} + +void IsegPs::reset() { + send("*RST"); + if (!ping()) throw std::runtime_error("No communication after reset."); +} + +std::string IsegPs::identify() { + std::string idn = sendreceive("*IDN?"); + return idn; +} + +void IsegPs::turnOn(unsigned channel) { send(":VOLT ON,", channel); } + +void IsegPs::turnOff(unsigned channel) { send(":VOLT OFF,", channel); } + +void IsegPs::setCurrentLevel(double cur, unsigned channel) { + send(":CURR " + std::to_string(cur) + ",", channel); +} + +double IsegPs::getCurrentLevel(unsigned channel) { + std::string const recv = sendreceive(":READ:CURR:NOM?", channel); + return std::stod(utils::trim_last_char(recv)); // remove the A from the end +} + +void IsegPs::setCurrentProtect(double maxcur, unsigned channel) { + logger(logWARNING) << "setCurrentProtect() not implemented for this PS. " + "Manual adjustment at PS required."; +} + +double IsegPs::getCurrentProtect(unsigned channel) { + std::string const recv = sendreceive(":READ:CURR:LIM?", channel); + return std::stod(utils::trim_last_char(recv)); +} + +double IsegPs::measureCurrent(unsigned channel) { + std::string const recv = sendreceive("MEAS:CURR?", channel); + return std::stod(utils::trim_last_char(recv)); // remove the A from the end +} + +void IsegPs::setVoltageLevel(double volt, unsigned channel) { + auto channelOn = isOn(channel); + auto PSPolarity = getPolarity(channel); // the current polarity set + auto ReqPolarityPos = + !std::signbit(volt); // is requested volt not negative + if (!ReqPolarityPos && (PSPolarity == IPowerSupply::Polarity::Positive)) { + if (channelOn) { + throw IPowerSupply::polarity_error( + "Attempting to switch to negative from positive polarity while " + "channel is on"); + } else { + setPolarity(IPowerSupply::Polarity::Negative, channel); + // need to wait before sending the new voltage, if no wait, the + // voltage will not be set correctly + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } else if (ReqPolarityPos && + (PSPolarity == IPowerSupply::Polarity::Negative)) { + if (channelOn) { + throw IPowerSupply::polarity_error( + "Attempting to switch to positive from negative polarity while " + "channel is on"); + } else { + setPolarity(IPowerSupply::Polarity::Positive, channel); + // need to wait before sending the new voltage, if no wait, the + // voltage will not be set correctly + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } + send(":VOLTAGE " + std::to_string(volt) + ",", channel); +} + +double IsegPs::getVoltageLevel(unsigned channel) { + std::string const recv = sendreceive(":READ:VOLTAGE?", channel); + return std::stod(utils::trim_last_char(recv)); // remove the V from the end +} + +void IsegPs::setVoltageProtect(double maxvolt, unsigned channel) { + logger(logWARNING) << "setVoltageProtect() not implemented for this PS. " + "Manual adjustment at PS required."; +} + +double IsegPs::getVoltageProtect(unsigned channel) { + std::string const recv = sendreceive(":READ:VOLTAGE:LIM?", channel); + return std::stod(utils::trim_last_char(recv)); // remove the V from the end +} + +double IsegPs::measureVoltage(unsigned channel) { + std::string const recv = sendreceive(":MEAS:VOLT?", channel); + return std::stod(utils::trim_last_char(recv)); // remove the V from the end +} + +void IsegPs::send(const std::string& cmd) { + std::string opcreply = sendreceive(cmd + ";*OPC?"); + utils::rtrim(opcreply); + if (opcreply != "1") + throw std::runtime_error("IsegPs::send: *OPC? check failed"); +} + +void IsegPs::send(const std::string& cmd, unsigned channel) { + if (m_maxChannels > 0) { // In-range channel check + if (channel > m_maxChannels) + throw std::runtime_error("Invalid channel: " + + std::to_string(channel)); + } + std::string retCmd = cmd; + if (m_maxChannels != 1) retCmd += " (@" + std::to_string(channel) + ")"; + send(retCmd); +} + +IPowerSupply::Polarity IsegPs::getPolarity(unsigned channel) { + if (m_maxChannels > 0) { // In-range channel check + if (channel > m_maxChannels) + throw std::runtime_error("Invalid channel: " + + std::to_string(channel)); + } + std::string cmd = ":CONF:OUTP:POL? (@" + std::to_string(channel) + ")"; + std::string recv = sendreceive(cmd); + if (recv == "p") + return IPowerSupply::Polarity::Positive; + else if (recv == "n") + return IPowerSupply::Polarity::Negative; + else + throw std::runtime_error( + "IsegPs::getPolarityPos: could not determine polarity"); +} + +void IsegPs::setPolarity(IPowerSupply::Polarity polarity, unsigned channel) { + if (m_maxChannels > 0) { // In-range channel check + if (channel > m_maxChannels) + throw std::runtime_error("Invalid channel: " + + std::to_string(channel)); + } + turnOff(channel); + std::string cmd; + if (polarity == IPowerSupply::Polarity::Positive) + cmd = ":CONF:OUTP:POL p, (@" + std::to_string(channel) + ")"; + else if (polarity == IPowerSupply::Polarity::Negative) + cmd = ":CONF:OUTP:POL n, (@" + std::to_string(channel) + ")"; + else + throw std::runtime_error("Invalid polarity"); + send(cmd); +} + +bool IsegPs::isOn(unsigned channel) { + if (m_maxChannels > 0) { // In-range channel check + if (channel > m_maxChannels) + throw std::runtime_error("Invalid channel: " + + std::to_string(channel)); + } + std::string const cmd = ":READ:VOLT:ON? (@" + std::to_string(channel) + ")"; + auto recv = sendreceive(cmd); + if (recv == "1") + return true; + else if (recv == "0") + return false; + else + throw std::runtime_error( + "IsegPs::isOn: could not determine if channel is on"); +} + +std::string IsegPs::sendreceive(const std::string& cmd) { + ScopeLock lock(m_com); + auto rec = m_com->sendreceive(cmd); + // In case the IsegSHR20xxPs uses a serial communication via USB/Serial its + // responses are echoed to validate communication integrity in these cases, + // the result looks like: <CMD><Term><RES><Term>, the ultimate termination + // is already stripped by the Com implementation, here we need to split the + // <CMD> and <RES> and validate that the read back <CMD> is the one we sent + auto text_serial_port = dynamic_cast<TextSerialCom*>(m_com.get()); + if (text_serial_port) { + auto term_seq = text_serial_port->returnTermination(); + auto rec_split = utils::split(rec, term_seq); + if (cmd != rec_split.at(0)) + throw std::runtime_error( + "IsegPs::sendreceive: readback of command failed!"); + if (rec_split.size() != 2) + throw std::runtime_error( + "IsegPs::sendreceive: received invalid reaback!"); + return rec_split.at(1); + } else { + return rec; + } +} + +std::string IsegPs::sendreceive(const std::string& cmd, unsigned channel) { + if (m_maxChannels > 0) { // In-range channel check + if (channel > m_maxChannels) + throw std::runtime_error("Invalid channel: " + + std::to_string(channel)); + } + std::string retCmd = cmd; + if (m_maxChannels != 1) retCmd += " (@" + std::to_string(channel) + ")"; + + return sendreceive(retCmd); +} diff --git a/src/libPS/IsegPs.h b/src/libPS/IsegPs.h new file mode 100644 index 0000000000000000000000000000000000000000..0a3687b1daeab5cb62a857af6fe96912a1e8e295 --- /dev/null +++ b/src/libPS/IsegPs.h @@ -0,0 +1,171 @@ +#ifndef ISEGPS_H +#define ISEGPS_H + +#include <chrono> +#include <memory> +#include <string> + +#include "IPowerSupply.h" + +/** + * Base implemetation for iseg power supplies that share + * a very similar command set. + * + * Options are possible to add additional for error checking on + * - channel checking (maximum number of channels, 0 means no check is + * performed) + */ +class IsegPs : public IPowerSupply { + public: + /** + * @param name Name of the power supply + * @param models List of supported models (empty means no check is + * performed) + * @param maxChannels Maximum number of channels in the power supply (0 + * means no maximum) + */ + IsegPs(const std::string& name, std::vector<std::string> models = {}, + unsigned maxChannels = 0); + ~IsegPs() = default; + + /** \name Communication + * @{ + */ + + virtual bool ping(); + + /** @} */ + + /** \name Power Supply Control + * @{ + + /** + * Returns the model identifier of the PS connected + * + */ + virtual std::string identify(); + + virtual void reset(); + + /** Turn on specific channel + * + * @param channel the channel to turn on + */ + virtual void turnOn(unsigned channel); + + /** Turn off specific channel + * + * @param channel the channel to turn off + */ + virtual void turnOff(unsigned channel); + + /** + * Returns true if the channel's output is on, and false otherwise + * + * @param channel the channel to query on state + */ + virtual bool isOn(unsigned channel); + + /** @} */ + + /** \name Current Control and Measurement + * @{ + */ + + virtual void setCurrentLevel(double cur, unsigned channel = 0); + virtual double getCurrentLevel(unsigned channel = 0); + virtual void setCurrentProtect(double maxcur, unsigned channel = 0); + virtual double getCurrentProtect(unsigned channel = 0); + virtual double measureCurrent(unsigned channel = 0); + + /** @} */ + + /** \name Voltage Control and Measurement + * @{ + */ + + /** + * Will set the voltage to the requested value for a given channel. Will + * throw an exception if the channel is powered on and a polarity switch is + * requested. Takes the signedness of the passed argument in order to + * determine the polarity, i.e. -0 and 0 produce a different polarity + * + * @param volt the voltage to set + * @param channel the channel to set the voltage + */ + virtual void setVoltageLevel(double volt, unsigned channel = 0); + virtual double getVoltageLevel(unsigned channel = 0); + virtual void setVoltageProtect(double maxvolt, unsigned channel = 0); + virtual double getVoltageProtect(unsigned channel = 0); + virtual double measureVoltage(unsigned channel = 0); + + /** @} */ + + /** \name Polarity Control and Measurement + * @{ + */ + + virtual IPowerSupply::Polarity getPolarity(unsigned channel = 0); + virtual void setPolarity(IPowerSupply::Polarity polarity, + unsigned channel = 0); + + /** @} */ + + protected: + //\brief Send power supply command and wait for operation complete. + /** + * Sends `cmd` to the power supply followed up `*OPC?`. Then blocks until + * a response is received. This prevents the program from terminating + * before the program is processed. + * + * An error is thrown if no response to `*OPC?` is seen. + * + * \param cmd Power supply command to transmit. + */ + void send(const std::string& cmd); + + //! \brief Add channel set to power supply send + /** + * Adds `" (@"+channel+")"` to the end of the actual command. This is + * done without injecting `*OPC?`. + * + * The rest is achieved using `IsegPs::send(constd std::string& cmd)`. + * + * \param cmd Power supply command to transmit. + * \param channel Target channel + */ + void send(const std::string& cmd, unsigned channel); + + //\brief Send power supply command and return response. + /** + * Wrapper around `ICom::sendreceive`. + * + * \param cmd Power supply command to transmit. + * + * \return Parameter response. + */ + std::string sendreceive(const std::string& cmd); + + //! \brief Add channel set to power supply sendreceive + /** + * Adds `" (@"+channel+")"` to the end of the actual command. This is + * done without injecting `*OPC?`. + * + * The rest is achieved using `IsegPs::sendreceive(constd std::string& + * cmd)`. + * + * \param cmd Power supply command to transmit. + * \param channel Target channel + * + * \return Response for `cmd` + */ + std::string sendreceive(const std::string& cmd, unsigned channel); + + private: + /** PS limitations @{ */ + + //! Maximum number of channels, 0 means unlimited + uint32_t m_maxChannels = 0; +}; + +#endif diff --git a/src/libPS/IsegSHR20xxPs.cpp b/src/libPS/IsegSHR20xxPs.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c470281c76706941454b0faab3466d3d3c72b087 --- /dev/null +++ b/src/libPS/IsegSHR20xxPs.cpp @@ -0,0 +1,11 @@ +#include "IsegSHR20xxPs.h" + +#include "StringUtils.h" +#include "TextSerialCom.h" + +// Register power supply +#include "PowerSupplyRegistry.h" +REGISTER_POWERSUPPLY(IsegSHR20xxPs) + +IsegSHR20xxPs::IsegSHR20xxPs(const std::string& name) + : IsegPs(name, {"SR020020"}, 2) {} \ No newline at end of file diff --git a/src/libPS/IsegSHR20xxPs.h b/src/libPS/IsegSHR20xxPs.h new file mode 100644 index 0000000000000000000000000000000000000000..0149fe02578c105526b64425623e0cc5f3d44f82 --- /dev/null +++ b/src/libPS/IsegSHR20xxPs.h @@ -0,0 +1,22 @@ +#ifndef ISEGSHR20XXPS_H +#define ISEGSHR20XXPS_H + +#include <string> + +#include "IsegPs.h" + +/** + * Implementation for the [ISEG SHR 20XX Dual Channel Output HV Power + * Supplies](https://iseg-hv.com/en/products/detail/SHR). + * The dual-channel ISEG power supplies appear to support the default [ISEG SHR + * programming + * model](https://iseg-hv.com/download/SOFTWARE/isegSCPI/SCPI_Programmers_Guide_en.pdf), + * but this has not been checked. + */ +class IsegSHR20xxPs : public IsegPs { + public: + IsegSHR20xxPs(const std::string& name); + ~IsegSHR20xxPs() = default; +}; + +#endif // ISEGSHR20XXPS_H diff --git a/src/libPS/RS_NGP804.cpp b/src/libPS/RS_NGP804.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4d9fa630df527736833eba95154b75100d334961 --- /dev/null +++ b/src/libPS/RS_NGP804.cpp @@ -0,0 +1,12 @@ +#include "RS_NGP804.h" + +#include <algorithm> +#include <thread> + +#include "Logger.h" + +// Register power supply +#include "PowerSupplyRegistry.h" +REGISTER_POWERSUPPLY(RS_NGP804) + +RS_NGP804::RS_NGP804(const std::string& name) : SCPIPs(name, {"NGP804"}, 4) {} diff --git a/src/libPS/RS_NGP804.h b/src/libPS/RS_NGP804.h new file mode 100644 index 0000000000000000000000000000000000000000..f6b4f1918c9faad9fa8161aaed2a4f9ec7ca100f --- /dev/null +++ b/src/libPS/RS_NGP804.h @@ -0,0 +1,24 @@ +#ifndef RS_NGP804_H +#define RS_NGP804_H + +#include <chrono> +#include <memory> +#include <string> + +#include "SCPIPs.h" +#include "SerialCom.h" + +/** \brief ROHDE&SCHWARZ NGP804 + * Implementation for the ROHDE&SCHWARZ NGP804 power supply. + * + * [Programming + * Manual](https://scdn.rohde-schwarz.com/ur/pws/dl_downloads/pdm/cl_manuals/user_manual/5601_5610_01/NGP800_User_Manual_en_11.pdf) + * + */ +class RS_NGP804 : public SCPIPs { + public: + RS_NGP804(const std::string& name); + ~RS_NGP804() = default; +}; + +#endif diff --git a/src/libPS/TTIXXXSPPs.cpp b/src/libPS/TTIXXXSPPs.cpp index 2ac101ad424942a9a6814450f12cb8c1daaac8c2..1abd8958f969b37924accc7ce91e797e60b7dda3 100644 --- a/src/libPS/TTIXXXSPPs.cpp +++ b/src/libPS/TTIXXXSPPs.cpp @@ -10,4 +10,4 @@ REGISTER_POWERSUPPLY(TTIXXXSPPs) TTIXXXSPPs::TTIXXXSPPs(const std::string& name) - : TTIPs(name, {"TSX1820P"}, 1) {} + : TTIPs(name, {"TSX1820P", "CPX400SP"}, 1) {} diff --git a/src/libPS/TTIXXXTPPs.cpp b/src/libPS/TTIXXXTPPs.cpp index aec28418255f4f9a2201cc479cb048027bc7868a..434ce07dd70e9d11d6edc67302b267005fa29056 100644 --- a/src/libPS/TTIXXXTPPs.cpp +++ b/src/libPS/TTIXXXTPPs.cpp @@ -10,4 +10,4 @@ REGISTER_POWERSUPPLY(TTIXXXTPPs) TTIXXXTPPs::TTIXXXTPPs(const std::string& name) - : TTIPs(name, {"MX180TP", "QL355TP"}, 3) {} + : TTIPs(name, {"MX100TP", "MX180TP", "QL355TP"}, 3) {} diff --git a/src/libPS/python.cpp b/src/libPS/python.cpp index 75ba925113d857d1a8344016a3daf10c7f82917d..1e866c49de6a83c00e37b480ae3a41d3e69083f6 100644 --- a/src/libPS/python.cpp +++ b/src/libPS/python.cpp @@ -9,13 +9,17 @@ #include "DT5471NPs.h" #include "DT5472NPs.h" #include "DT54xxPs.h" +#include "DT8033NPs.h" #include "IPowerSupply.h" +#include "IsegPs.h" +#include "IsegSHR20xxPs.h" #include "Keithley22XX.h" #include "Keithley24XX.h" #include "PowerSupplyChannel.h" #include "PowerSupplyRegistry.h" #include "RS_HMP2020.h" #include "RS_HMP4040.h" +#include "RS_NGP804.h" #include "RigolDP832.h" #include "SCPIPs.h" #include "SorensenPs.h" @@ -252,7 +256,7 @@ void register_ps(py::module &m) { py_DT54xxPs .def(py::init<const std::string &, const std::vector<std::string> &, - DT54xxPs::Polarity, double>()) + IPowerSupply::Polarity, double>()) .def("status", &DT54xxPs::status) .def("polarity", &DT54xxPs::polarity) .def("setIMonRange", &DT54xxPs::setIMonRange) @@ -274,8 +278,8 @@ void register_ps(py::module &m) { .value("CalError", DT54xxPs::Status::CalError); py::enum_<DT54xxPs::Polarity>(py_DT54xxPs, "Polarity") - .value("Positive", DT54xxPs::Polarity::Positive) - .value("Negative", DT54xxPs::Polarity::Negative); + .value("Positive", IPowerSupply::Polarity::Positive) + .value("Negative", IPowerSupply::Polarity::Negative); py::enum_<DT54xxPs::IMonRange>(py_DT54xxPs, "IMonRange") .value("High", DT54xxPs::IMonRange::High) @@ -287,6 +291,21 @@ void register_ps(py::module &m) { py::class_<DT5472NPs, DT54xxPs, std::shared_ptr<DT5472NPs>>(m, "DT5472NPs") .def(py::init<const std::string &>()); + py::class_<DT8033NPs, PyPS<DT8033NPs>, IPowerSupply, + std::shared_ptr<DT8033NPs>>(m, "DT8033NPs") + .def(py::init<const std::string &>()); + + py::class_<IsegPs, PyPS<IsegPs>, IPowerSupply, std::shared_ptr<IsegPs>>( + m, "IsegPs") + .def(py::init<const std::string &, std::vector<std::string> &, + unsigned>(), + py::arg("name"), py::arg("models") = py::list(), + py::arg("maxChannels") = 0); + + py::class_<IsegSHR20xxPs, IsegPs, std::shared_ptr<IsegSHR20xxPs>>( + m, "IsegSHR20xxPs") + .def(py::init<const std::string &>()); + py::class_<Keithley22XX, PyPS<Keithley22XX>, IPowerSupply, std::shared_ptr<Keithley22XX>>(m, "Keithley22XX") .def(py::init<const std::string &>()); @@ -305,6 +324,9 @@ void register_ps(py::module &m) { py::class_<RS_HMP2020, SCPIPs, std::shared_ptr<RS_HMP2020>>(m, "RS_HMP2020") .def(py::init<const std::string &>()); + py::class_<RS_NGP804, SCPIPs, std::shared_ptr<RS_NGP804>>(m, "RS_NGP804") + .def(py::init<const std::string &>()); + py::class_<RigolDP832, SCPIPs, std::shared_ptr<RigolDP832>>(m, "RigolDP832") .def(py::init<const std::string &>()); diff --git a/src/libUtils/StringUtils.h b/src/libUtils/StringUtils.h index b510ed08f837dd31d2c7577a692fc346026e34f2..2ef2b6df77b3990f23d7dbac80b7007b19ac9137 100644 --- a/src/libUtils/StringUtils.h +++ b/src/libUtils/StringUtils.h @@ -3,6 +3,7 @@ #include <algorithm> #include <iomanip> +#include <sstream> #include <string> #include <vector> @@ -85,6 +86,11 @@ static inline std::string to_string_with_precision(const T a_value, return out.str(); } +static inline std::string trim_last_char(std::string str) { + str.pop_back(); + return str; +} + }; // namespace utils #endif // STRINGUTILS_H diff --git a/src/tools/TfromNTC.cpp b/src/tools/TfromNTC.cpp index af81556395a34c71afb4cc9eb7f5693c9a615797..86b3fc4c7eeb652734719c2fc0d927f0c434faa1 100644 --- a/src/tools/TfromNTC.cpp +++ b/src/tools/TfromNTC.cpp @@ -168,10 +168,14 @@ int main(int argc, char* argv[]) { // Setup utility to convert NTC resistance to temperature. // Note: We read R_ntc directly from a meter, so no need to setup // ADCDevice. We had to expose RtoC in NTCSensor class. - // Default parameters - float ntc_para_A = 0.8676453371787721E-3; - float ntc_para_B = 2.541035850140508E-4; - float ntc_para_C = 1.868520310774293E-7; + // Default parameters for B57230V2103F260 which is the DCS NTC on the + // ITkPix v1.1 quad flex Datasheet + // https://www.tdk-electronics.tdk.com/inf/50/db/ntc/NTC_SMD_Standard_series_0402.pdf + // Parameters obtained using fit + // https://gitlab.cern.ch/kbai/ntc-param-fitter + float ntc_para_A = 8.76745581E-4; + float ntc_para_B = 2.53169819E-4; + float ntc_para_C = 1.86485152E-7; // Set the ntc parameters if provided if (params.size() == 3) { logger(logDEBUG) diff --git a/src/tools/chiller.cpp b/src/tools/chiller.cpp index a39cd77b7c6a664fab781c97643918f6848e5001..d4d4eec80994bc7085ab7ddc21b052726a497088 100644 --- a/src/tools/chiller.cpp +++ b/src/tools/chiller.cpp @@ -31,6 +31,10 @@ void usage(char* argv[]) { std::cerr << " meas-temp Get reading of current temperature in " "degrees-Celsius" << std::endl; + std::cerr << " meas-pressure Get reading of current pressure in PSI" + << std::endl; + std::cerr << " meas-flow Get reading of current flow in LPM" + << std::endl; std::cerr << " turn-on Turn on the chiller" << std::endl; std::cerr << " turn-off Turn off the chiller" << std::endl; std::cerr << "List of options:" << std::endl; @@ -172,6 +176,16 @@ int main(int argc, char* argv[]) { std::cout << chiller->measureTemperature(); if (logIt::loglevel >= logDEBUG) std::cout << " deg-C"; std::cout << std::endl; + } else if (command == "meas-pressure") { + if (logIt::loglevel >= logDEBUG) std::cout << "Measured-pressure: "; + std::cout << chiller->measurePressure(); + if (logIt::loglevel >= logDEBUG) std::cout << " PSI"; + std::cout << std::endl; + } else if (command == "meas-flow") { + if (logIt::loglevel >= logDEBUG) std::cout << "Measured-flow: "; + std::cout << chiller->measureFlow(); + if (logIt::loglevel >= logDEBUG) std::cout << " LPM"; + std::cout << std::endl; } else if (command == "turn-on") { logger(logDEBUG) << "Turning chiller ON"; chiller->turnOn();