├── .gitignore ├── docs ├── iPMA001.pdf ├── iPMA052_programmingManual.pdf ├── README.md └── iPM Software Interface Requirements.docx ├── ipm_0.1.orig.tar.xz ├── ipmenv.yml ├── Jenkinsfile_CentOS8 ├── Jenkinsfile_UbuntuBionic ├── tests ├── SConscript ├── cmd_gtest.cc ├── status_gtest.cc ├── bitresult_gtest.cc ├── measure_gtest.cc ├── record_gtest.cc ├── argparse_gtest.cc └── naiipm_gtest.cc ├── SConstruct ├── src ├── cmd.h ├── status.h ├── status.cc ├── bitresult.h ├── cmd.cc ├── measure.h ├── bitresult.cc ├── record.h ├── measure.cc ├── argparse.h ├── record.cc └── argparse.cc ├── CHANGELOG.md ├── README.md ├── snd_udp.py ├── naiipm.h ├── ctrl.cc ├── config.py ├── emulate.py ├── LICENSE ├── ipm.xml └── naiipm.cc /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | ipm_ctrl 3 | tests/g_test 4 | .sconsign.dblite 5 | -------------------------------------------------------------------------------- /docs/iPMA001.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCAR/aircraft_ipm/main/docs/iPMA001.pdf -------------------------------------------------------------------------------- /ipm_0.1.orig.tar.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCAR/aircraft_ipm/main/ipm_0.1.orig.tar.xz -------------------------------------------------------------------------------- /docs/iPMA052_programmingManual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCAR/aircraft_ipm/main/docs/iPMA052_programmingManual.pdf -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | * iPM Hardware specs 4 | * iPM Programming Manual 5 | * iPM software requirements 6 | 7 | -------------------------------------------------------------------------------- /ipmenv.yml: -------------------------------------------------------------------------------- 1 | name: ipmenv 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - conda-forge::pyserial 6 | - conda-forge::numpy 7 | -------------------------------------------------------------------------------- /docs/iPM Software Interface Requirements.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCAR/aircraft_ipm/main/docs/iPM Software Interface Requirements.docx -------------------------------------------------------------------------------- /Jenkinsfile_CentOS8: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | node { 4 | label 'CentOS8' 5 | } 6 | } 7 | triggers { 8 | pollSCM('H/5 * * * *') 9 | } 10 | stages { 11 | stage('Shell script 0') { 12 | steps { 13 | sh 'scons' 14 | } 15 | } 16 | } 17 | post { 18 | failure { 19 | emailext to: "janine@ucar.edu", 20 | subject: "iPM Jenkinsfile CentOS8 build failed", 21 | body: "See attached build console output", 22 | attachLog: true 23 | } 24 | } 25 | options { 26 | buildDiscarder(logRotator(numToKeepStr: '6')) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Jenkinsfile_UbuntuBionic: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | node { 4 | label 'UbuntuBionic32' 5 | } 6 | } 7 | triggers { 8 | pollSCM('H/5 * * * *') 9 | } 10 | stages { 11 | stage('Shell script 0') { 12 | steps { 13 | sh 'scons' 14 | } 15 | } 16 | } 17 | post { 18 | failure { 19 | emailext to: "janine@ucar.edu", 20 | subject: "iPM Jenkinsfile Ubuntu build failed", 21 | body: "See attached build console output", 22 | attachLog: true 23 | } 24 | } 25 | options { 26 | buildDiscarder(logRotator(numToKeepStr: '6')) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/SConscript: -------------------------------------------------------------------------------- 1 | # -*- python -*- 2 | import sys 3 | 4 | if sys.platform == "darwin": 5 | def testbase(env): 6 | env.Append(CXXFLAGS=['-std=c++14']) 7 | env.Append(CPPPATH='/opt/homebrew/include') 8 | env.Append(LIBPATH='/opt/homebrew/lib') 9 | env = Environment(tools=['default', testbase]) 10 | else: 11 | env = Environment(tools=['default']) 12 | 13 | env.Append(LIBS = ['gtest_main', 'gtest', 'gmock']) 14 | 15 | sources = Split(""" 16 | cmd_gtest.cc 17 | argparse_gtest.cc 18 | naiipm_gtest.cc 19 | measure_gtest.cc 20 | status_gtest.cc 21 | record_gtest.cc 22 | bitresult_gtest.cc 23 | """) 24 | 25 | env.Program(target = 'g_test', source = sources) 26 | -------------------------------------------------------------------------------- /SConstruct: -------------------------------------------------------------------------------- 1 | # -*- python -*- 2 | from SCons.Script import COMMAND_LINE_TARGETS 3 | import sys 4 | 5 | if sys.platform == "darwin": 6 | def ipmbase(env): 7 | env.Append(CXXFLAGS=['-std=c++14']) 8 | env = Environment(tools=['default', ipmbase]) 9 | else: 10 | env = Environment(tools=['default']) 11 | 12 | 13 | Export('env') 14 | 15 | 16 | sources = Split(""" 17 | ctrl.cc 18 | naiipm.cc 19 | src/argparse.cc 20 | src/cmd.cc 21 | src/measure.cc 22 | src/status.cc 23 | src/record.cc 24 | src/bitresult.cc 25 | """) 26 | 27 | 28 | ipm_ctrl=env.Program(target = 'ipm_ctrl', source = sources) 29 | env.Default(ipm_ctrl) 30 | 31 | env.Alias('install', env.Install('/opt/nidas/bin', 'ipm_ctrl')) 32 | 33 | env.SConscript("tests/SConscript") 34 | -------------------------------------------------------------------------------- /src/cmd.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | 6 | #include 7 | #include 8 | 9 | #ifndef CMD_H 10 | #define CMD_H 11 | 12 | class ipmCmd 13 | { 14 | public: 15 | 16 | ipmCmd(); 17 | ~ipmCmd(); 18 | 19 | void printMenu(); 20 | bool verify(std::string cmd); 21 | auto response(std::string msg) { return _ipm_commands.find(msg); } 22 | 23 | private: 24 | // Map message to expected response 25 | std::map_ipm_commands; 26 | 27 | }; 28 | 29 | #endif /* CMD_H */ 30 | -------------------------------------------------------------------------------- /src/status.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include 6 | #include 7 | 8 | #ifndef STATUS_H 9 | #define STATUS_H 10 | 11 | class ipmStatus 12 | { 13 | 14 | private: 15 | 16 | struct 17 | { 18 | uint8_t OPSTATE; // Operational State 19 | uint8_t POWEROK; // Power OK 20 | uint32_t TRIPFLAGS; // Power Trip Flags, performance exceeds limit 21 | uint32_t CAUTIONFLAGS; // Power Caution Flags, marginal performance 22 | uint16_t BITSTAT; // bitStatus 23 | } status; 24 | 25 | 26 | public: 27 | 28 | ipmStatus(); 29 | ~ipmStatus(); 30 | 31 | /* Parse response to the STATUS command into component variables */ 32 | void parse(uint8_t *cp, uint16_t *sp); 33 | /* Build a comma delimited string to send as a UDP packet to nidas */ 34 | void createUDP(char *buffer, int scaleflag, int badData); 35 | }; 36 | 37 | #endif /* STATUS_H */ 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Changelog for iPM code 4 | 5 | ## [Unreleased] 6 | 7 | ## [0.1] - 2023-09-10 - First tagged release 8 | 9 | - iPM control program 10 | - simple iPM emulator 11 | - jenkinsfiles for continuous build on CentOS8 and UbuntuBionic 12 | - License 13 | - Documentation 14 | - debian package builder 15 | 16 | ## [0.2] - 2024-09-27 - Updates to options and return codes 17 | 18 | - Move register configuration to a command line option requiring elevated privs 19 | - Update BITRESULT? to print response data as a UDP string of float or hex. 20 | - Update SERNO? and VER? to print return value rather than success/failure 21 | - Add return codes 22 | - Clean up verbose mode for nonprinting chars 23 | - Change -d debug to -H hexidecimal 24 | - Change -p port to -D device 25 | - Clarify meaning of procqueries and port in Usage statement 26 | - Add typical examples to Usage statement 27 | 28 | - Restructure code; naiipm class becoming unweildly; split into logical classes 29 | - Break up unit tests to mirror restucturing 30 | - Default packets sent to nidas are now hex. The ipm.xml file has been updated to apply scaling during processing. 31 | -------------------------------------------------------------------------------- /src/status.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include "status.h" 6 | 7 | ipmStatus::ipmStatus() 8 | { 9 | } 10 | 11 | ipmStatus::~ipmStatus() 12 | { 13 | } 14 | 15 | void ipmStatus::parse(uint8_t *cp, uint16_t *sp) 16 | { 17 | 18 | // There is a typo in the programming manual. Byte should start at 19 | // zero. 20 | status.OPSTATE = cp[0]; // Operating State 21 | // OpState: 0 - Off; 1 = reserved; 2 - reset; 3 - Tripped; 4 - Failed 22 | status.POWEROK = cp[1]; // PowerOK (1 - power good; 0 - no good) 23 | status.TRIPFLAGS = (((long)sp[2]) << 16) | sp[1]; 24 | status.CAUTIONFLAGS = (((long)sp[4]) << 16) | sp[3]; 25 | status.BITSTAT = sp[5]; // bitStatus 26 | } 27 | 28 | void ipmStatus::createUDP(char *buffer, int scaleflag, int badData) 29 | { 30 | if (scaleflag >= 1) { 31 | snprintf(buffer, 255, "STATUS,%u,%u,%u,%u,%u,%d\r\n", 32 | status.OPSTATE, status.POWEROK, status.TRIPFLAGS, 33 | status.CAUTIONFLAGS, status.BITSTAT, badData); 34 | } else { 35 | snprintf(buffer, 255, "STATUS,%02x,%02x,%04x,%04x,%04x\r\n", 36 | status.OPSTATE, status.POWEROK, status.TRIPFLAGS, 37 | status.CAUTIONFLAGS, status.BITSTAT); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/cmd_gtest.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include 6 | // sytem header includes must be before private public def 7 | #define private public // so can test private functions 8 | 9 | namespace cmdtests 10 | { 11 | #include "../src/argparse.cc" 12 | #include "../src/cmd.cc" 13 | } 14 | 15 | using namespace cmdtests; 16 | 17 | class CmdTest : public ::testing::Test { 18 | private: 19 | ipmArgparse _args; 20 | ipmCmd commands; 21 | 22 | void SetUp() 23 | { 24 | } 25 | 26 | void TearDown() 27 | { 28 | } 29 | }; 30 | 31 | /******************************************************************** 32 | ** Test verifying a command 33 | ******************************************************************** 34 | */ 35 | 36 | TEST_F(CmdTest, ipmVerifyInvalid) 37 | { 38 | testing::internal::CaptureStdout(); 39 | 40 | // Invalid iPM query 41 | _args.setCmd("REC"); 42 | commands.verify(_args.Cmd()); 43 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 44 | "Command REC is invalid. Please enter a valid command\n"); 45 | } 46 | 47 | TEST_F(CmdTest, ipmVerifyValid) 48 | { 49 | testing::internal::CaptureStdout(); 50 | 51 | // Invalid iPM query 52 | _args.setCmd("RECORD?"); 53 | commands.verify(_args.Cmd()); 54 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 55 | ""); 56 | } 57 | -------------------------------------------------------------------------------- /src/bitresult.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include 6 | #include 7 | 8 | #ifndef BITRESULT_H 9 | #define BITRESULT_H 10 | 11 | class ipmBitresult 12 | { 13 | 14 | private: 15 | 16 | struct 17 | { 18 | uint16_t bitStatus; 19 | uint16_t hREFV; // Half-Ref voltage (4.89mV) 20 | uint16_t VREFV; // VREF voltage (4.89mV) 21 | uint16_t FIVEV; // +5V voltage (9.78mV) 22 | uint16_t FIVEVA; // +5VA voltage (9.78mV) 23 | uint16_t RDV; // Relay Drive Voltage (53.76mV) 24 | // bytes 13-14 and 15-16 are reserved 25 | uint16_t ITVA; // Phase A input test voltage (4.89mV) - reserved 26 | uint16_t ITVB; // Phase B input test voltage (4.89mV) - reserved 27 | uint16_t ITVC; // Phase C input test voltage (4.89mV) - reserved 28 | uint16_t TEMP; // Temperature (0.1C) 29 | } bitresult; 30 | 31 | 32 | public: 33 | 34 | //unit conversions 35 | float _deci; // 0.1 36 | 37 | ipmBitresult(); 38 | ~ipmBitresult(); 39 | 40 | /* Parse response to the BITRESULT command into component variables */ 41 | void parse(uint16_t *sp); 42 | /* Build a comma delimited string */ 43 | void createUDP(char *buffer, int scaleflag); 44 | /* Return temperature scaled to degrees C */ 45 | float getTemperature(); 46 | }; 47 | 48 | #endif /* BITRESULT_H */ 49 | -------------------------------------------------------------------------------- /src/cmd.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include "cmd.h" 6 | #include 7 | 8 | ipmCmd::ipmCmd() 9 | { 10 | // Map message to expected response 11 | _ipm_commands = 12 | { 13 | { "OFF", "OK\n"}, // Turn Device OFF 14 | { "RESET", "OK\n"}, // Turn Device ON (reset) 15 | { "SERNO?", "^[0-9]{6}\n$"}, // Query Serial number (which changes) 16 | { "VER?", "VER A022(L) 2018-11-13\n"}, // Query Firmware Ver 17 | { "TEST", "OK\n"}, // Execute build-in self test 18 | { "BITRESULT?", "24\n"}, // Query self test result 19 | { "ADR", ""}, // Device Address Selection 20 | { "MEASURE?", "34\n"}, // Device Measurement 21 | { "STATUS?", "12\n"}, // Device Status 22 | { "RECORD?", "68\n"}, // Device Statistics 23 | }; 24 | } 25 | 26 | ipmCmd::~ipmCmd() 27 | { 28 | } 29 | 30 | void ipmCmd::printMenu() 31 | { 32 | std::cout << "=========================================" << std::endl; 33 | std::cout << "Type one of the following iPM commands or" << std::endl; 34 | std::cout << "enter 'q' to quit" << std::endl; 35 | std::cout << "=========================================" << std::endl; 36 | for (auto msg : _ipm_commands) { 37 | std::cout << msg.first << std::endl; 38 | } 39 | } 40 | 41 | bool ipmCmd::verify(std::string cmd) 42 | { 43 | // Confirm command is in list of acceptable command 44 | if (not _ipm_commands.count(cmd)) 45 | { 46 | std::cout << "Command " << cmd << " is invalid. Please enter a " << 47 | "valid command" << std::endl; 48 | return false; 49 | } else { 50 | return true; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/measure.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include 6 | #include 7 | 8 | #ifndef MEASURE_H 9 | #define MEASURE_H 10 | class ipmMeasure 11 | { 12 | 13 | private: 14 | 15 | struct 16 | { 17 | uint16_t FREQ; // AC Power Frequency 18 | uint16_t TEMP; // Temperature 19 | uint16_t VRMSA; // Phase A RMS AC Voltage 20 | uint16_t VRMSB; // Phase B RMS AC Voltage 21 | uint16_t VRMSC; // Phase C RMS AC Voltage 22 | uint16_t VPKA; // Phase A Peak AC Voltage 23 | uint16_t VPKB; // Phase B Peak AC Voltage 24 | uint16_t VPKC; // Phase C Peak AC Voltage 25 | uint16_t VDCA; // Phase A Voltage, DC Component 26 | uint16_t VDCB; // Phase B Voltage, DC Component 27 | uint16_t VDCC; // Phase C Voltage, DC Component 28 | uint16_t PHA; // Phase A Voltage, AC Phase Angle 29 | uint16_t PHB; // Phase B Voltage, AC Phase Angle 30 | uint16_t PHC; // Phase C Voltage, AC Phase Angle 31 | uint8_t THDA; // Phase A Voltage THD 32 | uint8_t THDB; // Phase B Voltage THD 33 | uint8_t THDC; // Phase C Voltage THD 34 | uint8_t POWEROK; // Power OK, All phases 35 | } measure; 36 | 37 | 38 | public: 39 | 40 | //unit conversions 41 | float _deci; // 0.1 42 | float _milli; // 0.001 43 | 44 | ipmMeasure(); 45 | ~ipmMeasure(); 46 | 47 | /* Parse response to the MEASURE command into component variables */ 48 | void parse(uint8_t *cp, uint16_t *sp); 49 | /* Build a comma delimited string to send as a UDP packet to nidas */ 50 | void createUDP(char *buffer, int scaleflag); 51 | }; 52 | 53 | #endif /* MEASURE_H */ 54 | -------------------------------------------------------------------------------- /tests/status_gtest.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include 6 | // sytem header includes must be before private public def 7 | #define private public // so can test private functions 8 | #include "../src/status.cc" 9 | 10 | class StatusTest : public ::testing::Test { 11 | public: 12 | char buffer[1000]; 13 | private: 14 | ipmStatus _status; 15 | 16 | void SetUp() 17 | { 18 | // Set binary data to some actual data from the iPM 19 | unsigned char status[] = {2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; 20 | memcpy(buffer, status, 12); 21 | } 22 | 23 | void TearDown() 24 | { 25 | } 26 | }; 27 | 28 | /******************************************************************** 29 | ** Test parsing response string 30 | ******************************************************************** 31 | */ 32 | TEST_F(StatusTest, Parse) 33 | { 34 | // STATUS,2,1,0,0,0 35 | char *data = &buffer[0]; 36 | uint8_t *cp = (uint8_t *)data; 37 | uint16_t *sp = (uint16_t *)data; 38 | _status.parse(cp, sp); 39 | EXPECT_EQ(_status.status.OPSTATE,2); 40 | EXPECT_EQ(_status.status.POWEROK,1); 41 | EXPECT_EQ(_status.status.TRIPFLAGS,0); 42 | EXPECT_EQ(_status.status.CAUTIONFLAGS,0); 43 | EXPECT_EQ(_status.status.BITSTAT,0); 44 | } 45 | 46 | /******************************************************************** 47 | ** Test creating UDP packet 48 | ******************************************************************** 49 | */ 50 | TEST_F(StatusTest, CreateUDP) 51 | { 52 | std::string str; 53 | 54 | _status.createUDP(buffer, 1, 0); // scaling turned on 55 | str = buffer; 56 | EXPECT_EQ(str,"STATUS,2,1,0,0,0,0\r\n"); 57 | 58 | _status.createUDP(buffer, 0, 0); // scaling turned off 59 | str = buffer; 60 | EXPECT_EQ(str,"STATUS,02,01,0000,0000,0000\r\n"); 61 | } 62 | -------------------------------------------------------------------------------- /src/bitresult.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include "bitresult.h" 6 | 7 | ipmBitresult::ipmBitresult() 8 | { 9 | // unit conversions 10 | _deci = 0.1; 11 | } 12 | 13 | ipmBitresult::~ipmBitresult() 14 | { 15 | } 16 | 17 | void ipmBitresult::parse(uint16_t *sp) 18 | { 19 | 20 | // There is a typo in the programming manual. Byte should start at 21 | // zero. 22 | bitresult.bitStatus = sp[0]; 23 | bitresult.hREFV = sp[1]; // Half-Ref voltage (4.89mV) 24 | bitresult.VREFV = sp[2]; // VREF voltage (4.89mV) 25 | bitresult.FIVEV = sp[3]; // +5V voltage (9.78mV) 26 | bitresult.FIVEVA = sp[4]; // +5VA voltage (9.78mV) 27 | bitresult.RDV = sp[5]; // Relay Drive Voltage (53.76mV) 28 | // bytes 13-14 and 15-16 are reserved 29 | bitresult.ITVA = sp[8]; // Phase A input test voltage (4.89mV) - reserved 30 | bitresult.ITVB = sp[9]; // Phase B input test voltage (4.89mV) - reserved 31 | bitresult.ITVC = sp[10]; // Phase C input test voltage (4.89mV) - reserved 32 | bitresult.TEMP = sp[11]; // Temperature (0.1C) 33 | } 34 | 35 | void ipmBitresult::createUDP(char *buffer, int scaleflag) 36 | { 37 | if (scaleflag >= 1) { 38 | snprintf(buffer, 255, "BITRESULT,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f," 39 | "%.2f,%.2f,%.2f\r\n", (float) bitresult.bitStatus, 40 | (float) bitresult.hREFV, (float) bitresult.VREFV, 41 | (float) bitresult.FIVEV, (float) bitresult.FIVEVA, 42 | (float) bitresult.RDV, (float) bitresult.ITVA, 43 | (float) bitresult.ITVB, (float) bitresult.ITVC, 44 | bitresult.TEMP * _deci); 45 | ; 46 | } else { 47 | snprintf(buffer, 255, "BITRESULT,%04x,%04x,%04x,%04x,%04x,%04x,%04x," 48 | "%04x,%04x,%04x\r\n", bitresult.bitStatus, 49 | bitresult.hREFV, bitresult.VREFV, bitresult.FIVEV, 50 | bitresult.FIVEVA, bitresult.RDV, bitresult.ITVA, 51 | bitresult.ITVB, bitresult.ITVC, bitresult.TEMP); 52 | } 53 | } 54 | 55 | float ipmBitresult::getTemperature() 56 | { 57 | return bitresult.TEMP * _deci; 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Model iPM - Intelligent Power Monitor control software 2 | EOL/RAF code to control the NAI iPM and send UDP packets to nidas. Main program can be forked by nidas dsm process or run standalone. 3 | 4 | ## Running the code 5 | This code can be run four different ways: from nidas, or from the command line with a menu, specifying an address and command, or free running as would be called from nidas. 6 | 7 | ### From nidas 8 | To run from nidas, use the ipm.xml file in this directory as a template to add the iPM to the project xml files. Download and install this software to the DSM where the iPM is mounted, eg `git clone this_repo`, `scons`, and `scons install`. Then when dsm_server is started, it will launch this code, initialize the iPM and send commands as configured in the XML. 9 | 10 | ### From the command line 11 | To run from the command line, login to the DSM where the iPM is mounted, and enter one of the following command line patterns: 12 | 13 | ``` 14 | > ipm_ctrl -m -r -b -n -0 -D " 15 | ``` 16 | will loop over command as specified in procqueries at the rates specified in measurerate and recordperiod 17 | 18 | ``` 19 | > ipm_ctrl -b -D -i" 20 | ``` 21 | to run in interactive mode and print a menu. 22 | 23 | ``` 24 | > ipm_ctrl -b -D -i -a
-c " 25 | ``` 26 | to send a single command to given address 27 | 28 | ## Building the software 29 | `scons` will build ipm_ctrl 30 | 31 | ## Developmemnt 32 | 33 | ### Running with the emulator 34 | The required python environment is captured in the ipmenv YAML file. The first time you use the environment, you will have to create it: 35 | ``` 36 | > conda env create -f ipmenv.yml 37 | ``` 38 | 39 | To activate the environment: 40 | 41 | ``` 42 | > conda activate ipmenv 43 | ``` 44 | 45 | Depending on your environment, you may also need to install other packages such as socat: eg `brew install socat` on Mac. 46 | To run with the emulator, run 47 | 48 | ``` 49 | > python3 emulate.py 50 | ``` 51 | 52 | In a separate window run one of the `ipm_ctrl` commands above and append `-e` to the command. The emulator responds more slowly than the iPM. The -e increases the timeout period. 53 | 54 | ### Unit tests 55 | This software uses googletest for unit testing. 56 | 57 | `scons tests` will build the tests. 58 | 59 | `tests/g_test` will run the tests. 60 | 61 | ### Deployment 62 | To deploy the code via a dpkg, run `./build_dpkg` 63 | -------------------------------------------------------------------------------- /snd_udp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | ########################################################################## 4 | # Ugly script to test XML parsing of iPM packets. Could use work but it 5 | # gets the job done. 6 | # 7 | # Written in Python 3 8 | # 9 | # COPYRIGHT: University Corporation for Atmospheric Research, 2023 10 | ########################################################################## 11 | """ 12 | import socket 13 | import time 14 | import argparse 15 | 16 | 17 | def parse_args(): 18 | """ Instantiate a command line argument parser """ 19 | 20 | # Define command line arguments which can be provided 21 | parser = argparse.ArgumentParser( 22 | description="Script to emulate a UDP packet") 23 | args = parser.parse_args() 24 | 25 | return args 26 | 27 | 28 | def main(): 29 | 30 | args = parse_args() 31 | 32 | # Instrument emulator 33 | udp_id = "STATUS" 34 | udp_id2 = "MEASURE" 35 | udp_id3 = "RECORD" 36 | # To send from the aircraft to the ground 37 | udp_send_port = 30101 # nidas 38 | udp_send_port2 = 30101 # nidas 39 | udp_send_port3 = 30103 # nidas 40 | udp_ip = "172.16.47.154" # from /etc/dhcp/dhcpd_ac.conf on the aircraft 41 | 42 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 43 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 44 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 45 | 46 | while 1: 47 | # STATUS 48 | buffer = "%s,%s,0,0,0\r\n" % \ 49 | (udp_id, time.strftime("%Y%m%dT%H%M%S", time.gmtime())) 50 | print(buffer) 51 | 52 | # MEASURE 1-phase 53 | buffer2 = "%s,%s,99,99,99,99,99,99,99\r\n" % \ 54 | (udp_id2, time.strftime("%Y%m%dT%H%M%S", time.gmtime())) 55 | print(buffer2) 56 | 57 | # RECORD 1-phase 58 | buffer3 = "%s,%s,99,99,99,99,99,99,99,99,99,99,99,99,99,99\r\n" % \ 59 | (udp_id3, time.strftime("%Y%m%dT%H%M%S", time.gmtime())) 60 | print(buffer3) 61 | 62 | if sock: 63 | bytes = sock.sendto(buffer.encode('utf-8'), 64 | (udp_ip, udp_send_port)) 65 | if udp_send_port2: 66 | bytes = sock.sendto(buffer2.encode('utf-8'), 67 | (udp_ip, udp_send_port2)) 68 | if udp_send_port3: 69 | bytes = sock.sendto(buffer3.encode('utf-8'), 70 | (udp_ip, udp_send_port3)) 71 | # print(bytes) 72 | 73 | time.sleep(1) 74 | 75 | 76 | if __name__ == "__main__": 77 | main() 78 | -------------------------------------------------------------------------------- /naiipm.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #ifndef NAIIPM_H 11 | #define NAIIPM_H 12 | 13 | #include "src/argparse.h" 14 | #include "src/cmd.h" 15 | 16 | extern ipmArgparse args; 17 | 18 | /** 19 | * Class to initialize and control North Atlantic Industries 20 | * Intelligent Power Monitor (iPM) 21 | */ 22 | class naiipm 23 | { 24 | public: 25 | naiipm(); 26 | ~naiipm(); 27 | 28 | int open_port(); 29 | void close_port(int fd); 30 | 31 | void open_udp(const char *ip); 32 | void send_udp(const char *buffer, int adr); 33 | void close_udp(int adr); 34 | 35 | bool setInteractiveMode(int fd); 36 | void singleCommand(int fd); 37 | bool readInput(int fd); 38 | bool clear(int fd, int addr); 39 | bool init(int fd); 40 | bool loop(int fd); 41 | 42 | void setRecordFreq(); 43 | void sleep(); 44 | 45 | private: 46 | char buffer[1000]; 47 | 48 | void parseData(std::string cmd, int addrIndex); 49 | void parseBitresult(uint16_t *sp); 50 | 51 | void get_response(int fd, int len, bool bin); 52 | void flush(int fd); 53 | virtual bool send_command(int fd, std::string msg, std::string msgarg = ""); 54 | void parse_binary(std::string cmd); 55 | 56 | uint_fast32_t get_baud(); 57 | 58 | int _recordCount; 59 | int _recordFreq; 60 | long _sleeptime; 61 | 62 | virtual bool setActiveAddress(int fd, int addr); 63 | void rmAddr(int i); 64 | 65 | void setData(std::string cmd, int binlen); 66 | char* getData(std::string msg) 67 | { return _ipm_data.find(msg)->second; } 68 | 69 | struct sockaddr_in _servaddr[8]; 70 | int _sock[8]; 71 | 72 | // Map message to expected response 73 | std::map_ipm_commands; 74 | 75 | // Map message to data string 76 | char _bitdata[25]; 77 | char _measuredata[35]; 78 | char _statusdata[13]; 79 | char _recorddata[69]; 80 | typedef std::map IpmMap; 81 | IpmMap _ipm_data; 82 | 83 | // unit conversions 84 | float _deci; // 0.1 85 | float _milli; // 0.001 86 | 87 | // Bad data counter 88 | int _badData; 89 | 90 | void trackBadData(); 91 | 92 | }; 93 | 94 | #endif /* NAIIPM_H */ 95 | -------------------------------------------------------------------------------- /tests/bitresult_gtest.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include 6 | // sytem header includes must be before private public def 7 | #define private public // so can test private functions 8 | #include "../src/bitresult.cc" 9 | 10 | class BitresultTest : public ::testing::Test { 11 | public: 12 | char buffer[1000]; 13 | private: 14 | ipmBitresult _bitresult; 15 | 16 | void SetUp() 17 | { 18 | // Set binary data to some actual data from the iPM 19 | unsigned char bitresult[] = {0,0,254,1,255,3,25,2,24,2,88,1,0,0,0,0, 20 | 39,2,249,1,249,1,253,1}; 21 | memcpy(buffer,bitresult,25); 22 | } 23 | 24 | void TearDown() 25 | { 26 | } 27 | }; 28 | 29 | /******************************************************************** 30 | ** Test parsing response string 31 | ******************************************************************** 32 | */ 33 | TEST_F(BitresultTest, Parse) 34 | { 35 | char *data = &buffer[0]; 36 | uint16_t *sp = (uint16_t *)data; 37 | _bitresult.parse(sp); 38 | EXPECT_EQ(_bitresult.bitresult.bitStatus, 0); 39 | EXPECT_EQ(_bitresult.bitresult.hREFV, 510); 40 | EXPECT_EQ(_bitresult.bitresult.VREFV, 1023); 41 | EXPECT_EQ(_bitresult.bitresult.FIVEV, 537); 42 | EXPECT_EQ(_bitresult.bitresult.FIVEVA, 536); 43 | EXPECT_EQ(_bitresult.bitresult.RDV, 344); 44 | EXPECT_EQ(_bitresult.bitresult.ITVA, 551); 45 | EXPECT_EQ(_bitresult.bitresult.ITVB, 505); 46 | EXPECT_EQ(_bitresult.bitresult.ITVC, 505); 47 | EXPECT_EQ(_bitresult.bitresult.TEMP,509); 48 | } 49 | 50 | /******************************************************************** 51 | ** Test creating UDP packet 52 | ******************************************************************** 53 | */ 54 | TEST_F(BitresultTest, CreateUDP) 55 | { 56 | std::string str; 57 | 58 | _bitresult.createUDP(buffer, 1); // scaling turned on 59 | str = buffer; 60 | EXPECT_EQ(str,"BITRESULT,0.00,510.00,1023.00,537.00,536.00,344.00,551.00,505.00,505.00,50.90\r\n"); 61 | 62 | _bitresult.createUDP(buffer, 0); // scaling turned off 63 | str = buffer; 64 | EXPECT_EQ(str,"BITRESULT,0000,01fe,03ff,0219,0218,0158,0227,01f9,01f9,01fd\r\n"); 65 | } 66 | 67 | /******************************************************************** 68 | ** Test retrieving iPM temperature 69 | ******************************************************************** 70 | */ 71 | TEST_F(BitresultTest, GetTemperature) 72 | { 73 | EXPECT_FLOAT_EQ(_bitresult.getTemperature(),50.9); 74 | } 75 | -------------------------------------------------------------------------------- /ctrl.cc: -------------------------------------------------------------------------------- 1 | /************************************************************************* 2 | * Program to send commands to an Intelligent Power Monitor (iPM), receive 3 | * returned data and generate a UDP packet to be sent to nidas. 4 | * 5 | * IN DEVELOPMENT: 6 | * Questions are marked "Question:" 7 | * Incomplete items are marked "TBD" 8 | * 9 | * 2024, Copyright University Corporation for Atmospheric Research 10 | ************************************************************************* 11 | */ 12 | 13 | #include "naiipm.h" 14 | #include "src/argparse.h" 15 | #include 16 | #include 17 | #include 18 | 19 | const char *acserver = "192.168.84.2"; 20 | 21 | int main(int argc, char * argv[]) 22 | { 23 | args.process(argc, argv); 24 | 25 | naiipm ipm; 26 | 27 | // set up logging to a timestamped file 28 | char time_buf[100]; 29 | time_t now = time({});; 30 | strftime(time_buf, 100, "%Y%m%d_%H%M%S", gmtime(&now)); 31 | 32 | std::string filename = "/var/log/ads/ipm_" + (std::string)time_buf + ".log"; 33 | std::ofstream logfile(filename); 34 | auto oldbuf = std::cout.rdbuf( logfile.rdbuf()); 35 | 36 | // If in interactive or debug mode, don't log to a file 37 | for (int i=0; i< argc; ++i) { 38 | if ((strcmp(argv[i],"-i") == 0) || (strcmp(argv[i],"-d") == 0)) { 39 | // go back to writing to stdout 40 | std::cout << "Start logging" << std::endl; 41 | std::cout.rdbuf(oldbuf); 42 | std::remove(filename.c_str()); // remove logfile 43 | } 44 | } 45 | 46 | int fd = ipm.open_port(); 47 | 48 | ipm.open_udp(acserver); 49 | 50 | bool status = true; 51 | if (args.Interactive()) 52 | { 53 | bool mode = ipm.setInteractiveMode(fd); 54 | // if mode is True, successfully configured a command line query, 55 | // so request that now. If false, just exit. 56 | if (mode == true) 57 | { 58 | int addr = atoi(args.Address()); 59 | ipm.clear(fd, addr); // Clear garbage off port 60 | ipm.singleCommand(fd); 61 | } 62 | return 0; 63 | } else { 64 | if (ipm.init(fd)) 65 | { 66 | if (args.Verbose()) 67 | { 68 | std::cout << "Device successfully initialized" << std::endl; 69 | } 70 | } else { 71 | std::cout << "Device failed to initialize" << std::endl; 72 | ipm.close_port(fd); 73 | return 1; 74 | } 75 | 76 | // Cycle on requested commands 77 | while (true) 78 | { 79 | ipm.setRecordFreq(); 80 | status = ipm.loop(fd); 81 | ipm.sleep(); 82 | } 83 | } 84 | 85 | std::cout << "Exiting ipm_ctrl" << std::endl; 86 | 87 | // Close socket descriptor 88 | ipm.close_udp(-1); 89 | 90 | ipm.close_port(fd); 91 | 92 | return 0; 93 | } 94 | -------------------------------------------------------------------------------- /tests/measure_gtest.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include 6 | // sytem header includes must be before private public def 7 | #define private public // so can test private functions 8 | #include "../src/measure.cc" 9 | 10 | class MeasureTest : public ::testing::Test { 11 | public: 12 | char buffer[1000]; 13 | private: 14 | ipmMeasure _measure; 15 | 16 | void SetUp() 17 | { 18 | // Set binary data to some actual data from the iPM 19 | unsigned char measure[] = {88, 2, 0, 0, 5, 2, 139, 4, 139, 4, 0, 0, 4, 20 | 6, 252, 5, 0, 0, 28, 0, 28, 0, 9, 0, 201, 13, 200, 6, 7, 7, 27, 27, 21 | 1, 1}; 22 | memcpy(buffer, measure, 34); 23 | } 24 | 25 | void TearDown() 26 | { 27 | } 28 | }; 29 | 30 | /******************************************************************** 31 | ** Test parsing response string 32 | ******************************************************************** 33 | */ 34 | TEST_F(MeasureTest, Parse) 35 | { 36 | //MEASURE,60.00,51.70,116.30,116.30,0.00,154.00,153.20,0.00,0.0280,0.0280, 37 | // 0.0090,352.90,173.60,179.90,2.70,2.70,0.10,1 38 | char *data = &buffer[0]; 39 | uint8_t *cp = (uint8_t *)data; 40 | uint16_t *sp = (uint16_t *)data; 41 | _measure.parse(cp, sp); 42 | EXPECT_EQ(_measure.measure.FREQ,600); 43 | EXPECT_EQ(_measure.measure.TEMP,517); 44 | EXPECT_EQ(_measure.measure.VRMSA,1163); 45 | EXPECT_EQ(_measure.measure.VRMSB,1163); 46 | EXPECT_EQ(_measure.measure.VRMSC,0); 47 | EXPECT_EQ(_measure.measure.VPKA,1540); 48 | EXPECT_EQ(_measure.measure.VPKB,1532); 49 | EXPECT_EQ(_measure.measure.VPKC,0); 50 | EXPECT_EQ(_measure.measure.VDCA,28); 51 | EXPECT_EQ(_measure.measure.VDCB,28); 52 | EXPECT_EQ(_measure.measure.VDCC,9); 53 | EXPECT_EQ(_measure.measure.PHA,3529); 54 | EXPECT_EQ(_measure.measure.PHB,1736); 55 | EXPECT_EQ(_measure.measure.PHC,1799); 56 | EXPECT_EQ(_measure.measure.THDA,27); 57 | EXPECT_EQ(_measure.measure.THDB,27); 58 | EXPECT_EQ(_measure.measure.THDC,1); 59 | EXPECT_EQ(_measure.measure.POWEROK,1); 60 | } 61 | 62 | /******************************************************************** 63 | ** Test creating UDP packet 64 | ******************************************************************** 65 | */ 66 | TEST_F(MeasureTest, CreateUDP) 67 | { 68 | std::string str; 69 | 70 | _measure.createUDP(buffer, 1); // scaling turned on 71 | str = buffer; 72 | EXPECT_EQ(str,"MEASURE,60.00,51.70,116.30,116.30,0.00,154.00,153.20,0.00,0.0280,0.0280,0.0090,352.90,173.60,179.90,2.70,2.70,0.10,1\r\n"); 73 | 74 | _measure.createUDP(buffer, 0); // scaling turned off 75 | str = buffer; 76 | EXPECT_EQ(str,"MEASURE,0258,0205,048b,048b,0000,0604,05fc,0000,001c,001c,0009,0dc9,06c8,0707,1b,1b,01,01\r\n"); 77 | } 78 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """ 2 | ########################################################################## 3 | # This file contains variables that hold configuration details for the iPM 4 | # including commands and responses, the format of data included in the 5 | # device response, and sample data (for testing). 6 | # 7 | # Written in Python 3 8 | # 9 | # COPYRIGHT: University Corporation for Atmospheric Research, 2022 10 | ########################################################################## 11 | """ 12 | from collections import namedtuple 13 | import numpy 14 | 15 | # I am playing with using a namedtuple to hold the components of 16 | # the reported response. Still not sure this is better than a 17 | # dictionary... 18 | 19 | # Some sample data from instrument runs in the DSM lab, with a few changes 20 | # since the Python to C link is causing issues with some non-printing chars. 21 | TEST_DATA = '\0\0?\0?\0\0\0\0\0P\0\0\0\0\0#\0?\0?\0L\x02\n' # 24 bytes 22 | MEASURE_DATA = '\x58\x02\x00\x00\x05\x02\x8b\x02\x8b\x02\x00\x00\x06\x06' \ 23 | '\xFC\x06\x00\x00\x1B\x00\x1B\x00\x09\x00\xC9\x0D\xC8\x06' \ 24 | '\x06\x06\x1B\x1B\x01\x01\n' # 34 bytes 25 | STATUS_DATA = '\0\0\0\0\0\0\0\0\0\0\0\0\n' # 12 bytes 26 | RECORD_DATA = '\x00\x02\x63\x00\x00\x00\x8B\x44\x69\x05\x00\x00\x00\x00\x00' \ 27 | '\x00\x00\x00\xD1\x00\x9B\x05\xD1\x00\x9B\x05\x00\x00\x00\x00' \ 28 | '\x45\x02\x58\x02\x00\x00\x5E\x00\x00\x00\x55\x00\x00\x00\x0d' \ 29 | '\x00\x1B\x71\x1B\x71\x01\x01\x05\x06\x5A\x06\x05\x06\x53\x06' \ 30 | '\x00\x00\x18\x00\x0d\x1B\x7C\x08\n' # 68 bytes 31 | 32 | # Tuple to store iPM commands and responses 33 | Command = namedtuple('Command', ['msg', 'response', 'bytes']) 34 | 35 | sequence = [ 36 | Command('OFF', 'OK\n', ''), # Turn Device OFF 37 | Command('RESET', 'OK\n', ''), # Turn Device ON (reset) 38 | Command('SERNO?', '200728\n', ''), # Query Serial number 39 | Command('VER?', 'VER A022(L) 2018-11-13\n', ''), # Query Firmware Ver 40 | Command('TEST', 'OK\n', ''), # Execute build-in self test 41 | Command('BITRESULT?', '24\n', TEST_DATA), # Query self test result 42 | Command('ADR', '', ''), # Device Address Selection 43 | Command('MEASURE?', '34\n', MEASURE_DATA), # Device Measurement 44 | Command('STATUS?', '12\n', STATUS_DATA), # Device Status 45 | Command('RECORD?', '68\n', RECORD_DATA), # Device Statistics 46 | ] 47 | 48 | # Tuple to store byte responses to queries 49 | Data = namedtuple('Data', "len varName description units scale val") 50 | 51 | measure = [ 52 | Data(2, 'FREQ', 'AC Power Frequency', 'Hz', '0.1', numpy.nan), 53 | Data(2, 'reserved', 'unused', '', '1', numpy.nan), 54 | Data(2, 'T', 'Temperature', 'C', '0.1', numpy.nan), 55 | Data(2, 'VRMSA', 'AC Voltage RMS Phase A', 'V', '0.1', numpy.nan), 56 | Data(2, 'VRMSB', 'AC Voltage RMS Phase B', 'V', '0.1', numpy.nan), 57 | Data(2, 'VRMSC', 'AC Voltage RMS Phase C', 'V', '0.1', numpy.nan), 58 | Data(2, 'VPKA', 'AC Voltage Peak Phase A', 'V', '0.001', numpy.nan) 59 | ] 60 | -------------------------------------------------------------------------------- /src/record.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include 6 | #include 7 | 8 | #ifndef RECORD_H 9 | #define RECORD_H 10 | class ipmRecord 11 | { 12 | 13 | private: 14 | 15 | struct 16 | { 17 | uint8_t EVTYPE; // Event Type 18 | uint8_t OPSTATE; // Operating State 19 | uint32_t POWERCNT; // Power Up Count 20 | uint32_t TIME; // Elapsed time since power-up (ms) 21 | uint32_t TFLAG; // Trip Flag 22 | uint32_t CFLAG; // Caution Flag 23 | uint16_t VRMSMINA; // Phase A RMS Voltage Min 24 | uint16_t VRMSMAXA; // Phase A RMS Voltage Max 25 | uint16_t VRMSMINB; // Phase B RMS Voltage Min 26 | uint16_t VRMSMAXB; // Phase B RMS Voltage Max 27 | uint16_t VRMSMINC; // Phase C RMS Voltage Min 28 | uint16_t VRMSMAXC; // Phase C RMS Voltage Max 29 | uint16_t FREQMIN; // Frequency Min 30 | uint16_t FREQMAX; // Frequency Max 31 | uint16_t VDCMINA; // Phase A Voltage, DC Coomponent Min 32 | uint16_t VDCMAXA; // Phase A Voltage, DC Coomponent Max 33 | uint16_t VDCMINB; // Phase B Voltage, DC Coomponent Min 34 | uint16_t VDCMAXB; // Phase B Voltage, DC Coomponent Max 35 | uint16_t VDCMINC; // Phase C Voltage, DC Coomponent Min 36 | uint16_t VDCMAXC; // Phase C Voltage, DC Coomponent Max 37 | uint8_t THDMINA; // Phase A Voltage THD Min 38 | uint8_t THDMAXA; // Phase A Voltage THD Max 39 | uint8_t THDMINB; // Phase B Voltage THD Min 40 | uint8_t THDMAXB; // Phase B Voltage THD Max 41 | uint8_t THDMINC; // Phase C Voltage THD Min 42 | uint8_t THDMAXC; // Phase C Voltage THD Max 43 | uint16_t VPKMINA; // Phase A Peak Voltage Min 44 | uint16_t VPKMAXA; // Phase A Peak Voltage Max 45 | uint16_t VPKMINB; // Phase B Peak Voltage Min 46 | uint16_t VPKMAXB; // Phase B Peak Voltage Max 47 | uint16_t VPKMINC; // Phase C Peak Voltage Min 48 | uint16_t VPKMAXC; // Phase C Peak Voltage Max 49 | uint32_t CRC; // CRC-32 50 | } record; 51 | 52 | 53 | public: 54 | 55 | //unit conversions 56 | float _deci; // 0.1 57 | float _milli; // 0.001 58 | 59 | ipmRecord(); 60 | ~ipmRecord(); 61 | 62 | /* Parse response to the RECORD command into component variables */ 63 | void parse(uint8_t *cp, uint16_t *sp, uint32_t *lp); 64 | /* Build a comma delimited string to send as a UDP packet to nidas */ 65 | void createUDP(char *buffer, int scaleflag); 66 | /* Return time since power-up in minutes */ 67 | float getTimeSincePowerup(); 68 | 69 | // CRC validation 70 | uint32_t _crcTable[256]; 71 | void generateCRCTable(); 72 | uint32_t calculateCRC32(unsigned char *buf, int ByteCount); 73 | void checkCRC(uint8_t *cp, uint32_t crc); 74 | }; 75 | 76 | #endif /* RECORD_H */ 77 | -------------------------------------------------------------------------------- /src/measure.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include "measure.h" 6 | 7 | ipmMeasure::ipmMeasure() 8 | { 9 | // unit conversions 10 | _deci = 0.1; 11 | _milli = 0.001; 12 | } 13 | 14 | ipmMeasure::~ipmMeasure() 15 | { 16 | } 17 | 18 | void ipmMeasure::parse(uint8_t *cp, uint16_t *sp) 19 | { 20 | 21 | // There is a typo in the programming manual. Byte should start at 22 | // zero. 23 | measure.FREQ = sp[0]; // Frequency (0.1Hz) 24 | // bytes 3-4 are reserved 25 | measure.TEMP = sp[2]; // Temperature (0.1 C) 26 | measure.VRMSA = sp[3]; // Phase A voltage rms (0.1 V) 27 | measure.VRMSB = sp[4]; // Phase B voltage rms (0.1 V) 28 | measure.VRMSC = sp[5]; // Phase C voltage rms (0.1 V) 29 | measure.VPKA = sp[6]; // Phase A voltage peak (0.1 V) 30 | measure.VPKB = sp[7]; // Phase B voltage peak (0.1 V) 31 | measure.VPKC = sp[8]; // Phase C voltage peak (0.1 V) 32 | measure.VDCA = sp[9]; // Phase A DC component (1 mV DC) 33 | measure.VDCB = sp[10]; // Phase B DC component (1 mV DC) 34 | measure.VDCC = sp[11]; // Phase C DC component (1 mV DC) 35 | measure.PHA = sp[12]; // Phase A Phase Angle (0.1 deg) rel. to Phase A 36 | measure.PHB = sp[13]; // Phase B Phase Angle (0.1 deg) rel. to Phase B 37 | measure.PHC = sp[14]; // Phase C Phase Angle (0.1 deg) rel. to Phase C 38 | measure.THDA = cp[30]; // Phase A THD (0.1%) 39 | measure.THDB = cp[31]; // Phase B THD (0.1%) 40 | measure.THDC = cp[32]; // Phase C THD (0.1%) 41 | measure.POWEROK = cp[33]; // PowerOK (1 = power good, 0 = no good) 42 | } 43 | 44 | void ipmMeasure::createUDP(char *buffer, int scaleflag) 45 | { 46 | if (scaleflag >= 1) { 47 | snprintf(buffer, 255, "MEASURE,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f," 48 | "%.2f,%.4f,%.4f,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%u\r\n", 49 | measure.FREQ * _deci, measure.TEMP * _deci, 50 | measure.VRMSA * _deci, measure.VRMSB * _deci, 51 | measure.VRMSC * _deci, measure.VPKA * _deci, 52 | measure.VPKB * _deci, measure.VPKC * _deci, 53 | measure.VDCA * _milli, measure.VDCB * _milli, 54 | measure.VDCC * _milli, measure.PHA * _deci, 55 | measure.PHB * _deci, measure.PHC * _deci, 56 | measure.THDA * _deci, measure.THDB * _deci, 57 | measure.THDC * _deci, measure.POWEROK); 58 | } else { 59 | snprintf(buffer, 255, "MEASURE,%04x,%04x,%04x,%04x,%04x,%04x,%04x," 60 | "%04x,%04x,%04x,%04x,%04x,%04x,%04x,%02x,%02x,%02x,%02x\r\n", 61 | measure.FREQ, measure.TEMP, 62 | measure.VRMSA, measure.VRMSB, 63 | measure.VRMSC, measure.VPKA, 64 | measure.VPKB, measure.VPKC, 65 | measure.VDCA, measure.VDCB, 66 | measure.VDCC, measure.PHA, 67 | measure.PHB, measure.PHC, 68 | measure.THDA, measure.THDB, 69 | measure.THDC, measure.POWEROK); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/argparse.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #ifdef __linux__ 10 | #include 11 | #endif 12 | #include 13 | 14 | #include "cmd.h" 15 | 16 | #ifndef ARGPARSE_H 17 | #define ARGPARSE_H 18 | 19 | extern ipmCmd commands; 20 | 21 | class ipmArgparse 22 | { 23 | public: 24 | ipmArgparse(); 25 | ~ipmArgparse(); 26 | 27 | void setDevice(const char device[]) { _device = device; } 28 | const char* Device() { return _device; } 29 | 30 | const char* measureRate() { return _measureRate; } 31 | void setRate(const char rate[]) { _measureRate = rate; } 32 | 33 | const char* recordPeriod() { return _recordPeriod; }; 34 | void setPeriod(const char period[]) { _recordPeriod = period; } 35 | 36 | void setBaud(const char baud[]) { _baudRate = baud; } 37 | const char* BaudRate() { return _baudRate; } 38 | 39 | void setNumAddr(const char numaddr[]) { _numaddr = atoi(numaddr); } 40 | int numAddr() { return _numaddr; } 41 | void updateNumAddr(int naddr) { _numaddr = naddr; } 42 | 43 | void setAddrInfo(int optopt, char addrinfo[]) 44 | { _addrinfo[optopt] = addrinfo; } 45 | bool parse_addrInfo(int index); 46 | char* addrInfo(int index) { return _addrinfo[index]; } 47 | void updateAddrInfo(int index, char* adinfo) 48 | { _addrinfo[index] = adinfo; } 49 | 50 | int Addr(int index) { return _addr[index]; } 51 | void setAddr(int index, char* ptr) { _addr[index] = atoi(ptr); } 52 | void updateAddr(int index, int addr) { _addr[index] = addr; } 53 | 54 | int Procqueries(int index) { return _procqueries[index]; } 55 | void setProcqueries(int index, char* ptr) 56 | { _procqueries[index] = atoi(ptr); } 57 | void updateProcqueries(int index, int procq) 58 | { _procqueries[index] = procq; } 59 | 60 | int Addrport(int index) { return _addrport[index]; } 61 | void setAddrPort(int index, char* ptr) 62 | { _addrport[index] = atoi(ptr); } 63 | void updateAddrPort(int index, int adrp) 64 | { _addrport[index] = adrp; } 65 | 66 | void setAddress(const char address[]) {_address = address; } 67 | const char* Address() { return _address; } 68 | 69 | void setCmd(const char cmd[]) {_cmd = cmd; } 70 | const char* Cmd() { return _cmd; } 71 | 72 | void setInteractive() { _interactive = true; } 73 | bool Interactive() { return _interactive; }; 74 | 75 | void setSilent(bool state) { _silent = state; } 76 | bool Silent() { return _silent; } 77 | 78 | void setVerbose() { _verbose = true; } 79 | bool Verbose() { return _verbose; }; 80 | 81 | void setScaleFlag(int flag) { _scaleflag = flag; } 82 | int scaleflag() { return _scaleflag; } 83 | 84 | void setEmulate() { _emulate = true; } 85 | bool Emulate() { return _emulate; }; 86 | 87 | void setDebug() { _debug = true; } 88 | bool Debug() { return _debug; }; 89 | 90 | void configureSerialPort(); 91 | 92 | void Usage(); 93 | void process(int argc, char *argv[]); 94 | 95 | private: 96 | const char* _device; 97 | const char* _measureRate; 98 | const char* _recordPeriod; 99 | const char* _baudRate; 100 | int _numaddr; 101 | char* _addrinfo[8]; 102 | int _addr[8]; 103 | int _procqueries[8]; 104 | int _addrport[8]; 105 | const char* _address; 106 | const char* _cmd; 107 | bool _interactive; 108 | bool _silent = false; 109 | bool _verbose; 110 | int _scaleflag; 111 | bool _emulate; 112 | bool _debug; 113 | 114 | }; 115 | 116 | #endif /* ARGPARSE_H */ 117 | -------------------------------------------------------------------------------- /tests/record_gtest.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include 6 | // sytem header includes must be before private public def 7 | #define private public // so can test private functions 8 | #include "../src/record.cc" 9 | 10 | class RecordTest : public ::testing::Test { 11 | public: 12 | char buffer[1000]; 13 | private: 14 | ipmRecord _record; 15 | 16 | void SetUp() 17 | { 18 | // Set binary data to some actual data from the iPM 19 | unsigned char record[] = {0, 2, 99, 0, 0, 0, 139, 68, 105, 4, 0, 0, 0, 20 | 0, 0, 0, 0, 0, 209, 0, 155, 4, 209, 0, 155, 4, 0, 0, 0, 0, 69, 2, 21 | 88, 2, 0, 0, 94, 0, 0, 0, 85, 0, 0, 0, 21, 0, 26, 113, 26, 113, 1, 22 | 1, 4, 6, 90, 6, 4, 6, 83, 6, 0, 0, 24, 0, 19, 27, 124, 8 }; 23 | memcpy(buffer, record, 68); 24 | } 25 | 26 | void TearDown() 27 | { 28 | } 29 | }; 30 | 31 | /******************************************************************** 32 | ** Test parsing response string 33 | ******************************************************************** 34 | */ 35 | TEST_F(RecordTest, Parse) 36 | { 37 | //RECORD,0,2,99,74007691,0,0,20.90,117.90,20.90,117.90,0.00,0.00,58.10, 38 | // 60.00,0.0000,0.0940,0.0000,0.0850,0.0000,0.0210,2.60,11.30,2.60, 39 | // 11.30,0.10,0.10,154.00,162.60,154.00,161.90,0.00,2.40,6931 40 | char *data = &buffer[0]; 41 | uint8_t *cp = (uint8_t *)data; 42 | uint16_t *sp = (uint16_t *)data; 43 | uint32_t *lp = (uint32_t *)data; 44 | _record.parse(cp, sp, lp); 45 | EXPECT_EQ(_record.record.EVTYPE,0); 46 | EXPECT_EQ(_record.record.OPSTATE,2); 47 | EXPECT_EQ(_record.record.POWERCNT,99); 48 | EXPECT_EQ(_record.record.TIME,74007691); 49 | EXPECT_EQ(_record.record.TFLAG,0); 50 | EXPECT_EQ(_record.record.CFLAG,0); 51 | EXPECT_EQ(_record.record.VRMSMINA,209); 52 | EXPECT_EQ(_record.record.VRMSMAXA,1179); 53 | EXPECT_EQ(_record.record.VRMSMINB,209); 54 | EXPECT_EQ(_record.record.VRMSMAXB,1179); 55 | EXPECT_EQ(_record.record.VRMSMINC,0); 56 | EXPECT_EQ(_record.record.VRMSMAXC,0); 57 | EXPECT_EQ(_record.record.FREQMIN,581); 58 | EXPECT_EQ(_record.record.FREQMAX,600); 59 | EXPECT_EQ(_record.record.VDCMINA,0); 60 | EXPECT_EQ(_record.record.VDCMAXA,94); 61 | EXPECT_EQ(_record.record.VDCMINB,0); 62 | EXPECT_EQ(_record.record.VDCMAXB,85); 63 | EXPECT_EQ(_record.record.VDCMINC,0); 64 | EXPECT_EQ(_record.record.VDCMAXC,21); 65 | EXPECT_EQ(_record.record.THDMINA,26); 66 | EXPECT_EQ(_record.record.THDMAXA,113); 67 | EXPECT_EQ(_record.record.THDMINB,26); 68 | EXPECT_EQ(_record.record.THDMAXB,113); 69 | EXPECT_EQ(_record.record.THDMINC,1); 70 | EXPECT_EQ(_record.record.THDMAXC,1); 71 | EXPECT_EQ(_record.record.VPKMINA,1540); 72 | EXPECT_EQ(_record.record.VPKMAXA,1626); 73 | EXPECT_EQ(_record.record.VPKMINB,1540); 74 | EXPECT_EQ(_record.record.VPKMAXB,1619); 75 | EXPECT_EQ(_record.record.VPKMINC,0); 76 | EXPECT_EQ(_record.record.VPKMAXC,24); 77 | EXPECT_EQ(_record.record.CRC,142351123); 78 | } 79 | 80 | /******************************************************************** 81 | ** Test creating UDP packet 82 | ******************************************************************** 83 | */ 84 | TEST_F(RecordTest, CreateUDP) 85 | { 86 | std::string str; 87 | 88 | _record.createUDP(buffer, 1); // scaling turned on 89 | str = buffer; 90 | EXPECT_EQ(str,"RECORD,0,2,99,74007691,0,0,20.90,117.90,20.90,117.90,0.00,0.00,58.10,60.00,0.0000,0.0940,0.0000,0.0850,0.0000,0.0210,2.60,11.30,2.60,11.30,0.10,0.10,154.00,162.60,154.00,161.90,0.00,2.40,142351123\r\n"); 91 | 92 | _record.createUDP(buffer, 0); // scaling turned off 93 | str = buffer; 94 | EXPECT_EQ(str,"RECORD,00,02,00000063,0469448b,00000000,00000000,00d1,049b,00d1,049b,0000,0000,0245,0258,0000,005e,0000,0055,0000,0015,1a,71,1a,71,01,01,0604,065a,0604,0653,0000,0018,087c1b13\r\n"); 95 | } 96 | 97 | /******************************************************************** 98 | ** Test retrieving time since iPM powerup 99 | ******************************************************************** 100 | */ 101 | TEST_F(RecordTest, GetTimeSincePowerup) 102 | { 103 | EXPECT_FLOAT_EQ(_record.getTimeSincePowerup(), 1233); 104 | } 105 | -------------------------------------------------------------------------------- /tests/argparse_gtest.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include 6 | // sytem header includes must be before private public def 7 | #define private public // so can test private functions 8 | 9 | namespace argtests 10 | { 11 | #include "../src/argparse.cc" 12 | #include "../src/cmd.cc" 13 | } 14 | 15 | using namespace argtests; 16 | 17 | class ArgTest : public ::testing::Test { 18 | private: 19 | 20 | ipmArgparse _args; 21 | void SetUp() 22 | { 23 | } 24 | 25 | void TearDown() 26 | { 27 | } 28 | }; 29 | 30 | 31 | /******************************************************************** 32 | ** Test parsing addrinfo block 33 | ******************************************************************** 34 | */ 35 | TEST_F(ArgTest, ParseAddrInfo) 36 | { 37 | // Parse the addInfo block 38 | char addrinfo[12]; 39 | strcpy(addrinfo, "0,1,5,30101"); 40 | _args.setAddrInfo(0, addrinfo); 41 | bool stat = _args.parse_addrInfo(0); 42 | EXPECT_EQ(stat, 0); 43 | 44 | strcpy(addrinfo, "0,5,30101"); 45 | _args.setAddrInfo(0, addrinfo); 46 | stat = _args.parse_addrInfo(0); 47 | EXPECT_EQ(stat, 1); 48 | EXPECT_EQ(_args.Addr(0), 0); 49 | EXPECT_EQ(_args.Procqueries(0), 5); 50 | EXPECT_EQ(_args.Addrport(0), 30101); 51 | } 52 | 53 | /******************************************************************** 54 | ** Test storing and retrieving command line args. ipm_ctrl accepts 55 | ** the following command line arguments: 56 | ** -D device iPM is connected to 57 | ** -s port to send status msgs to 58 | ** -m STATUS & MEASURE collection rate (hz) 59 | ** -r period of RECORD queries (minutes) 60 | ** -b baud rate 61 | ** -n number of active addresses on iPM 62 | ** -# 63 | ** number 0 to n-1 followed by info block 64 | ** -i run in interactive mode (optional) 65 | ** -a
set address (optional) 66 | ** -c set command (optional) 67 | ** -v run in verbose mode (optional) 68 | ** -H run in hexadecimal output mode, comma-delimited 69 | ** don't scale vars (optional) 70 | ** -e run with emulator; longer timeout (optional) 71 | ******************************************************************** 72 | */ 73 | 74 | TEST_F(ArgTest, SetGetIpmDevice) 75 | { 76 | // device iPM is connected to 77 | _args.setDevice("/dev/ttyS0"); 78 | EXPECT_EQ(_args.Device(), "/dev/ttyS0"); 79 | 80 | _args.setDevice("/dev/ttyUSB0"); 81 | EXPECT_EQ(_args.Device(), "/dev/ttyUSB0"); 82 | } 83 | 84 | TEST_F(ArgTest, SetGetMeasureRate) 85 | { 86 | // STATUS & MEASURE collection rate (hz) 87 | _args.setRate("1"); 88 | EXPECT_EQ(atoi(_args._measureRate), 1); 89 | } 90 | 91 | TEST_F(ArgTest, SetGetRecordPeriod) 92 | { 93 | // period of RECORD queries (minutes) 94 | _args.setPeriod("10"); 95 | EXPECT_EQ(atoi(_args._recordPeriod), 10); 96 | } 97 | 98 | TEST_F(ArgTest, SetGetBaudRate) 99 | { 100 | // baud rate 101 | _args.setBaud("57600"); 102 | EXPECT_EQ(atoi(_args._baudRate), 57600); 103 | 104 | _args.setBaud("115200"); 105 | EXPECT_EQ(atoi(_args._baudRate), 115200); 106 | 107 | // invalid baud rate 108 | _args.setBaud("9999"); 109 | EXPECT_EQ(atoi(_args._baudRate), 9999); 110 | } 111 | 112 | TEST_F(ArgTest, SetGetNumAddr) 113 | { 114 | // number of active address on iPM 115 | _args.setNumAddr("1"); 116 | EXPECT_EQ(_args.numAddr(), 1); 117 | } 118 | 119 | TEST_F(ArgTest, SetGetAddrInfo) 120 | { 121 | // address info block 122 | _args.setAddrInfo(0, (char *)"0,1,5,30101"); 123 | EXPECT_EQ(_args._addrinfo[0], "0,1,5,30101"); 124 | } 125 | 126 | TEST_F(ArgTest, SetGetInteractive) 127 | { 128 | // Interactive mode 129 | _args.setInteractive(); 130 | EXPECT_EQ(_args.Interactive(), true); 131 | } 132 | 133 | TEST_F(ArgTest, SetGetSilent) 134 | { 135 | // Silent mode - don't print output when in interactive mode 136 | _args.setSilent(true); 137 | EXPECT_EQ(_args.Silent(), true); 138 | 139 | // Print mode 140 | _args.setSilent(false); 141 | EXPECT_EQ(_args.Silent(), false); 142 | } 143 | 144 | TEST_F(ArgTest, SetGetAddress) 145 | { 146 | // address from command line in interactive mode 147 | _args.setAddress("2"); 148 | EXPECT_EQ(_args.Address(), "2"); 149 | } 150 | 151 | TEST_F(ArgTest, SetGetCmd) 152 | { 153 | // ipm cmd from command line in interactive mode 154 | _args.setCmd("MEASURE?"); 155 | EXPECT_EQ(_args.Cmd(), "MEASURE?"); 156 | } 157 | 158 | TEST_F(ArgTest, SetGetVerbose) 159 | { 160 | // Verbose mode 161 | _args.setVerbose(); 162 | EXPECT_EQ(_args.Verbose(), true); 163 | } 164 | 165 | TEST_F(ArgTest, SetGetEmulate) 166 | { 167 | // Emulate mode 168 | _args.setEmulate(); 169 | EXPECT_EQ(_args.Emulate(), true); 170 | } 171 | -------------------------------------------------------------------------------- /emulate.py: -------------------------------------------------------------------------------- 1 | #! /bin/env python3 2 | """ 3 | ########################################################################## 4 | # Program to emulate an iPM. Given iPM prompts on a serial port, write out 5 | # corresponding responses on stdout. 6 | # 7 | # Written in Python 3 8 | # 9 | # COPYRIGHT: University Corporation for Atmospheric Research, 2022 10 | ########################################################################## 11 | """ 12 | import logging 13 | import argparse 14 | import os 15 | import sys 16 | import time 17 | import shutil 18 | import tempfile 19 | import subprocess as sp 20 | 21 | import re 22 | 23 | import serial 24 | from config import sequence 25 | 26 | logger = logging.getLogger('ipmLogger') 27 | 28 | 29 | class VirtualPorts(): 30 | """ 31 | Shamelessly stolen from the GNI emulator code. -JAA 12/15/2022 32 | 33 | Setup virtual serial devices. 34 | 35 | This creates two subprocesses: the socat process which manages the pty 36 | devices for us, and the socat "serial relay" to which the emulator 37 | process will connect. 38 | 39 | The emulator opens the "instrument port", or instport, while the 40 | control program opens the "user port", or userport. 41 | """ 42 | def __init__(self): 43 | """ Initialize some instance variables """ 44 | self.socat = None 45 | self.instport = None 46 | self.userport = None 47 | self.tmpdir = None 48 | 49 | def get_user_port(self): 50 | """ Return the user port """ 51 | return self.userport 52 | 53 | def get_instrument_port(self): 54 | """ Return the instrument port """ 55 | return self.instport 56 | 57 | def start_ports(self): 58 | """ Start just the ports. The emulator is run separately.""" 59 | self.tmpdir = tempfile.mkdtemp() 60 | self.userport = os.path.join(self.tmpdir, "userport") 61 | self.instport = os.path.join(self.tmpdir, "instport") 62 | cmd = ["socat"] 63 | 64 | cmd.extend(["PTY,echo=0,link=%s" % (self.instport), 65 | "PTY,echo=0,link=%s" % (self.userport)]) 66 | 67 | # Open ports 68 | self.socat = sp.Popen(cmd, close_fds=True, shell=False) 69 | started = time.time() 70 | 71 | found = False 72 | while time.time() - started < 5 and not found: 73 | time.sleep(1) 74 | found = bool(os.path.exists(self.userport) and 75 | os.path.exists(self.instport)) 76 | 77 | # Error handling 78 | if not found: 79 | raise Exception("serial port devices still do not exist " 80 | "after 5 seconds") 81 | return self.instport 82 | 83 | def stop(self): 84 | """ Shut down socat and clean up """ 85 | logger.info("Stopping...") 86 | if self.socat: 87 | logger.debug("killing socat...") 88 | self.socat.kill() 89 | self.socat.wait() 90 | self.socat = None 91 | if self.tmpdir: 92 | logger.debug("removing %s", (self.tmpdir)) 93 | shutil.rmtree(self.tmpdir) 94 | self.tmpdir = None 95 | 96 | 97 | class IpmEmulator(): 98 | """ Class to emulate an Intelligent Power Monitor """ 99 | 100 | def __init__(self, device): 101 | 102 | # Establish serial connection to client 103 | self.sport = serial.Serial( 104 | port=device, 105 | baudrate=57600, 106 | timeout=1, 107 | parity=serial.PARITY_NONE, 108 | stopbits=serial.STOPBITS_ONE, 109 | bytesize=serial.EIGHTBITS 110 | ) 111 | self.sport.nonblocking() 112 | 113 | def listen(self): 114 | """ Loop and wait for commands to arrive """ 115 | adr = -1 116 | while True: 117 | rdata = self.sport.read(128) 118 | msg = rdata.decode('UTF-8').rstrip() 119 | 120 | if (msg != ''): 121 | print('Received command ' + msg) 122 | 123 | # On receipt of 'x', quit cycling and exit 124 | if msg == 'x': 125 | return 126 | 127 | # This chunk of code tests removing address 2 if it is 128 | # requested but not active. Uncomment to test. 129 | #m = re.search('ADR', msg) 130 | #if (m): 131 | # adr = msg[4] 132 | # print('Set address to ' + str(adr)) 133 | 134 | #if (msg == "OFF" and adr == "2"): 135 | # print('Sending null response ' + 136 | # str("".encode('iso-8859-1'))) 137 | # self.sport.write("".encode('iso-8859-1')) 138 | # continue 139 | # End tests removing address 2 140 | 141 | for cmd in sequence: 142 | fullcmd = cmd.msg # Add line feed to expected string 143 | if fullcmd == msg: 144 | logger.debug(fullcmd) 145 | print('Sending response ' + 146 | str(cmd.response.encode('iso-8859-1'))) 147 | self.sport.write(cmd.response.encode('iso-8859-1')) 148 | # If the command has a binary component, send that too 149 | if cmd.bytes != '': 150 | print('Sending binary data ' + str(cmd.bytes)) 151 | print('of length ' + str(len(cmd.bytes))) 152 | self.sport.write(cmd.bytes.encode('iso-8859-1')) 153 | sys.stdout.flush() 154 | 155 | 156 | def parse_args(): 157 | """ Instantiate a command line argument parser """ 158 | 159 | # Define command line arguments which can be provided by users 160 | parser = argparse.ArgumentParser( 161 | description="Script to operate an iPM") 162 | parser.add_argument( 163 | '-d', '--debug', dest='loglevel', action='store_const', 164 | const=logging.DEBUG, default=logging.INFO, 165 | help="Show debug log messages") 166 | 167 | # Parse the command line arguments 168 | args = parser.parse_args() 169 | 170 | return args 171 | 172 | 173 | def main(): 174 | """ 175 | Instantiate ports and iPM emulator and start listening for commands 176 | from the control program 177 | """ 178 | # Process command line arguments 179 | args = parse_args() 180 | 181 | # Set log level 182 | logging.basicConfig(level=args.loglevel) 183 | 184 | # Instantiate a set of virtual ports for iPM emulator and control code 185 | # to communicate over. When the control program is manually started, it 186 | # needs to connect to the userport. 187 | vports = VirtualPorts() 188 | 189 | instport = vports.start_ports() 190 | print("Emulator connecting to virtual serial port:", (instport)) 191 | print("User clients connect to virtual serial port:", 192 | (vports.get_user_port())) 193 | 194 | # Instantiate iPM emulator and connect to instport. 195 | ipm = IpmEmulator(instport) 196 | 197 | # Loop and listen for commands from control program 198 | ipm.listen() 199 | 200 | # Clean up 201 | vports.stop() 202 | sys.exit(1) 203 | 204 | 205 | if __name__ == "__main__": 206 | main() 207 | -------------------------------------------------------------------------------- /src/record.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include 6 | #include "record.h" 7 | 8 | ipmRecord::ipmRecord() 9 | { 10 | // unit conversions 11 | _deci = 0.1; 12 | _milli = 0.001; 13 | 14 | // generate CRC 15 | generateCRCTable(); 16 | } 17 | 18 | ipmRecord::~ipmRecord() 19 | { 20 | } 21 | 22 | void ipmRecord::parse(uint8_t *cp, uint16_t *sp, uint32_t *lp) 23 | { 24 | record.EVTYPE = cp[0]; // Event Type 25 | // Event Type: 0 - Max Interval; 1 - Power Up; 2 - Power Down; 3 - Off; 26 | // 4 - Reset; 5 - Trip; 6 - Fail; 7 - Output On; 8 - Output Off 27 | 28 | record.OPSTATE = cp[1]; // Operating State 29 | // There is a typo in the programming manual. Power up count should 30 | // be 4 bytes and elapsed time should start at byte 6 and be 4 bytes 31 | // as coded here. 32 | record.POWERCNT = (((long)sp[2]) << 16) | sp[1]; // Power Up Count 33 | record.TIME = (((long)sp[4]) << 16) | sp[3]; // Power Up Time (1 ms) 34 | record.TFLAG = (((long)sp[6]) << 16) | sp[5]; // Trip Flag 35 | record.CFLAG = (((long)sp[8]) << 16) | sp[7]; // Caution Flag 36 | record.VRMSMINA = sp[9]; // Phase A Voltage Min (0.1 V rms) 37 | record.VRMSMAXA = sp[10]; // Phase A Voltage Max (0.1 V rms) 38 | record.VRMSMINB = sp[11]; // Phase B Voltage Min (0.1 V rms) 39 | record.VRMSMAXB = sp[12]; // Phase B Voltage Max (0.1 V rms) 40 | record.VRMSMINC = sp[13]; // Phase C Voltage Min (0.1 V rms) 41 | record.VRMSMAXC = sp[14]; // Phase C Voltage Max (0.1 V rms) 42 | record.FREQMIN = sp[15]; // Frequency Min (0.1 Hz) 43 | record.FREQMAX = sp[16]; // Frequency Max (0.1 Hz) 44 | record.VDCMINA = sp[17]; // Phase A DC Content Min (1 mV) 45 | record.VDCMAXA = sp[18]; // Phase A DC Content Max (1 mV) 46 | record.VDCMINB = sp[19]; // Phase B DC Content Min (1 mV) 47 | record.VDCMAXB = sp[20]; // Phase B DC Content Max (1 mV) 48 | record.VDCMINC = sp[21]; // Phase C DC Content Min (1 mV) 49 | record.VDCMAXC = sp[22]; // Phase C DC Content Max (1 mV) 50 | record.THDMINA = cp[46]; // Phase A Distortion Min (0.1 %) 51 | record.THDMAXA = cp[47]; // Phase A Distortion Max (0.1 %) 52 | record.THDMINB = cp[48]; // Phase B Distortion Min (0.1 %) 53 | record.THDMAXB = cp[49]; // Phase B Distortion Max (0.1 %) 54 | record.THDMINC = cp[50]; // Phase C Distortion Min (0.1 %) 55 | record.THDMAXC = cp[51]; // Phase C Distortion Max (0.1 %) 56 | record.VPKMINA = sp[26]; // Phase A Peak Voltage Min (0.1 V) 57 | record.VPKMAXA = sp[27]; // Phase A Peak Voltage Max (0.1 V) 58 | record.VPKMINB = sp[28]; // Phase B Peak Voltage Min (0.1 V) 59 | record.VPKMAXB = sp[29]; // Phase B Peak Voltage Max (0.1 V) 60 | record.VPKMINC = sp[30]; // Phase C Peak Voltage Min (0.1 V) 61 | record.VPKMAXC = sp[31]; // Phase C Peak Voltage Max (0.1 V) 62 | record.CRC = lp[16]; // CRC-32 63 | } 64 | 65 | void ipmRecord::createUDP(char *buffer, int scaleflag) 66 | { 67 | if (scaleflag >= 1) { 68 | snprintf(buffer, 255, "RECORD,%u,%u,%u,%u,%u,%u,%.2f,%.2f,%.2f," 69 | "%.2f,%.2f,%.2f,%.2f,%.2f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.2f," 70 | "%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f," 71 | "%u\r\n", 72 | record.EVTYPE, record.OPSTATE, record.POWERCNT, 73 | record.TIME, record.TFLAG, record.CFLAG, 74 | record.VRMSMINA * _deci, record.VRMSMAXA * _deci, 75 | record.VRMSMINB * _deci, record.VRMSMAXB * _deci, 76 | record.VRMSMINC * _deci, record.VRMSMAXC * _deci, 77 | record.FREQMIN * _deci, record.FREQMAX * _deci, 78 | record.VDCMINA * _milli, record.VDCMAXA * _milli, 79 | record.VDCMINB * _milli, record.VDCMAXB * _milli, 80 | record.VDCMINC * _milli, record.VDCMAXC * _milli, 81 | record.THDMINA * _deci, record.THDMAXA * _deci, 82 | record.THDMINB * _deci, record.THDMAXB * _deci, 83 | record.THDMINC * _deci, record.THDMAXC * _deci, 84 | record.VPKMINA * _deci, record.VPKMAXA * _deci, 85 | record.VPKMINB * _deci, record.VPKMAXB * _deci, 86 | record.VPKMINC * _deci, record.VPKMAXC * _deci, 87 | record.CRC); 88 | } else { 89 | snprintf(buffer, 255, "RECORD,%02x,%02x,%08x,%08x,%08x,%08x,%04x," 90 | "%04x,%04x,%04x,%04x,%04x,%04x,%04x,%04x,%04x,%04x,%04x,%04x," 91 | "%04x,%02x,%02x,%02x,%02x,%02x,%02x,%04x,%04x,%04x,%04x,%04x," 92 | "%04x,%08x\r\n", 93 | record.EVTYPE, record.OPSTATE, record.POWERCNT, record.TIME, 94 | record.TFLAG, record.CFLAG, record.VRMSMINA, record.VRMSMAXA, 95 | record.VRMSMINB, record.VRMSMAXB, record.VRMSMINC, 96 | record.VRMSMAXC, record.FREQMIN, record.FREQMAX, 97 | record.VDCMINA, record.VDCMAXA, record.VDCMINB, record.VDCMAXB, 98 | record.VDCMINC, record.VDCMAXC, record.THDMINA, record.THDMAXA, 99 | record.THDMINB, record.THDMAXB, record.THDMINC, record.THDMAXC, 100 | record.VPKMINA, record.VPKMAXA, record.VPKMINB, record.VPKMAXB, 101 | record.VPKMINC, record.VPKMAXC, record.CRC); 102 | } 103 | } 104 | 105 | float ipmRecord::getTimeSincePowerup() 106 | { 107 | return record.TIME/60000; 108 | } 109 | 110 | /******************************************************************** 111 | ** CRC validation 112 | ** Compare CRC to Reversed 0xEDB88320 at 113 | ** https://www.scadacore.com/tools/programming-calculators/online-checksum-calculator/ 114 | ** My crc calculation here match the online tool, but the CRC returned 115 | ** by the iPM does not. I tried both including and excluding the 116 | ** response length in the CRC but cannot match the returned value. 117 | ** Leaving the code here so that this can be investigated more later if 118 | ** desired. 119 | ******************************************************************** 120 | */ 121 | void ipmRecord::generateCRCTable() 122 | { 123 | uint32_t crc, poly; 124 | int i, j; 125 | 126 | poly = 0xEDB88320; 127 | for (i=0; i < 256; i++) 128 | { 129 | crc = i; 130 | for (j = 8; j > 0; j--) 131 | { 132 | if (crc & 1) 133 | crc = (crc >> 1) ^ poly; 134 | else 135 | crc >>= 1; 136 | } 137 | _crcTable[i] = crc; 138 | } 139 | } 140 | 141 | uint32_t ipmRecord::calculateCRC32 (unsigned char *buf, int ByteCount) 142 | { 143 | uint32_t crc; 144 | int i, j, ch; 145 | 146 | crc = 0xFFFFFFFF; 147 | for (i=0; i < ByteCount; i++) 148 | { 149 | ch = *buf++; 150 | crc = (crc>>8) ^ _crcTable[(crc ^ ch) & 0xFF]; 151 | } 152 | return (crc ^ 0xFFFFFFFF); 153 | } 154 | 155 | /* Print a hex line of data suitable for copy/paste into the above online 156 | calculator. 157 | */ 158 | void ipmRecord::checkCRC(uint8_t *cp, uint32_t crc) 159 | { 160 | for (int j=0;j<64;j++) 161 | { 162 | char c = cp[j]; 163 | unsigned int i = (unsigned char)c; 164 | std::cout << std::setfill ('0') << std::setw(2) << std::hex << i; 165 | } 166 | std::cout << std::dec << std::endl; 167 | 168 | // Print out the CRC calculated here and the CRC from the data in hex 169 | // so can easily compare to output from online calculator. 170 | std::cout << std::hex << crc << std::dec << " : calculated CRC " 171 | << std::endl; 172 | std::cout << std::hex << record.CRC << std::dec << " : CRC from iPM" 173 | << std::endl; 174 | } 175 | -------------------------------------------------------------------------------- /tests/naiipm_gtest.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include 6 | #include 7 | // sytem header includes must be before private public def 8 | #define private public // so can test private functions 9 | 10 | #include "../naiipm.cc" 11 | #include "../src/argparse.cc" 12 | #include "../src/cmd.cc" 13 | 14 | using ::testing::Return; 15 | 16 | class MockNaiipm : public naiipm { 17 | public: 18 | // Define methods to be mocked 19 | MOCK_METHOD(bool, send_command, (int fd, std::string msg, 20 | std::string msgarg), (override)); 21 | MOCK_METHOD(bool, setActiveAddress, (int fd, int addr), 22 | (override)); 23 | }; 24 | 25 | class IpmTest : public ::testing::Test { 26 | private: 27 | naiipm ipm; 28 | 29 | void SetUp() 30 | { 31 | // Set binary data to some actual data from the iPM 32 | unsigned char record[] = {0, 2, 99, 0, 0, 0, 139, 68, 105, 4, 0, 0, 0, 33 | 0, 0, 0, 0, 0, 209, 0, 155, 4, 209, 0, 155, 4, 0, 0, 0, 0, 69, 2, 34 | 88, 2, 0, 0, 94, 0, 0, 0, 85, 0, 0, 0, 21, 0, 26, 113, 26, 113, 1, 35 | 1, 4, 6, 90, 6, 4, 6, 83, 6, 0, 0, 24, 0, 19, 27, 124, 8 }; 36 | memcpy(ipm.buffer, record, 68); 37 | ipm.setData("RECORD?", 68); 38 | 39 | unsigned char measure[] = {88, 2, 0, 0, 5, 2, 139, 4, 139, 4, 0, 0, 4, 40 | 6, 252, 5, 0, 0, 28, 0, 28, 0, 9, 0, 201, 13, 200, 6, 7, 7, 27, 27, 41 | 1, 1}; 42 | memcpy(ipm.buffer, measure, 34); 43 | ipm.setData("MEASURE?", 34); 44 | 45 | unsigned char status[] = {2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; 46 | memcpy(ipm.buffer, status, 12); 47 | ipm.setData("STATUS?", 12); 48 | } 49 | 50 | void TearDown() 51 | { 52 | } 53 | }; 54 | 55 | /******************************************************************** 56 | ** Test send_command function 57 | ******************************************************************** 58 | */ 59 | TEST_F(IpmTest, ipmSendBadCommand) 60 | { 61 | testing::internal::CaptureStdout(); 62 | 63 | int fd = 1; // Set to stdout 64 | EXPECT_EQ(ipm.send_command(fd, "BADCOMMAND"), false); 65 | 66 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 67 | "Command BADCOMMAND is invalid. Please enter a valid command\n"); 68 | } 69 | /******************************************************************** 70 | ** Test clear function (using mock) 71 | ******************************************************************** 72 | */ 73 | TEST_F(IpmTest, ipmClear) 74 | { 75 | int fd = 1; // Set to stdout 76 | int addr = 2; 77 | std::string msg = "VER?"; 78 | // Instantiate the mock naiipm class 79 | MockNaiipm mipm; 80 | // Define what the mocked functions will return 81 | EXPECT_CALL(mipm, setActiveAddress) 82 | .Times(1) 83 | .WillRepeatedly(Return(true)); 84 | EXPECT_CALL(mipm, send_command) 85 | .Times(1) 86 | .WillRepeatedly(Return(true)); 87 | 88 | // Do the test, using the mocked instance, mipm 89 | testing::internal::CaptureStdout(); 90 | 91 | bool status = mipm.clear(fd, addr); 92 | EXPECT_EQ(status, true); 93 | 94 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 95 | "Took 0 ADR commands to clear iPM on init\n"); 96 | } 97 | /******************************************************************** 98 | ** Test setting interactive mode 99 | ******************************************************************** 100 | */ 101 | TEST_F(IpmTest, ipmSetInteractiveModeAnoC) 102 | { 103 | 104 | testing::internal::CaptureStdout(); 105 | int fd = 1; // Set to stdout 106 | 107 | // got -a but not -c 108 | const char *addr = "2"; 109 | args.setAddress(addr); 110 | args.setCmd(""); 111 | ipm.setInteractiveMode(fd); 112 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 113 | "Command is invalid. Please enter a valid command\n"); 114 | } 115 | 116 | TEST_F(IpmTest, ipmSetInteractiveModeCnoA) 117 | { 118 | testing::internal::CaptureStdout(); 119 | int fd = 1; // Set to stdout 120 | 121 | // got -c but not -a (so default to zero) 122 | args.setCmd("RECORD?"); 123 | args.setAddress("-1"); 124 | ipm.setInteractiveMode(fd); 125 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 126 | "Setting default address of 0\n"); 127 | } 128 | 129 | /******************************************************************** 130 | ** Test removing inactive address 131 | ******************************************************************** 132 | */ 133 | TEST_F(IpmTest, ipmRmAddr) 134 | { 135 | testing::internal::CaptureStdout(); 136 | char addrinfo[12]; 137 | 138 | args.setNumAddr("4"); 139 | strcpy(addrinfo, "0,1,30101"); 140 | args.setAddrInfo(0, addrinfo); 141 | args.parse_addrInfo(0); 142 | strcpy(addrinfo, "2,5,30102"); 143 | args.setAddrInfo(1, addrinfo); 144 | args.parse_addrInfo(1); 145 | strcpy(addrinfo, "5,7,30103"); 146 | args.setAddrInfo(2, addrinfo); 147 | args.parse_addrInfo(2); 148 | strcpy(addrinfo, "6,7,30104"); 149 | args.setAddrInfo(3, addrinfo); 150 | args.parse_addrInfo(3); 151 | 152 | EXPECT_EQ(args.numAddr(), 4); 153 | EXPECT_EQ(args.Addr(0), 0); 154 | EXPECT_EQ(args.Procqueries(0), 1); 155 | EXPECT_EQ(args.Addrport(0), 30101); 156 | EXPECT_EQ(args.Addr(1), 2); 157 | EXPECT_EQ(args.Procqueries(1), 5); 158 | EXPECT_EQ(args.Addrport(1), 30102); 159 | EXPECT_EQ(args.Addr(2), 5); 160 | EXPECT_EQ(args.Procqueries(2), 7); 161 | EXPECT_EQ(args.Addrport(2), 30103); 162 | EXPECT_EQ(args.Addr(3), 6); 163 | EXPECT_EQ(args.Procqueries(3), 7); 164 | EXPECT_EQ(args.Addrport(3), 30104); 165 | 166 | ipm.rmAddr(1); // remove address at index 1 167 | EXPECT_EQ(args.numAddr(), 3); 168 | EXPECT_EQ(args.Addr(0), 0); 169 | EXPECT_EQ(args.Procqueries(0), 1); 170 | EXPECT_EQ(args.Addrport(0), 30101); 171 | EXPECT_EQ(args.Addr(1), 5); 172 | EXPECT_EQ(args.Procqueries(1), 7); 173 | EXPECT_EQ(args.Addrport(1), 30103); 174 | EXPECT_EQ(args.Addr(2), 6); 175 | EXPECT_EQ(args.Procqueries(2), 7); 176 | EXPECT_EQ(args.Addrport(2), 30104); 177 | 178 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 179 | "Removing address 2 from active address list\n"); 180 | 181 | } 182 | 183 | /******************************************************************** 184 | ** Test parsing response strings 185 | ******************************************************************** 186 | */ 187 | TEST_F(IpmTest, ipmParseDataNoScale) 188 | { 189 | 190 | ipm.open_udp("192.168.84.2"); 191 | 192 | std::string str; 193 | char addrinfo[12]; 194 | 195 | // do not scale 196 | strcpy(addrinfo, "0,5,30101"); 197 | args.setScaleFlag(0); // do not scale 198 | args.setAddrInfo(0, addrinfo); 199 | args.parse_addrInfo(0); 200 | 201 | testing::internal::CaptureStdout(); 202 | ipm.parseData("MEASURE?", 0); 203 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 204 | "sending to port 30101 UDP string MEASURE,0258,0205,048b,048b,0000,0604,05fc,0000,001c,001c,0009,0dc9,06c8,0707,1b,1b,01,01\r\n"); 205 | 206 | testing::internal::CaptureStdout(); 207 | ipm.parseData("STATUS?", 0); 208 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 209 | "sending to port 30101 UDP string STATUS,02,01,0000,0000,0000\r\n"); 210 | 211 | testing::internal::CaptureStdout(); 212 | ipm.parseData("RECORD?", 0); 213 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 214 | "sending to port 30101 UDP string RECORD,00,02,00000063,0469448b,00000000,00000000,00d1,049b,00d1,049b,0000,0000,0245,0258,0000,005e,0000,0055,0000,0015,1a,71,1a,71,01,01,0604,065a,0604,0653,0000,0018,087c1b13\r\n"); 215 | } 216 | 217 | TEST_F(IpmTest, ipmParseDataScale) 218 | { 219 | ipm.open_udp("192.168.84.2"); 220 | 221 | std::string str; 222 | char addrinfo[12]; 223 | 224 | // scale 225 | strcpy(addrinfo, "0,5,30101"); 226 | args.setScaleFlag(1); // scale 227 | args.setAddrInfo(0, addrinfo); 228 | args.parse_addrInfo(0); 229 | 230 | testing::internal::CaptureStdout(); 231 | ipm.parseData("MEASURE?", 0); 232 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 233 | "sending to port 30101 UDP string MEASURE,60.00,51.70,116.30,116.30,0.00,154.00,153.20,0.00,0.0280,0.0280,0.0090,352.90,173.60,179.90,2.70,2.70,0.10,1\r\n"); 234 | 235 | testing::internal::CaptureStdout(); 236 | ipm.parseData("STATUS?", 0); 237 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 238 | "sending to port 30101 UDP string STATUS,2,1,0,0,0,0\r\n"); 239 | 240 | testing::internal::CaptureStdout(); 241 | ipm.parseData("RECORD?", 0); 242 | EXPECT_EQ(testing::internal::GetCapturedStdout(), 243 | "sending to port 30101 UDP string RECORD,0,2,99,74007691,0,0,20.90,117.90,20.90,117.90,0.00,0.00,58.10,60.00,0.0000,0.0940,0.0000,0.0850,0.0000,0.0210,2.60,11.30,2.60,11.30,0.10,0.10,154.00,162.60,154.00,161.90,0.00,2.40,142351123\r\n"); 244 | 245 | ipm.close_udp(atoi(args.Address())); 246 | } 247 | 248 | /******************************************************************** 249 | ** Test implementation of measureRate (hz) and recordPeriod (minutes) 250 | ******************************************************************** 251 | */ 252 | TEST_F(IpmTest, ipmSleep) 253 | { 254 | args.setRate("1"); // hz 255 | ipm.sleep(); 256 | EXPECT_EQ(ipm._sleeptime, 800000); 257 | 258 | args.setRate("5"); // hz 259 | ipm.sleep(); 260 | EXPECT_EQ(ipm._sleeptime, 0); 261 | 262 | args.setRate("2"); // hz 263 | ipm.sleep(); 264 | EXPECT_EQ(ipm._sleeptime, 300000); 265 | } 266 | 267 | TEST_F(IpmTest, ipmSetRecordFreq) 268 | { 269 | args.setRate("1"); // hz 270 | args.setPeriod("10"); // minutes 271 | ipm.setRecordFreq(); 272 | EXPECT_EQ(ipm._recordFreq, 600); 273 | 274 | args.setRate("2"); // hz 275 | args.setPeriod("1"); // minutes 276 | ipm.setRecordFreq(); 277 | EXPECT_EQ(ipm._recordFreq, 120); 278 | 279 | args.setRate("5"); // hz 280 | args.setPeriod("1"); // minutes 281 | ipm.setRecordFreq(); 282 | EXPECT_EQ(ipm._recordFreq, 300); 283 | } 284 | -------------------------------------------------------------------------------- /src/argparse.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | #include "argparse.h" 6 | 7 | ipmCmd commands; 8 | 9 | ipmArgparse::ipmArgparse():_interactive(false) 10 | { 11 | // Set defaults 12 | const char *device = "/dev/ttyUSB0"; 13 | setDevice(device); 14 | setScaleFlag(1); // turn on scaling by default 15 | const char *baud = "57600"; 16 | setBaud(baud); 17 | const char *naddr = "1"; // one address by default 18 | setNumAddr(naddr); 19 | const char *undefAddr = "-1"; 20 | setAddress(undefAddr); 21 | setCmd(""); 22 | } 23 | 24 | ipmArgparse::~ipmArgparse() 25 | { 26 | } 27 | 28 | void ipmArgparse::Usage() 29 | { 30 | std::cout << 31 | "\nUsage:\n" 32 | "\t-D device\tiPM connection device (Default:/dev/ttyUSB0)\n" 33 | "\t-m measurerate\tSTATUS & MEASURE collection rate (hz)\n" 34 | "\t-r recordperiod\tperiod of RECORD queries (minutes)\n" 35 | "\t-b baudrate\tbaud rate (Default:57600)\n" 36 | "\t-n num_addr\tnumber of active addresses on iPM\n" 37 | "\t-# addr,procqueries,port\n" 38 | "\t\t\t - addr is the iPM address; a number 0 to n-1\n" 39 | "\t\t\t - procqueries is an integer representing a 3-bit\n" 40 | "\t\t\t boolean field indicating which query responses\n" 41 | "\t\t\t [RECORD,MEASURE,STATUS] should be processed, eg\n" 42 | "\t\t\t 3 (b’011) requests MEASURE+STATUS\n" 43 | "\t\t\t 5 (b’101) requests RECORD+STATUS\n" 44 | "\t\t\t - port which to send the output UDP string\n" 45 | "\t-i \t\trun in interactive mode (optional)\n" 46 | "\t\t\t - When in interactive mode only -D and -b are required\n" 47 | "\t\t\t - Inclusion of -a and -c will send a single command\n" 48 | "\t\t\t and exit.\n" 49 | "\t-a \t\tset address (optional; defaults to 0 in interactive mode)n" 50 | "\t-c \t\tset command (optional)\n" 51 | "\t-v \t\trun in verbose mode (optional)\n" 52 | "\t-H \t\trun in hexadecimal output mode, comma-delimited\n" 53 | "\t\t\t don't scale vars (optional)\n" 54 | "\t-e \t\trun with emulator; longer timeout (optional)\n" 55 | "\t-d \t\tRun in debug mode - prints to screen rather than logfile\n" 56 | "\t\t\t when in looping (non-interactive) mode (optional)\n" 57 | "\t-S \t\tConfigure serial port and exit. Must be run as\n" 58 | "\t\t\t root\n" 59 | "\n" 60 | "Examples:\n" 61 | "\t./ipm_ctrl -i -a 2 -D /dev/ttyUSB0 -c RECORD?\n" 62 | "\t Interactive, Send a single RECORD? query to iPM " 63 | "address 2 on\n\t /dev/ttyUSB0\n" 64 | "\t./ipm_ctrl -i -a 1 -D /dev/ttyUSB0 -c MEASURE? -H\n" 65 | "\t Interactive, Send a single MEASURE? query to iPM " 66 | "address 1 on \n\t /dev/ttyUSB0 and output hex values\n" 67 | "\t./ipm_ctrl -i\n" 68 | "\t Interactive, Start menu-based control of iPM\n" 69 | "\t./ipm_ctrl -m 1 -r 10 -n 2 -0 0,5,30101 -1 2,5,30102" 70 | " -D /dev/ttyUSB0\n\t Launch full application with bus " 71 | "identification at two\n\t addresses, initialization, " 72 | "periodic data queries and\n\t transmission to network " 73 | "IP port.\n\n"; 74 | } 75 | 76 | void ipmArgparse::process(int argc, char *argv[]) 77 | { 78 | int opt; 79 | int errflag = 0, nopt = 0; 80 | bool D = false; 81 | bool m = false; 82 | bool r = false; 83 | bool b = false; 84 | bool n = false; 85 | bool i = false; 86 | int a = -1; 87 | std::string c = ""; 88 | int nInfo = 0; 89 | 90 | // Options between colons require an argument 91 | // Options after last colon do not. 92 | while((opt = getopt(argc, argv, ":D:m:r:b:n:0:1:2:3:4:5:6:7:a:c:ivHedS")) 93 | != -1) 94 | { 95 | nopt++; 96 | switch(opt) 97 | { 98 | case 'D': // Device the iPM is connected to 99 | D = true; 100 | setDevice(optarg); 101 | break; 102 | case 'm': // STATUS & MEASURE collection rate (hz) 103 | m = true; 104 | setRate(optarg); 105 | break; 106 | case 'r': // Period of RECORD queries (minutes) 107 | r = true; 108 | setPeriod(optarg); 109 | break; 110 | case 'b': // Baud rate 111 | b = true; 112 | setBaud(optarg); 113 | break; 114 | case 'n': // Number of addresses in use on iPM 115 | n = true; 116 | setNumAddr(optarg); 117 | break; 118 | case '0': 119 | case '1': 120 | case '2': 121 | case '3': 122 | case '4': 123 | case '5': 124 | case '6': 125 | case '7': 126 | setAddrInfo(opt-'0', optarg); 127 | if (not parse_addrInfo(opt-'0')) 128 | { 129 | std::cout << optarg << " is not a valid address info block" 130 | << std::endl; 131 | exit(1); 132 | } 133 | 134 | nInfo++; 135 | break; 136 | case 'a': 137 | if (atoi(optarg) < 0 or atoi(optarg) > 7) // verify 138 | { 139 | std::cout << "Address " << optarg << " is invalid. " 140 | "Please enter a valid address" << std::endl; 141 | exit(1); 142 | } else { 143 | setAddress(optarg); 144 | } 145 | break; 146 | case 'c': 147 | if (commands.verify(optarg)) 148 | { 149 | setCmd(optarg); 150 | } else { 151 | exit(1); 152 | } 153 | break; 154 | case 'i': // Run in interactive (menu) mode 155 | setInteractive(); 156 | i = true; 157 | break; 158 | case 'v': // Run in verbose mode 159 | setVerbose(); 160 | break; 161 | case 'H': // Run in hexadecimal output mode, comma delimited 162 | setScaleFlag(0); // Turn off scaling 163 | case 'e': // Run in emulator mode 164 | setEmulate(); 165 | break; 166 | case 'd': // Run in debug mode 167 | setDebug(); 168 | break; 169 | case 'S': // Configure serial port 170 | configureSerialPort(); 171 | exit(0); 172 | case ':': 173 | std::cerr << "option -" << char(optopt) << 174 | " needs a value" << std::endl; 175 | errflag++; 176 | break; 177 | case '?': 178 | std::cerr << "unknown option: " << char(optopt) << std::endl; 179 | errflag++; 180 | break; 181 | } 182 | } 183 | // Confirm that the number of addrinfo command line entries equals the 184 | // the numaddr number. 185 | if (nInfo != 0 and numAddr() != nInfo) 186 | { 187 | std::cout << "-n option must match number of addresses given on " << 188 | "command line" << std::endl; 189 | errflag++; 190 | } 191 | 192 | // If not in interactive mode, default to hex data for UDP packets 193 | if (not Interactive()) 194 | { 195 | setScaleFlag(0); // Turn off scaling 196 | } 197 | 198 | // On error, print the usage statement and exit 199 | if (errflag or (geteuid() != 0 and 200 | not i and (not nopt or not m or not r or not n))) 201 | { 202 | Usage(); 203 | exit(1); 204 | } 205 | 206 | // If running as root and included -S option, will exit before get here 207 | if ((geteuid() == 0)) // Running as root 208 | { 209 | std::cout << "\n**** Running as root. If you are trying to configure " 210 | "****\n**** serial ports, please use the -S option ****\n" 211 | << std::endl; 212 | Usage(); 213 | exit(1); 214 | } 215 | return; 216 | } 217 | 218 | // If on a linux machine and outb function exists, configure serial port 219 | // This command only works if this program is run with sudo. Since we do 220 | // not want to run with sudo in general, exit after this command is run. 221 | void ipmArgparse::configureSerialPort() 222 | { 223 | #ifdef __linux__ 224 | if ((geteuid() == 0)) { // Running as root 225 | if (not ioperm(0x1E9, 3, 1)) { // serial port not configured 226 | std::cout << "Configuring serial port... " << std::endl; 227 | outb(0x00, 0x1E9); // Here order is DATA, ADDRESS, but at command 228 | outb(0x3E, 0x1EA); // line outb takes address data eg. 229 | // sudo outb 0x1E9 0x00 230 | std::cout << " done." << std::endl; 231 | } else { 232 | std::cout << "Serial port already configured. Nothing to do" 233 | << std::endl; 234 | } 235 | } else { 236 | std::cout << "Must be root to configure serial port" << std::endl; 237 | } 238 | #else 239 | std::cout << "serial port configuration only works on linux" << std::endl; 240 | #endif 241 | } 242 | 243 | // Parse the addrInfo block from the command line 244 | // Block contains addr,procqueries,port 245 | bool ipmArgparse::parse_addrInfo(int i) 246 | { 247 | char *addrinfo = addrInfo(i); 248 | // Validate address info block with simple comma count 249 | std::string s = (std::string)addrinfo; 250 | if (std::count(s.begin(), s.end(), ',') != 2) 251 | { 252 | return false; 253 | } 254 | if (Verbose()) 255 | { 256 | std::cout << "Parsing info block " << addrinfo << std::endl; 257 | } 258 | char *ptr = strtok(addrinfo, ","); 259 | setAddr(i, ptr); 260 | if (Verbose()) 261 | { 262 | std::cout << "addr: " << Addr(i) << std::endl; 263 | } 264 | 265 | ptr = strtok(NULL, ","); 266 | setProcqueries(i, ptr); 267 | if (Verbose()) 268 | { 269 | std::cout << "procqueries: " << Procqueries(i) << std::endl; 270 | } 271 | 272 | ptr = strtok(NULL, ","); 273 | setAddrPort(i, ptr); 274 | if (Verbose()) 275 | { 276 | std::cout << "addrport: " << Addrport(i) << std::endl; 277 | } 278 | 279 | return true; 280 | } 281 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 University Corporation for Atmospheric Research 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | Any external libraries used in this software are subject to their own 204 | licenses, when applicable. 205 | -------------------------------------------------------------------------------- /ipm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 354 | 355 | 356 | 357 | 358 | 359 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | -------------------------------------------------------------------------------- /naiipm.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | ** 2024, Copyright University Corporation for Atmospheric Research 3 | ******************************************************************** 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #ifdef __linux__ 13 | #include 14 | #endif 15 | 16 | #include "naiipm.h" 17 | #include "src/cmd.h" 18 | #include "src/measure.h" 19 | #include "src/status.h" 20 | #include "src/record.h" 21 | #include "src/bitresult.h" 22 | 23 | ipmArgparse args; 24 | 25 | naiipm::naiipm() 26 | { 27 | 28 | // unit conversions 29 | _deci = 0.1; 30 | _milli = 0.001; 31 | 32 | // Initialize the binary data map 33 | _ipm_data["BITRESULT?"] = _bitdata; // Query self test result 34 | _ipm_data["MEASURE?"] = _measuredata; // Device Measurement 35 | _ipm_data["STATUS?"] = _statusdata; // Device Status 36 | _ipm_data["RECORD?"] = _recorddata; // Device Statistics 37 | 38 | _recordCount = 0; 39 | _badData = 0; 40 | 41 | } 42 | 43 | naiipm::~naiipm() 44 | { 45 | } 46 | 47 | // When installed on the GV (as opposed to in the lab), on power up 48 | // the iPM frequently comes up in a hung state (returns nothing in 49 | // response to sent commands). The working theory is that there is some 50 | // junk on the port that is corrupting commands being sent. Attempt to 51 | // clear this by repeatedly sending the ADR and VER? commands up to 10 52 | // times. 53 | bool naiipm::clear(int fd, int addr) 54 | { 55 | bool status = true; 56 | std::string msg; 57 | 58 | if (args.Interactive()) 59 | { 60 | // Set silent so don't print VER output when clearing. 61 | args.setSilent(true); 62 | } 63 | 64 | for (int j=0; j < 10; j++) 65 | { 66 | // ADR should return nothing so can send it to gather junk on line 67 | status = setActiveAddress(fd, addr); 68 | 69 | // Query Firmware Version 70 | msg = "VER?"; 71 | if((status = send_command(fd, msg))) { // success so stop iterating 72 | std::cout << "Took " << j << " ADR commands to clear iPM on" << 73 | " init" << std::endl; 74 | break; 75 | } 76 | 77 | // Wait half a second and try again 78 | usleep(500000); // 0.5 seconds 79 | } 80 | 81 | if (args.Interactive()) 82 | { 83 | // Turn output back on 84 | args.setSilent(false); 85 | } 86 | 87 | return status; 88 | } 89 | 90 | //Initialize the iPM device. Returns a verified list of device addresses 91 | // that may be shorter than the list passed in if some addresses did not 92 | // pass verification. 93 | bool naiipm::init(int fd) 94 | { 95 | 96 | flush(fd); 97 | 98 | // Verify device existence at all addresses 99 | std::cout << "This ipm should have " << args.numAddr() << " active address(es)" 100 | << std::endl; 101 | for (int i=0; i < args.numAddr(); i++) 102 | { 103 | std::string msg; 104 | std::cout << "Info for address " << i << " is " << args.Addr(i) << "," 105 | << args.Procqueries(i) << "," << args.Addrport(i) 106 | << std::endl; 107 | 108 | bool status; 109 | status = setActiveAddress(fd, args.Addr(i)); 110 | if(not status) 111 | { 112 | std::cout << "Unable to set active address to " << args.Addr(i) << 113 | ". Skipping address " << args.Addr(i) << "for this iteration" << 114 | std::endl; 115 | continue; 116 | } 117 | 118 | status = clear(fd, args.Addr(i)); 119 | if (not status) { 120 | std::cout << "Unable to clear device" << std::endl; 121 | } 122 | 123 | // Turn Device OFF, wait > 100ms then turn ON to reset state 124 | msg = "OFF"; 125 | if(not send_command(fd, msg)) { 126 | // OFF query failed, so remove address from active address list 127 | rmAddr(i); 128 | i--; // back up to where next addrinfo is now stored 129 | continue; 130 | } 131 | 132 | unsigned int microseconds = 110000; 133 | usleep(microseconds); // Wait > 100ms 134 | 135 | msg = "RESET"; 136 | if(not send_command(fd, msg)) { return false; } 137 | 138 | // Query Serial Number 139 | msg = "SERNO?"; 140 | if(not send_command(fd, msg)) { return false; } 141 | // Query Firmware Version 142 | msg = "VER?"; 143 | if(not send_command(fd, msg)) { return false; } 144 | 145 | // Execute build-in self test 146 | msg = "TEST"; 147 | if(not send_command(fd, msg)) { return false; } 148 | msg = "BITRESULT?"; 149 | if(not send_command(fd, msg)) { return false; } 150 | parseData(msg, i); 151 | } 152 | 153 | if (args.numAddr() == 0) 154 | { 155 | // if setting all active addresses fails on init, wait 5s and close 156 | // program so nidas can restart it. 157 | std::cout << "There are no active addresses available to select" 158 | << std::endl; 159 | usleep(5000000); // 5 seconds 160 | return false; 161 | } 162 | 163 | return true; 164 | } 165 | 166 | // Remove address from active address list 167 | // i is index of addrinfo string, not actual address to be removed 168 | void naiipm::rmAddr(int i) 169 | { 170 | std::cout << "Removing address " << args.Addr(i) << 171 | " from active address list" << std::endl; 172 | for (int j=i; j x('\0' + procq); 361 | if (args.Verbose()) 362 | { 363 | std::cout << ": [" << procq << "] " << '\0' + procq << " : " << x 364 | << std::endl; 365 | } 366 | 367 | if (setActiveAddress(fd, args.Addr(i))) 368 | { 369 | // Per software requirements, MEASURE? Is queried first, followed 370 | // by STATUS?, followed by RECORD? 371 | std::bitset<4> m = x; 372 | if ((m &= 0b0010) == 2) // MEASURE command requested 373 | { 374 | msg = "MEASURE?"; 375 | if(not send_command(fd, msg)) { return false; } 376 | parseData(msg, i); 377 | } 378 | std::bitset<4> s = x; 379 | if ((s &= 0b0001) == 1) // STATUS command requested 380 | { 381 | msg = "STATUS?"; 382 | if(not send_command(fd, msg)) { return false; } 383 | parseData(msg, i); 384 | } 385 | std::bitset<4> r = x; 386 | if ((r &= 0b0100) == 4) // RECORD command requested 387 | { 388 | if (_recordCount >= _recordFreq) 389 | { 390 | msg = "RECORD?"; 391 | if(not send_command(fd, msg)) { return false; } 392 | parseData(msg, i); 393 | _recordCount = 0; 394 | } 395 | } 396 | } 397 | } 398 | 399 | return true; 400 | 401 | } 402 | 403 | // rate for STATUS and MEASURE is quicker than RECORD, so use that as the base. 404 | // Rather than setting a timer to get responses at the exact interval requested, 405 | // since this is housekeeping data and timing is not critical, set sleep so we 406 | // get at least one response per requested time period. From test runs on 407 | // Gigajoules, request for all three commands returns in ~0.2 seconds, so 408 | // subtract that from requested rate. 409 | void naiipm::sleep() 410 | { 411 | // TBD: Will likely need to adjust this when the iPM is mounted on the 412 | // aircraft. 413 | _sleeptime = ((1000000 / atoi(args.measureRate())) - 200000); // usec 414 | usleep(_sleeptime); 415 | } 416 | 417 | void naiipm::setData(std::string cmd, int len) 418 | { 419 | // free the previous binary data memory space 420 | // and update the map to point to the new space 421 | memcpy(_ipm_data[cmd], buffer, len); 422 | } 423 | 424 | // read response from iPM 425 | void naiipm::get_response(int fd, int len, bool bin) 426 | { 427 | int n = 0, r = 0; 428 | char c; 429 | int ret; 430 | fd_set set; 431 | buffer[0] = '\0'; 432 | if (args.Verbose()) 433 | { 434 | std::cout << "Expected response length " << len << std::endl; 435 | } 436 | 437 | while (true) 438 | { 439 | // If iPM never returns expected number of bytes, timeout 440 | struct timeval timeout; 441 | FD_ZERO(&set); 442 | FD_SET(fd, &set); 443 | 444 | // During operation, the iPM timeout should be 100ms. When developing 445 | // using the Python emulator, this is too short, so add a second. 446 | int tout; 447 | if (args.Emulate()) 448 | { 449 | tout = 1; // Add a second to timeout when developing 450 | } else 451 | { 452 | tout = 0; // Deployment mode - leave timeout at 100ms 453 | } 454 | timeout.tv_sec = tout; 455 | timeout.tv_usec = 100000; // 100ms timeout 456 | 457 | int rv = select(fd + 1, &set, NULL, NULL, &timeout); 458 | if (rv == -1) 459 | { 460 | perror("select()"); 461 | break; /* an error occurred */ 462 | } 463 | else if (rv == 0) 464 | { 465 | if (len != 0) // expected a response but didn't get one 466 | { 467 | // timeout 468 | trackBadData(); 469 | std::cout << "timeout" << std::endl; /* a timeout occured */ 470 | } 471 | break; 472 | } 473 | else 474 | { 475 | ret = read(fd, &c, 1); 476 | } 477 | 478 | if (ret > 0) // successful read 479 | { 480 | std::bitset<8> x(c); 481 | unsigned int i = (unsigned char)c; 482 | if (args.Verbose()) 483 | { 484 | // If c is not a printable character, for printing purposes 485 | // replace it with a null string terminator" 486 | char ch = c; 487 | if (not std::isprint(static_cast(c))) { 488 | ch = '\0'; 489 | } else { 490 | ch = c; 491 | } 492 | std::cout << n+1 << ": [" << ch << "] " << std::dec << i << ","; 493 | std::cout << std::hex << i << " : " << x << std::dec << std::endl; 494 | } 495 | buffer[n] = c; 496 | // linefeed is a valid value mid-binary query so only test 497 | // if NOT reading binary data 498 | if (c == '\n' && not bin) { // found linefeed 499 | break; 500 | } 501 | n++; 502 | } else if (ret != -1) // read did not return timeout 503 | { 504 | std::cout << "unknown response " << c << std::endl; 505 | return; 506 | } else if (ret == -1) // Resource temporarily unavailable 507 | { 508 | std::cout << "Read from iPM returned error " << strerror(errno) 509 | << std::endl; 510 | } 511 | 512 | 513 | // if receive len chars without an endline, return anyway 514 | // (handles binary data) 515 | if (n > (len - 1)) 516 | { 517 | n--; // decrement char count since never found linefeed 518 | break; 519 | } 520 | } 521 | 522 | buffer[n+1] = '\0'; // terminate the string 523 | 524 | if (not bin && (n+1 != len)) 525 | { 526 | if (len != 0) // For ADR command, expect len zero response, and when 527 | { // get no response n+1 = 1, so will fail above check but 528 | // ignore here. I am sure there is a better logic construct 529 | // to catch this, but I am not coming up with it right now. 530 | if (n == 0) // Did not receive a response at all when expected 531 | { 532 | std::cout << "Didn't receive a response from the iPM." 533 | << std::endl; 534 | std::cout << "Are you sure the selected address is active?" 535 | << std::endl; 536 | } else { 537 | // n and len should be the same for ascii data 538 | std::cout << "Didn't receive all expected chars: received " << 539 | n+1 << ", expected " << len << " : " << buffer << std::endl; 540 | // data size error; increment bad data counter 541 | trackBadData(); 542 | } 543 | } 544 | } 545 | } 546 | 547 | // If bad data is received (e.g. header error, size error, CRC error, query 548 | // timeout) then the counter is incremented. If counter reaches 10 errors, 549 | // application shall wait 5 seconds and then reinit. Log that we shut down for 550 | // data error reasons. 551 | void naiipm::trackBadData() 552 | { 553 | _badData++; 554 | if (_badData == 10) 555 | { 556 | std::cout << "Found 10 data errors - shutting down and restarting" 557 | << std::endl; 558 | // Upon exit, nidas will wait for timeout given in XML (should be 5s) 559 | // and then will attempt to restart program. 560 | exit(1); 561 | } 562 | } 563 | // send a single command entered on the command line 564 | void naiipm::singleCommand(int fd) 565 | { 566 | int addr = atoi(args.Address()); 567 | setActiveAddress(fd, addr); 568 | std::string cmd = args.Cmd(); 569 | if (args.Verbose()) 570 | { 571 | std::cout << "Sending command " << cmd << std::endl; 572 | } 573 | send_command(fd, cmd, ""); 574 | parse_binary(cmd); 575 | } 576 | 577 | // send command to iPM and verify response 578 | bool naiipm::send_command(int fd, std::string msg, std::string msgarg) 579 | { 580 | // Confirm command is in list of acceptable command 581 | if (not commands.verify(msg)) {return false;} 582 | 583 | if (args.Verbose()) 584 | { 585 | std::cout << "Got message " << msg << std::endl; 586 | } 587 | 588 | // Find expected response for this message 589 | auto response = commands.response(msg); 590 | std::string expected_response = response->second; 591 | 592 | if (args.Verbose()) 593 | { 594 | std::cout << "Expect response " << expected_response << std::endl; 595 | } 596 | 597 | // Send message to ipm 598 | if (msgarg != "") 599 | { 600 | msg.append(' ' + msgarg); 601 | } 602 | std::string sendmsg = msg + "\n"; // Add linefeed to end of command 603 | if (args.Verbose()) 604 | { 605 | std::cout << "Sending message " << sendmsg << std::endl; 606 | std::cout << "of length " << sendmsg.length() << std::endl; 607 | } 608 | write(fd, sendmsg.c_str(), sendmsg.length()); 609 | if (tcdrain(fd) == -1) // wait for write to complete 610 | { 611 | std::cout << errno << std::endl; 612 | } 613 | if (args.Verbose()) 614 | { 615 | std::cout << "Write completed" << std::endl; 616 | } 617 | 618 | if (msg == "SERNO?") // Serial # changes frequently, so just check regex 619 | { 620 | // Since SERNO uses a regex, can't get expected response length by 621 | // checking length of expected response. So just hardcode as length of 622 | // 7. 623 | // Get response from ipm 624 | get_response(fd, 7, false); 625 | if (args.Verbose()) 626 | { 627 | std::cout << "Received " << buffer << std::endl; 628 | } 629 | std::string str = (std::string)buffer; 630 | std::regex r(expected_response); 631 | std::smatch m; 632 | if (not std::regex_match(str, m, r)) 633 | { 634 | std::cout << "Device command " << msg << " did not return " 635 | << "expected response " << expected_response << std::endl; 636 | return false; // command failed 637 | } else { 638 | if (args.Interactive()) 639 | { 640 | std::cout << buffer << std::endl; 641 | } 642 | } 643 | } 644 | else 645 | { 646 | // Get response from ipm 647 | get_response(fd, int(expected_response.length()), false); 648 | if (args.Verbose()) 649 | { 650 | std::cout << "Received " << buffer << std::endl; 651 | } 652 | 653 | if(buffer != expected_response) 654 | { 655 | // header error so increment bad data counter 656 | trackBadData(); 657 | std::cout << "Device command " << msg << " did not return " 658 | << "expected response " << expected_response << std::endl; 659 | return false; // command failed 660 | } else { 661 | if (msg == "VER?" && args.Interactive() && not args.Silent()) 662 | { 663 | std::cout << buffer << std::endl; 664 | } 665 | } 666 | } 667 | 668 | // Read binary part of response. Length of binary response was 669 | // returned as first response to query. 670 | 671 | if (_ipm_data.find(msg) != _ipm_data.end()) // cmd returns data 672 | { 673 | int binlen = std::stoi(buffer); 674 | if (args.Verbose()) 675 | { 676 | std::cout << "Now get " << binlen << " bytes" << std::endl; 677 | } 678 | get_response(fd, binlen, true); // true indicates reading binary data 679 | setData(msg, binlen); 680 | } 681 | 682 | flush(fd); 683 | 684 | return true; // command succeeded 685 | } 686 | 687 | // Determine which interactive mode user is requesting 688 | // There are two options: either give an address and ipm query on the command 689 | // line, receive a result and exit, or launch an interactive menu from which 690 | // to select queries. 691 | bool naiipm::setInteractiveMode(int fd) 692 | { 693 | // If giving address and query on command line, ensure both exist 694 | // and are valid; 695 | // got -a but not -c 696 | if (atoi(args.Address()) != -1 and strcmp(args.Cmd(),"") == 0) 697 | { 698 | commands.verify(args.Cmd()); 699 | return false; 700 | } 701 | // got -c but not -a 702 | if (atoi(args.Address()) == -1 and strcmp(args.Cmd(),"") != 0) 703 | { 704 | std::cout << "Setting default address of 0" << std::endl; 705 | args.setAddress("0"); 706 | return true; 707 | } 708 | 709 | // iPM command (-c) and address (-a) both given on command line 710 | // so send query to iPM 711 | if (atoi(args.Address()) != -1 and strcmp(args.Cmd(),"") != 0) 712 | { 713 | return true; 714 | } 715 | 716 | // didn't get -a or -c, so print menu and wait for user input 717 | bool status = true; 718 | while (status == true) 719 | { 720 | commands.printMenu(); 721 | status = readInput(fd); 722 | } 723 | 724 | return status; // will be false if user requested to quit 725 | } 726 | 727 | // Flush serial port 728 | void naiipm::flush(int fd) 729 | { 730 | if (tcflush(fd, TCIOFLUSH) == -1) 731 | { 732 | std::cout << "Flush returned error " << errno << std::endl; 733 | } 734 | 735 | } 736 | 737 | bool naiipm::readInput(int fd) 738 | { 739 | std::string cmd = ""; 740 | 741 | // Request user input 742 | std::cin >> (cmd); 743 | if (args.Verbose()) 744 | { 745 | std::cout << "User requested " << cmd << std::endl; 746 | } 747 | 748 | // Catch exit request 749 | if (cmd.compare("q") == 0) 750 | { 751 | if (args.Verbose()) 752 | { 753 | std::cout << "Exiting..." << std::endl; 754 | } 755 | return false; 756 | } 757 | 758 | // Confirm command is in list of acceptable command 759 | // If it is not, ask user to enter another command 760 | if (not commands.verify(cmd)) {return true;} 761 | 762 | // If command is valid, send to ipm 763 | const char *cmdInput = cmd.c_str(); 764 | if (cmd.compare("ADR") == 0) 765 | { 766 | // ADR is only command that requires a second component 767 | std::string addr = ""; 768 | std::cout << "Which address would you like to activate (0-7)?" 769 | << std::endl; 770 | std::cin >> (addr); 771 | std::cout << "User requested " << cmd << " " << addr << std::endl; 772 | clear(fd, atoi(addr.c_str())); 773 | if (not send_command(fd, (char *)cmdInput, addr)) { return false; } 774 | } else { 775 | if (not send_command(fd, (char *)cmdInput)) { return false; } 776 | } 777 | 778 | parse_binary(cmd); 779 | 780 | return true; 781 | } 782 | void naiipm::parse_binary(std::string cmd) 783 | { 784 | // Check if command has binary data component. If so, parse it into 785 | // it's component variables. 786 | if (_ipm_data.find(cmd) != _ipm_data.end()) // found cmd in binary map 787 | { 788 | // Parse binary data 789 | parseData(cmd, 0); // In interactive mode, only one address is used 790 | } 791 | } 792 | 793 | void naiipm::parseData(std::string cmd, int adr) 794 | { 795 | if (args.Verbose()) 796 | { 797 | std::cout << '{' << cmd << '}' << std::endl; 798 | std::cout << "In parseData: Info for address " << adr << " is " << 799 | args.Addr(adr) << "," << args.Procqueries(adr) << "," << 800 | args.Addrport(adr) << std::endl; 801 | } 802 | // retrieve binary data 803 | char* data = getData(cmd); // data content 804 | 805 | // Create some pointers to access data of various lengths 806 | uint8_t *cp = (uint8_t *)data; 807 | uint16_t *sp = (uint16_t *)data; 808 | uint32_t *lp = (uint32_t *)data; 809 | unsigned char *up = (unsigned char *)data; 810 | 811 | // parse data 812 | if (cmd == "BITRESULT?") { 813 | ipmBitresult _bitresult; 814 | _bitresult.parse(sp); 815 | _bitresult.createUDP(buffer, args.scaleflag()); 816 | 817 | if (args.Verbose()) 818 | { 819 | std::cout << "iPM temperature (C) = " 820 | << _bitresult.getTemperature() << std::endl; 821 | } 822 | 823 | } 824 | 825 | if (cmd == "RECORD?") { 826 | ipmRecord _record; 827 | _record.parse(cp, sp, lp); 828 | 829 | // CRC validation doesn't currently work. See notes in src/record.cc 830 | // Leaving the code here so that this can be investigated more later 831 | // if desired. 832 | uint32_t crc = _record.calculateCRC32(&up[0], 64); 833 | //_record.checkCRC(cp, crc); 834 | // If CRC from the data and calculatedCRC don't match, increment bad 835 | // data counter: 836 | // trackBadData(); 837 | 838 | if (args.Verbose()) 839 | { 840 | std::cout << _record.getTimeSincePowerup() 841 | << " minutes since power-up" << std::endl; 842 | } 843 | 844 | _record.createUDP(buffer, args.scaleflag()); 845 | } 846 | 847 | if (cmd == "MEASURE?") { 848 | ipmMeasure _measure; 849 | _measure.parse(cp, sp); 850 | _measure.createUDP(buffer, args.scaleflag()); 851 | } 852 | 853 | if (cmd == "STATUS?") { 854 | ipmStatus _status; 855 | _status.parse(cp, sp); 856 | _status.createUDP(buffer, args.scaleflag(), _badData); 857 | } 858 | 859 | if (args.Interactive()) 860 | { 861 | std::cout << buffer << std::endl; 862 | } else 863 | { 864 | send_udp(buffer, adr); 865 | } 866 | 867 | } 868 | --------------------------------------------------------------------------------