diff --git a/README.md b/README.md index 371026e..0881c7e 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,15 @@ -# HIL Tester for PER +# HIL2 Tester for PER -## Running +## Folders -- Code in `./TestBench` runs on the Arduino - - Basically it just reads commands over the serial port and either executs them or writes messages back over the serial port - - To flash it, use the Arduino IDE -- Code in `./scripts` starts the Python code that runs on your laptop - - It uses all the Python files - - Each file in `./scripts` can run a Pytest script to test some board or signal set on the car -- Make sure you correctly set `firmware_path` in `./hil_params.json` to the path of the primary PER firmware repo! +- `./TestBench`: the Teensy code +- `./hil2`: the main HIL "engine" +- `./mk_assert`: a simple and low magic test framework +- `./tests`: the test scripts and configuration files +- `./device_configs`: the device configuration files -## Python ibraries +## Python libraries - `pyserial` for serial communication -- `pytest` (and `pytest-check`) for testing -- `python-can`, `cantools`, and `gs_usb` for CAN communication -- `numpy` for data types -- `jsonschema` for validating JSON files - -## Notes - -### Input vs Output - -- `AI`/`DI` = inputs to hil (reads from the car/other board -> Arduino -> laptop/Python) -- `AO`/`DO` = outputs from hil (writes from laptop/Python -> Arduino -> car/other board) \ No newline at end of file +- `cantools` for CAN DBC encoding/decoding +- `colorama` for cross platform colored terminal output diff --git a/TestBench/Adafruit_MCP4706.cpp b/TestBench/Adafruit_MCP4706.cpp new file mode 100644 index 0000000..5c8c44c --- /dev/null +++ b/TestBench/Adafruit_MCP4706.cpp @@ -0,0 +1,77 @@ +/**************************************************************************/ +/*! + Authors: + - Original: K.Townsend (Adafruit Industries) + - Modified for MCP4706 by Pio Baettig + - Now modifed by Millan kumar for PER HIL 2.0 2025 usage +*/ +/**************************************************************************/ + +#if ARDUINO >= 100 + #include "Arduino.h" +#else + #include "WProgram.h" +#endif + +#include + +#include "Adafruit_MCP4706.h" + +/**************************************************************************/ +/*! + @brief Instantiates a new MCP4706 class +*/ +/**************************************************************************/ +Adafruit_MCP4706::Adafruit_MCP4706() { } + +/**************************************************************************/ +/*! + @brief Setups the HW +*/ +/**************************************************************************/ +void Adafruit_MCP4706::begin(uint8_t addr, TwoWire &wire) { + _i2caddr = addr; + _wire = &wire; + _wire->begin(); +} + +/**************************************************************************/ +/*! + @brief Sets the output voltage to a fraction of source vref. (Value + can be 0..255) + + @param[in] output + The 8-bit value representing the relationship between + the DAC's input voltage and its output voltage. +*/ +/**************************************************************************/ +void Adafruit_MCP4706::setVoltage(uint8_t output) { + uint8_t twbrback = TWBR; + TWBR = 12; // 400 khz + // TWBR = 72; // 100 khz + _wire->beginTransmission(_i2caddr); + _wire->write(MCP4706_CMD_VOLDAC); // First byte: Command (CMD_VOLDAC = 0) + _wire->write(output); // Second byte: Data bits (D7.D6.D5.D4.D3.D2.D1.D0) + _wire->endTransmission(); + TWBR = twbrback; +} + + +/**************************************************************************/ +/*! + @brief Puts the DAC into a low power state using the specified resistor mode + + @param[in] mode + Power-down mode, one of: + - MCP4706_AWAKE + - MCP4706_PWRDN_1K + - MCP4706_PWRDN_100K + - MCP4706_PWRDN_500K +*/ +/**************************************************************************/ +void Adafruit_MCP4706::setMode(uint8_t mode) { + uint8_t config = (mode & ~MCP4706_PWRDN_MASK); // ensure only power-down bits are set + _wire->beginTransmission(_i2caddr); + _wire->write(config | MCP4706_CMD_VOLCONFIG); // command with config register + _wire->endTransmission(); +} \ No newline at end of file diff --git a/TestBench/Adafruit_MCP4706.h b/TestBench/Adafruit_MCP4706.h new file mode 100644 index 0000000..f36c54a --- /dev/null +++ b/TestBench/Adafruit_MCP4706.h @@ -0,0 +1,42 @@ +/**************************************************************************/ +/*! + Authors: + - Original: K.Townsend (Adafruit Industries) + - Modified for MCP4706 by Pio Baettig + - Now modifed by Millan kumar for PER HIL 2.0 2025 usage +*/ +/**************************************************************************/ + +#if ARDUINO >= 100 + #include "Arduino.h" +#else + #include "WProgram.h" +#endif + +#include + +// Power Down Mode definitions +#define MCP4706_PWRDN_MASK 0xF9 +#define MCP4706_AWAKE 0x00 +#define MCP4706_PWRDN_1K 0x02 +#define MCP4706_PWRDN_100K 0x04 +#define MCP4706_PWRDN_500K 0x06 + +// Command definitioins +#define MCP4706_CMD_MASK 0x1F +#define MCP4706_CMD_VOLDAC 0x00 +#define MCP4706_CMD_VOLALL 0x40 +#define MCP4706_CMD_VOLCONFIG 0x80 +#define MCP4706_CMD_ALL 0x60 + +class Adafruit_MCP4706{ + public: + Adafruit_MCP4706(); + void begin(uint8_t addr, TwoWire &wire = Wire); + void setVoltage(uint8_t output); + void setMode(uint8_t mode); + + private: + uint8_t _i2caddr; + TwoWire *_wire; +}; diff --git a/TestBench/DFRobot_MCP4725.cpp b/TestBench/DFRobot_MCP4725.cpp deleted file mode 100644 index ae212f5..0000000 --- a/TestBench/DFRobot_MCP4725.cpp +++ /dev/null @@ -1,394 +0,0 @@ -/*! - * @file DFRobot_MCP4725.cpp - * @brief Implementation of the MCP4725 function library class definition - * @copyright Copyright (c) 2010 DFRobot Co.Ltd (http://www.dfrobot.com) - * @license The MIT License (MIT) - * @author [TangJie]](jie.tang@dfrobot.com) - * @version V1.0.0 - * @date 2018-01-15 - * @url https://github.com/DFRobot/DFRobot_MCP4725 - */ -#include "DFRobot_MCP4725.h" - -const PROGMEM uint16_t DACLookup_FullSine_5Bit[32] = -{ - 2048, 2447, 2831, 3185, 3495, 3750, 3939, 4056, - 4095, 4056, 3939, 3750, 3495, 3185, 2831, 2447, - 2048, 1648, 1264, 910, 600, 345, 156, 39, - 0, 39, 156, 345, 600, 910, 1264, 1648 -}; - -const PROGMEM uint16_t DACLookup_FullSine_6Bit[64] = -{ - 2048, 2248, 2447, 2642, 2831, 3013, 3185, 3346, - 3495, 3630, 3750, 3853, 3939, 4007, 4056, 4085, - 4095, 4085, 4056, 4007, 3939, 3853, 3750, 3630, - 3495, 3346, 3185, 3013, 2831, 2642, 2447, 2248, - 2048, 1847, 1648, 1453, 1264, 1082, 910, 749, - 600, 465, 345, 242, 156, 88, 39, 10, - 0, 10, 39, 88, 156, 242, 345, 465, - 600, 749, 910, 1082, 1264, 1453, 1648, 1847 -}; - -const PROGMEM uint16_t DACLookup_FullSine_7Bit[128] = -{ - 2048, 2148, 2248, 2348, 2447, 2545, 2642, 2737, - 2831, 2923, 3013, 3100, 3185, 3267, 3346, 3423, - 3495, 3565, 3630, 3692, 3750, 3804, 3853, 3898, - 3939, 3975, 4007, 4034, 4056, 4073, 4085, 4093, - 4095, 4093, 4085, 4073, 4056, 4034, 4007, 3975, - 3939, 3898, 3853, 3804, 3750, 3692, 3630, 3565, - 3495, 3423, 3346, 3267, 3185, 3100, 3013, 2923, - 2831, 2737, 2642, 2545, 2447, 2348, 2248, 2148, - 2048, 1947, 1847, 1747, 1648, 1550, 1453, 1358, - 1264, 1172, 1082, 995, 910, 828, 749, 672, - 600, 530, 465, 403, 345, 291, 242, 197, - 156, 120, 88, 61, 39, 22, 10, 2, - 0, 2, 10, 22, 39, 61, 88, 120, - 156, 197, 242, 291, 345, 403, 465, 530, - 600, 672, 749, 828, 910, 995, 1082, 1172, - 1264, 1358, 1453, 1550, 1648, 1747, 1847, 1947 -}; - -const PROGMEM uint16_t DACLookup_FullSine_8Bit[256] = -{ - 2048, 2098, 2148, 2198, 2248, 2298, 2348, 2398, - 2447, 2496, 2545, 2594, 2642, 2690, 2737, 2784, - 2831, 2877, 2923, 2968, 3013, 3057, 3100, 3143, - 3185, 3226, 3267, 3307, 3346, 3385, 3423, 3459, - 3495, 3530, 3565, 3598, 3630, 3662, 3692, 3722, - 3750, 3777, 3804, 3829, 3853, 3876, 3898, 3919, - 3939, 3958, 3975, 3992, 4007, 4021, 4034, 4045, - 4056, 4065, 4073, 4080, 4085, 4089, 4093, 4094, - 4095, 4094, 4093, 4089, 4085, 4080, 4073, 4065, - 4056, 4045, 4034, 4021, 4007, 3992, 3975, 3958, - 3939, 3919, 3898, 3876, 3853, 3829, 3804, 3777, - 3750, 3722, 3692, 3662, 3630, 3598, 3565, 3530, - 3495, 3459, 3423, 3385, 3346, 3307, 3267, 3226, - 3185, 3143, 3100, 3057, 3013, 2968, 2923, 2877, - 2831, 2784, 2737, 2690, 2642, 2594, 2545, 2496, - 2447, 2398, 2348, 2298, 2248, 2198, 2148, 2098, - 2048, 1997, 1947, 1897, 1847, 1797, 1747, 1697, - 1648, 1599, 1550, 1501, 1453, 1405, 1358, 1311, - 1264, 1218, 1172, 1127, 1082, 1038, 995, 952, - 910, 869, 828, 788, 749, 710, 672, 636, - 600, 565, 530, 497, 465, 433, 403, 373, - 345, 318, 291, 266, 242, 219, 197, 176, - 156, 137, 120, 103, 88, 74, 61, 50, - 39, 30, 22, 15, 10, 6, 2, 1, - 0, 1, 2, 6, 10, 15, 22, 30, - 39, 50, 61, 74, 88, 103, 120, 137, - 156, 176, 197, 219, 242, 266, 291, 318, - 345, 373, 403, 433, 465, 497, 530, 565, - 600, 636, 672, 710, 749, 788, 828, 869, - 910, 952, 995, 1038, 1082, 1127, 1172, 1218, - 1264, 1311, 1358, 1405, 1453, 1501, 1550, 1599, - 1648, 1697, 1747, 1797, 1847, 1897, 1947, 1997 -}; - -const PROGMEM uint16_t DACLookup_FullSine_9Bit[512] = -{ - 2048, 2073, 2098, 2123, 2148, 2174, 2199, 2224, - 2249, 2274, 2299, 2324, 2349, 2373, 2398, 2423, - 2448, 2472, 2497, 2521, 2546, 2570, 2594, 2618, - 2643, 2667, 2690, 2714, 2738, 2762, 2785, 2808, - 2832, 2855, 2878, 2901, 2924, 2946, 2969, 2991, - 3013, 3036, 3057, 3079, 3101, 3122, 3144, 3165, - 3186, 3207, 3227, 3248, 3268, 3288, 3308, 3328, - 3347, 3367, 3386, 3405, 3423, 3442, 3460, 3478, - 3496, 3514, 3531, 3548, 3565, 3582, 3599, 3615, - 3631, 3647, 3663, 3678, 3693, 3708, 3722, 3737, - 3751, 3765, 3778, 3792, 3805, 3817, 3830, 3842, - 3854, 3866, 3877, 3888, 3899, 3910, 3920, 3930, - 3940, 3950, 3959, 3968, 3976, 3985, 3993, 4000, - 4008, 4015, 4022, 4028, 4035, 4041, 4046, 4052, - 4057, 4061, 4066, 4070, 4074, 4077, 4081, 4084, - 4086, 4088, 4090, 4092, 4094, 4095, 4095, 4095, - 4095, 4095, 4095, 4095, 4094, 4092, 4090, 4088, - 4086, 4084, 4081, 4077, 4074, 4070, 4066, 4061, - 4057, 4052, 4046, 4041, 4035, 4028, 4022, 4015, - 4008, 4000, 3993, 3985, 3976, 3968, 3959, 3950, - 3940, 3930, 3920, 3910, 3899, 3888, 3877, 3866, - 3854, 3842, 3830, 3817, 3805, 3792, 3778, 3765, - 3751, 3737, 3722, 3708, 3693, 3678, 3663, 3647, - 3631, 3615, 3599, 3582, 3565, 3548, 3531, 3514, - 3496, 3478, 3460, 3442, 3423, 3405, 3386, 3367, - 3347, 3328, 3308, 3288, 3268, 3248, 3227, 3207, - 3186, 3165, 3144, 3122, 3101, 3079, 3057, 3036, - 3013, 2991, 2969, 2946, 2924, 2901, 2878, 2855, - 2832, 2808, 2785, 2762, 2738, 2714, 2690, 2667, - 2643, 2618, 2594, 2570, 2546, 2521, 2497, 2472, - 2448, 2423, 2398, 2373, 2349, 2324, 2299, 2274, - 2249, 2224, 2199, 2174, 2148, 2123, 2098, 2073, - 2048, 2023, 1998, 1973, 1948, 1922, 1897, 1872, - 1847, 1822, 1797, 1772, 1747, 1723, 1698, 1673, - 1648, 1624, 1599, 1575, 1550, 1526, 1502, 1478, - 1453, 1429, 1406, 1382, 1358, 1334, 1311, 1288, - 1264, 1241, 1218, 1195, 1172, 1150, 1127, 1105, - 1083, 1060, 1039, 1017, 995, 974, 952, 931, - 910, 889, 869, 848, 828, 808, 788, 768, - 749, 729, 710, 691, 673, 654, 636, 618, - 600, 582, 565, 548, 531, 514, 497, 481, - 465, 449, 433, 418, 403, 388, 374, 359, - 345, 331, 318, 304, 291, 279, 266, 254, - 242, 230, 219, 208, 197, 186, 176, 166, - 156, 146, 137, 128, 120, 111, 103, 96, - 88, 81, 74, 68, 61, 55, 50, 44, - 39, 35, 30, 26, 22, 19, 15, 12, - 10, 8, 6, 4, 2, 1, 1, 0, - 0, 0, 1, 1, 2, 4, 6, 8, - 10, 12, 15, 19, 22, 26, 30, 35, - 39, 44, 50, 55, 61, 68, 74, 81, - 88, 96, 103, 111, 120, 128, 137, 146, - 156, 166, 176, 186, 197, 208, 219, 230, - 242, 254, 266, 279, 291, 304, 318, 331, - 345, 359, 374, 388, 403, 418, 433, 449, - 465, 481, 497, 514, 531, 548, 565, 582, - 600, 618, 636, 654, 673, 691, 710, 729, - 749, 768, 788, 808, 828, 848, 869, 889, - 910, 931, 952, 974, 995, 1017, 1039, 1060, - 1083, 1105, 1127, 1150, 1172, 1195, 1218, 1241, - 1264, 1288, 1311, 1334, 1358, 1382, 1406, 1429, - 1453, 1478, 1502, 1526, 1550, 1575, 1599, 1624, - 1648, 1673, 1698, 1723, 1747, 1772, 1797, 1822, - 1847, 1872, 1897, 1922, 1948, 1973, 1998, 2023 -}; - -bool DFRobot_MCP4725::check_mcp4725() -{ - uint8_t error; - Wire.beginTransmission(_IIC_addr); - error = Wire.endTransmission(); - if(error == 0){ - return true; - }else{ - return false; - } -} -void DFRobot_MCP4725::init(uint8_t addr, uint16_t vRef) -{ - byte error; - _IIC_addr = addr; - _refVoltage = vRef; - _PowerMode = MCP4725_NORMAL_MODE; - Wire.begin(); - Wire.beginTransmission(_IIC_addr); - - Wire.endTransmission(); - /* - while(error) - { - Wire.beginTransmission(_IIC_addr); - - error = Wire.endTransmission(); - Serial.println("ERROR! Not found I2C device address "); - delay(500); - } - */ -} - -void DFRobot_MCP4725::setMode(uint8_t powerMode) -{ - _PowerMode = powerMode; - outputVoltage(_voltage); -} - -void DFRobot_MCP4725::outputVoltage( uint16_t voltage) -{ - uint16_t data = 0; - _voltage = voltage; - if(_voltage > _refVoltage) - { - Serial.print("ERROR! The input voltage is greater than the maximum voltage!"); - return ; - } - else - { - data = (uint16_t)(((float)_voltage / _refVoltage) * 4095); - - Wire.beginTransmission(_IIC_addr); - - Wire.write(MCP4725_Write_CMD | (_PowerMode << 1)); - - Wire.write(data / 16); - Wire.write((data % 16) << 4); - Wire.endTransmission(); - } -} - -void DFRobot_MCP4725::outputVoltageEEPROM( uint16_t voltage) -{ - uint16_t data = 0; - _voltage = voltage; - if(_voltage > _refVoltage) - { - Serial.print("ERROR! The input voltage is greater than the maximum voltage!"); - return ; - } - else - { - data = (uint16_t)(((float)_voltage / _refVoltage) * 4095); - - Wire.beginTransmission(_IIC_addr); - Wire.write(MCP4725_WriteEEPROM_CMD | (_PowerMode << 1)); - Wire.write(data / 16); - Wire.write((data % 16) << 4); - Wire.endTransmission(); - } -} - -void DFRobot_MCP4725::outputTriangle(uint16_t amp, uint16_t freq, uint16_t offset, uint8_t dutyCycle) -{ - uint64_t starttime; - uint64_t stoptime; - uint64_t looptime; - uint64_t frame; - uint16_t num = 64; - uint16_t up_num; - uint16_t down_num; - uint16_t maxV; - maxV=amp*(4096/(float)_refVoltage); - if(freq > 100){ - num = 16; - }else if(50 <= freq && freq <= 100){ - num = 32; - }else{ - num = 64; - } - frame = 1000000/(freq*num*2); - if(dutyCycle>100){ - dutyCycle = 100; - } - if(dutyCycle<0){ - dutyCycle=0; - } - up_num = (2*num)*((float)dutyCycle/100); - down_num = ((2*num) - up_num); -#ifdef TWBR - uint8_t twbrback = TWBR; - TWBR = ((F_CPU / 400000L) - 16) / 2; // Set I2C frequency to 400kHz -#endif - uint32_t counter; - uint32_t enterV; - - for (counter = 0; counter < (maxV-(maxV/up_num)-1); counter+=(maxV/up_num)) - { - starttime = micros(); - enterV=counter+(offset*(4096/(float)_refVoltage)); - if(enterV > 4095){ - enterV = 4095; - }else if(enterV < 0){ - enterV = 0; - } - Wire.beginTransmission(_IIC_addr); - Wire.write(MCP4725_Write_CMD | (_PowerMode << 1)); - Wire.write(enterV / 16); - Wire.write((enterV % 16) << 4); - Wire.endTransmission(); - stoptime = micros(); - looptime = stoptime-starttime; - while(looptime <= frame){ - stoptime = micros(); - looptime = stoptime-starttime; - } - } - for (counter = maxV-1; counter > (maxV/down_num); counter-=(maxV/down_num)) - { - starttime = micros(); - enterV=counter+(offset*(4096/(float)_refVoltage)); - if(enterV > 4095){ - enterV = 4095; - }else if(enterV < 0){ - enterV = 0; - } - Wire.beginTransmission(_IIC_addr); - Wire.write(MCP4725_Write_CMD | (_PowerMode << 1)); - Wire.write(enterV / 16); - Wire.write((enterV % 16) << 4); - Wire.endTransmission(); - stoptime = micros(); - looptime = stoptime-starttime; - while(looptime <= frame){ - stoptime = micros(); - looptime = stoptime-starttime; - } - } -#ifdef TWBR - TWBR = twbrback; -#endif -} - -void DFRobot_MCP4725::outputSin(uint16_t amp, uint16_t freq, uint16_t offset) -{ - uint64_t starttime; - uint64_t stoptime; - uint64_t looptime; - uint64_t frame; - uint16_t num=512; -#ifdef TWBR - uint8_t twbrback = TWBR; - TWBR = ((F_CPU / 400000L) - 16) / 2; // Set I2C frequency to 400kHz -#endif - int16_t data = 0; - - if(freq < 8){ - num = 512; - }else if( 8 <= freq && freq <= 16){ - num = 256; - }else if(16 < freq && freq < 33){ - num = 128; - }else if(33 <= freq && freq <= 68 ){ - num = 64; - }else{ - num = 32; - } - if(freq > 100){ - freq = 100; - } - frame = 1000000/(freq*num); - for(int i=0;i= 4095){ - data=4095; - } - Wire.beginTransmission(_IIC_addr); - Wire.write(MCP4725_Write_CMD); - Wire.write(data / 16); - Wire.write((data % 16) << 4); - Wire.endTransmission(); - stoptime = micros(); - looptime = stoptime-starttime; - while(looptime <= frame){ - stoptime = micros(); - looptime = stoptime-starttime; - } - } -#ifdef TWBR - TWBR = twbrback; -#endif -} diff --git a/TestBench/DFRobot_MCP4725.h b/TestBench/DFRobot_MCP4725.h deleted file mode 100644 index a468074..0000000 --- a/TestBench/DFRobot_MCP4725.h +++ /dev/null @@ -1,101 +0,0 @@ -/*! - * @file DFRobot_MCP4725.h - * @brief Definition and explanation of the MCP4725 function library class - * @copyright Copyright (c) 2010 DFRobot Co.Ltd (http://www.dfrobot.com) - * @license The MIT License (MIT) - * @author [TangJie]](jie.tang@dfrobot.com) - * @version V1.0.0 - * @date 2018-01-15 - * @url https://github.com/DFRobot/DFRobot_MCP4725 - */ - -#if ARDUINO >= 100 - #include "Arduino.h" -#else - #include "WProgram.h" -#endif - -#include - -#define MCP4725_Write_CMD 0x40 ///< Write data to the DAC address. -#define MCP4725_WriteEEPROM_CMD 0x60 ///< Write data to the DAC EEPROM address. - -///< The IIC address of MCP4725A0 may be 0x60 or 0x61, depending on the location of the dial code switch on the sensor. -#define MCP4725A0_IIC_Address0 0x60 -#define MCP4725A0_IIC_Address1 0x61 - -#define MCP4725_NORMAL_MODE 0 -#define MCP4725_POWER_DOWN_1KRES 1 -#define MCP4725_POWER_DOWN_100KRES 2 -#define MCP4725_POWER_DOWN_500KRES 3 - -class DFRobot_MCP4725{ -public: - /** - * @fn init - * @brief init MCP4725 device - * @param addr Init the IIC address. - * @param vRefSetting the base voltage of DAC must equal the power supply voltage, and the unit is millivolt. - * @return None - */ - void init(uint8_t addr, uint16_t vRef); - - /** - * @fn setMode - * @brief set power mode - * @param powerMode Set power mode,three are normal mode and power down mode. - * @n The following are three modes of power down. - * @n MCP4725_POWER_DOWN_1KRES 1 kΩ resistor to ground - * @n MCP4725_POWER_DOWN_100KRES 100 kΩ resistor to ground - * @n MCP4725_POWER_DOWN_500KRES 500 kΩ resistor to ground - * @return None - */ - void setMode(uint8_t powerMode); - - /** - * @fn outputVoltage - * @brief Output voltage value range 0-5000mv. - * @param voltage Voltage value, range 0-5000, unit millivolt. - * @return None - */ - void outputVoltage(uint16_t voltage); - - /** - * @fn outputVoltageEEPROM - * @brief Output voltage value range 0-5000mv and write to the EEPROM, - * @n meaning that the DAC will retain the current voltage output - * @n after power-down or reset. - * @param voltage Voltage value, range 0-5000, unit millivolt. - * @return None - */ - void outputVoltageEEPROM(uint16_t voltage); - - /** - * @fn outputSin - * @brief Output a sine wave. - * @param amp amp value, output sine wave amplitude range 0-5000mv - * @param freq freq value, output sine wave frequency - * @param offset offset value, output sine wave DC offset - * @return None - */ - void outputSin(uint16_t amp, uint16_t freq, uint16_t offset); - - /** - * @fn outputTriangle - * @brief Output a sine wave. - * @param amp amp value, output triangular wave amplitude range 0-5000mv - * @param freq freq value, output the triangle wave frequency - * @param offset offset value, output the DC offset of the triangle wave - * @param dutyCycle dutyCycle value, set the rising percentage of the triangle wave as a percentage of the entire cycle. - * @n Value range 0-100 (0 for only the decline of 100, only the rise of paragraph) - * @return None - */ - void outputTriangle(uint16_t amp, uint16_t freq, uint16_t offset, uint8_t dutyCycle); - - private: - bool check_mcp4725(); - uint8_t _IIC_addr; - uint8_t _PowerMode; - uint16_t _refVoltage; - uint16_t _voltage; -}; diff --git a/TestBench/MCP4021.cpp b/TestBench/MCP4021.cpp deleted file mode 100644 index c78ca54..0000000 --- a/TestBench/MCP4021.cpp +++ /dev/null @@ -1,256 +0,0 @@ - -/* - Microchip 63 taps Single Digital Potentiometer - Simple two-wire UP/DOWN interface - Author: dzalf - Daniel Melendrez - Date: June 2020 (COVID-19 Vibes) - Version: 1.1.1 - Initial deployment - 1.1.2 - General cleanup. Implemented new and overloaded methods that allow - to select the desired tap. It is now possible to select the nominal - resistance value or override it by setting the measured value - -*/ - -#include "MCP4021.h" - -// Constructor - -MCP4021::MCP4021(uint8_t CS, uint8_t UD) { - - _CSPin = CS; - _UDPin = UD; - _debug = false; -} - -MCP4021::MCP4021(uint8_t CS, uint8_t UD, bool DEBUG) { - - _CSPin = CS; - _UDPin = UD; - _debug = DEBUG; -} - -// Methods - -void MCP4021::setup() { - - pinMode(_UDPin, OUTPUT); - pinMode(_CSPin, OUTPUT); - - digitalWrite(_CSPin, HIGH); // _CSPin is active low, keep it HIGH at the beginning - digitalWrite(_UDPin, HIGH); // _UDPin -> U is active HIGH, keep it LOW at the beginning -} - -void MCP4021::begin() { - _tapPointer = MCP4021_DEFAULT_TAP_COUNT; - _nominalResistance = MCP4021_NOMINAL_RESISTANCE; - - if(_debug){ - Serial.println(F("Initializing MCP4021...")); - } -} - -void MCP4021::begin(float nomRes) { - _tapPointer = MCP4021_DEFAULT_TAP_COUNT; - _nominalResistance = nomRes; - - if(_debug){ - Serial.println(F("Initializing MCP4021...")); - } -} - -float MCP4021::wiper() { - return (_tapPointer / MCP4021_TAP_NUMBER); -} - -void MCP4021::inc() { // return wiper count! - - if ((_tapPointer < MCP4021_TAP_NUMBER)) { - // Note: The digitalWrite command is slow enough for the device. No additional delays are needed - - unsigned long _startIncTime = micros(); - digitalWrite(_UDPin, HIGH); // We want the wiper to go UP - - //After at least 500 ns bring _CSPin low - digitalWrite(_CSPin, LOW); // Start the command - - // /* Increment command*/ - //*Subsequent rising edges of _UDPin move the wiper */ - // One tap - digitalWrite(_UDPin, LOW); - - digitalWrite(_UDPin, HIGH); // Last _UDPin state is HIGH - // Here the wiper should have increased already - - // Leave the _CSPin pin ready for next instruction - digitalWrite(_CSPin, HIGH); // Release _CSPin to avoid changing the Pot - - _tapPointer++; - - if (_tapPointer >= MCP4021_TAP_NUMBER) - _tapPointer = MCP4021_TAP_NUMBER; - - _incDelay = micros() - _startIncTime; - //delay(100); // delay to ensure no wiper skip - } -} - -void MCP4021::dec() { - - if ((_tapPointer > 0)) { - - unsigned long _startDecTime = micros(); - - digitalWrite(_UDPin, LOW); // We want the wiper to go DOWN - - //After at least 500 ns bring _CSPin low - digitalWrite(_CSPin, LOW); // Start the "move wiper" command - - // /* Decrement command*/ - // One tap - digitalWrite(_UDPin, HIGH); - digitalWrite(_UDPin, HIGH); - - digitalWrite(_UDPin, LOW); - // Here the wiper should have decreased already - //*Subsequent falling edges of _UDPin move the wiper */ - - // Leave the _CSPin pin ready for next instruction - digitalWrite(_CSPin, HIGH); - - _tapPointer--; - - if (_tapPointer <= 0) - _tapPointer = 0; - - _decDelay = micros() - _startDecTime; - //delay(100); // delay to ensure no wiper skip - - } -} - -unsigned long MCP4021::incMicros() { - return _incDelay; -} - -unsigned long MCP4021::decMicros() { - return _decDelay; -} - -unsigned long MCP4021::setMicros() { - return _setDelay; -} - -int MCP4021::taps() { - return _tapPointer; // value within [1-64] that points to the wiper taps [0,63] -} - -uint8_t MCP4021::setValue(float desiredR) { - - float _currentValue; - float _distance; - int _tapTarget; - - unsigned long _startSetTime = micros(); - - _tapTarget = round((desiredR * MCP4021_TAP_NUMBER) / (_nominalResistance)); - _distance = abs(_tapPointer - _tapTarget); - - if (_debug) { - Serial.print("Distance to target: "); - Serial.println(_distance); - - Serial.print("Target tap: "); - Serial.println(_tapTarget); - } - - if (_tapTarget < _tapPointer) { - for (int i = _tapPointer; i > _tapTarget; i--) { - dec(); - } - - } else if (_tapTarget > _tapPointer) { - for (int i = _tapPointer; i < _tapTarget; i++) { - inc(); - } - - } else { - // Leave everything where it is - } - - _setDelay = micros() - _startSetTime; - - return _tapTarget; -} - -uint8_t MCP4021::setTap(uint8_t desiredTap) { - - float _currentValue; - float _distance; - int _tapTarget; - - unsigned long _startSetTime = micros(); - - _tapTarget = desiredTap; - _distance = abs(_tapPointer - _tapTarget); - - if (_debug) { - Serial.print("Distance to target: "); - Serial.println(_distance); - - Serial.print("Target tap: "); - Serial.println(_tapTarget); - } - - if (_tapTarget < _tapPointer) { - for (int i = _tapPointer; i > _tapTarget; i--) { - dec(); - } - - } else if (_tapTarget > _tapPointer) { - for (int i = _tapPointer; i < _tapTarget; i++) { - inc(); - } - - } else { - // No touchy...Leave everything where it is...drink beer or coffee - } - - _setDelay = micros() - _startSetTime; - - return _tapTarget; -} - -void MCP4021::zeroWiper() { - - // It is possible to do this with the latest setTap() method - /* - - for (int i = MCP4021_DEFAULT_TAP_COUNT ; i >= 0; i--) { - dec(); - } - - */ - - setTap(0); - - _tapPointer = 0; -} - -void MCP4021::maxWiper() { - - // It is possible to do this with the latest setTap() method - /* - - for (int i = _tapPointer; i <= MCP4021_TAP_NUMBER; i++) { - inc(); - } - - */ - setTap(MCP4021_TAP_NUMBER); - - _tapPointer = MCP4021_TAP_NUMBER; -} - -float MCP4021::readValue() { - return (_tapPointer / MCP4021_TAP_NUMBER) * (_nominalResistance); -} diff --git a/TestBench/MCP4021.h b/TestBench/MCP4021.h deleted file mode 100644 index 52052f8..0000000 --- a/TestBench/MCP4021.h +++ /dev/null @@ -1,81 +0,0 @@ - -/* - Microchip 63 taps Single Digital Potentiometer - Simple two-wire UP/DOWN interface - Author: dzalf - Daniel Melendrez - Date: June 2020 (COVID-19 Vibes) - Version: 1.1.1 - Initial deployment - 1.1.2 - General cleanup. Implemented new and overloaded methods that allow - to select the desired tap. It is now possible to select the nominal - resistance value or override it by setting the measured value - -*/ - -#ifndef MCP4021_h -#define MCP4021_h - -#include "Arduino.h" - -#define MCP4021_TAP_NUMBER 63 // Total taps, 63 resistors. Wiper values are from 0x00 to 0x3F -#define MCP4021_DEFAULT_TAP_COUNT 63 // Half way resistance -#define MCP4021_NOMINAL_RESISTANCE 2100 // MCP4021 -#define MCP4021_WIPER_RESISTANCE 75 // 75 typical (According to datasheet) - -class MCP4021 { - - public: - - // Constructors: - MCP4021(uint8_t cs, uint8_t ud); - MCP4021(uint8_t cs, uint8_t ud, bool dbg); - - // Methods: - - // Setup the device's connections - void setup(void); - // Begin the digital potentiometer using a nominal resistance of 100 kOhms - void begin(void); - // Begin the digital potentiometer with a custom value. Overloaded method. - void begin(float); - // Retrieve the currently set tap - int taps(void); - // Retrieve the fractional position of the wiper - float wiper(void); - // Issue a single tap increment command - void inc(void); - // Issue a single tap decrement command - void dec(void); - // Set the wiper position to the minimum - void zeroWiper(void); - // Set the wiper position to the maximum - void maxWiper(void); - // Retrieve a mathematical approximation of the current resistance value - float readValue(void); - // Set the closest possible resistance value -> mathematical approximation - uint8_t setValue(float); - // Set the tap to a desired position within its nominal range - uint8_t setTap(uint8_t); - // Read the time it took for increasing the tap - unsigned long incMicros(void); - // Read the time it took for decreasing the tap - unsigned long decMicros(void); - // Read the time it took for setting the tap - unsigned long setMicros(void); - - private: - - uint8_t _tapPointer; - float _nominalResistance; - - protected: - - uint8_t _CSPin; - uint8_t _UDPin; - unsigned long _incDelay; - unsigned long _decDelay; - unsigned long _setDelay; - bool _debug; - -}; - -#endif diff --git a/TestBench/SW_MCP4017.cpp b/TestBench/SW_MCP4017.cpp new file mode 100644 index 0000000..708bc03 --- /dev/null +++ b/TestBench/SW_MCP4017.cpp @@ -0,0 +1,72 @@ +#include "SW_MCP4017.h" + +MCP4017::MCP4017(uint8_t maxSteps, float maxOhms) { + _maxSteps = maxSteps; + _currentStep = 0; + _currentRout = 0.0; + _maxOhm = maxOhms; +} + +void MCP4017::begin(uint8_t adcAddress, TwoWire &wire) { + I2CADCAddress = adcAddress; + _wire = &wire; + _wire->begin(); +} + +///////////////////////////////////////////////////////////////////////////// +/*! + Sets the resistance of the digital pot by sending a number of steps + +*/ +///////////////////////////////////////////////////////////////////////////// + +void MCP4017::setSteps(uint8_t steps) { + _currentStep = steps; + float temp1 = (float)steps / _maxSteps; + float temp2 = temp1 * _maxOhm; + _currentRout = temp2 + WIPEROHMS; + //_currentRout = (((float)steps / _maxSteps) * _maxOhm) + WIPEROHMS; + I2CSendSteps(_currentStep); + +} + +///////////////////////////////////////////////////////////////////////////// +/*! + Calculates the number of steps to send based on a desired resistance + +*/ +///////////////////////////////////////////////////////////////////////////// + +void MCP4017::setResistance(double Rout) { + uint8_t tempsteps = (int)((_maxSteps * (Rout - WIPEROHMS)) / _maxOhm); + setSteps(tempsteps); +} + +///////////////////////////////////////////////////////////////////////////// +/*! + just in case you need it, a way to calculate the resistance + since most of these potentiometers loss settings at power down (or they simply loose power) + they generally default to their midrange +*/ +///////////////////////////////////////////////////////////////////////////// + +float MCP4017::calcResistance() { + //float Rout; + //Rout = ((_currentStep / _maxSteps) * _maxOhm) + WIPEROHMS; + return _currentRout; +} + +///////////////////////////////////////////////////////////////////////////// +/*! + Here is our actual method where we send the steps over to the Digital Potentiometer! + +*/ +///////////////////////////////////////////////////////////////////////////// + +void MCP4017::I2CSendSteps(uint8_t steps) { + _wire->beginTransmission(I2CADCAddress); + _wire->write(steps); // + _wire->endTransmission(); +} + + diff --git a/TestBench/SW_MCP4017.h b/TestBench/SW_MCP4017.h new file mode 100644 index 0000000..485f5ae --- /dev/null +++ b/TestBench/SW_MCP4017.h @@ -0,0 +1,34 @@ +#ifndef _SW_MCP4017_H +#define _SW_MCP4017_H + + +#if ARDUINO >= 100 + #include "Arduino.h" +#else + #include "WProgram.h" +#endif + +#include + +#define WIPEROHMS 20 //the aproximate amount of extra resistance/error added by the wiper +#define MCP4017ADDRESS 0x2F //Microchip MCP4017 I2C 5K, 10K, 50K and 100K digital potentiometers, default address (only address really) + +class MCP4017 { + public: + MCP4017(uint8_t maxSteps, float maxOhms); + void begin(uint8_t adcAddress, TwoWire &wire = Wire); + void setSteps(uint8_t steps); + void setResistance(double Rout); + float calcResistance(); + + private: + uint8_t I2CADCAddress; + void I2CSendSteps(uint8_t steps); + int _maxSteps; //this is the Vin of the MCP3221 in Millivolts + int _currentStep; + float _maxOhm; + float _currentRout; + TwoWire *_wire; +}; + +#endif \ No newline at end of file diff --git a/TestBench/TestBench.ino b/TestBench/TestBench.ino index c103572..aef76fb 100644 --- a/TestBench/TestBench.ino +++ b/TestBench/TestBench.ino @@ -1,189 +1,276 @@ - #include -#include "MCP4021.h" +#include +#include + +#include "Adafruit_MCP4706.h" +#include "SW_MCP4017.h" +//----------------------------------------------------------------------------// -// #define STM32 -#ifdef STM32 - #define SERIAL SerialUSB - #define HAL_DAC_MODULE_ENABLED 1 -#else - #define SERIAL Serial -#endif const int TESTER_ID = 1; -#define DAC - -#ifdef STM32 - #ifdef DAC - #warning "Can't have both DAC and STM32 enabled" - #endif -#endif - -#ifdef DAC - #include "DFRobot_MCP4725.h" - #define NUM_DACS 2 - - DFRobot_MCP4725 dacs[NUM_DACS]; - uint8_t dac_power_down[NUM_DACS]; - const uint16_t dac_vref = 4095; -#endif - -#define DIGIPOT_EN -#ifdef DIGIPOT_EN - const uint8_t DIGIPOT_UD_PIN = 7; - const uint8_t DIGIPOT_CS1_PIN = 22; // A4 - const uint8_t DIGIPOT_CS2_PIN = 23; // A5 - - MCP4021 digipot1(DIGIPOT_CS1_PIN, DIGIPOT_UD_PIN, false); // initialize Digipot 1 - MCP4021 digipot2(DIGIPOT_CS2_PIN, DIGIPOT_UD_PIN, false); // initialize Digipot 2 -#endif - -enum GpioCommand { - READ_ADC = 0, - READ_GPIO = 1, - WRITE_DAC = 2, - WRITE_GPIO = 3, - READ_ID = 4, - WRITE_POT = 5, + +// Serial conf ---------------------------------------------------------------// +#define SERIAL_BAUDRATE 115200 +#define SERIAL_CON Serial +//----------------------------------------------------------------------------// + +// DAC conf ------------------------------------------------------------------// +#define NUM_DACS 8 +#define DAC_WIRE Wire +#define DAC_SDA 17 +#define DAC_SCL 24 +#define DAC_BASE_ADDR 0x60 +//----------------------------------------------------------------------------// + +// Digipot conf --------------------------------------------------------------// +#define NUM_DIGIPOTS 2 + +#define DIGIPOT_0_WIRE Wire1 +#define DIGIPOT_0_SDA 25 +#define DIGIPOT_0_SCL 16 + +#define DIGIPOT_1_WIRE Wire2 +#define DIGIPOT_1_SDA 18 +#define DIGIPOT_1_SCL 19 + +const uint8_t DIGIPOT_MAX_STEPS = 128; +const float DIGIPOT_MAX_OHMS = 10000; +//----------------------------------------------------------------------------// + +// CAN conf ------------------------------------------------------------------// +#define CAN_BAUDRATE 500000 +#define CAN_RX RX_SIZE_256 +#define CAN_TX TX_SIZE_16 + +#define VCAN_BUS 1 +#define MCAN_BUS 2 +//----------------------------------------------------------------------------// + + +// Global peripherals --------------------------------------------------------// +Adafruit_MCP4706 dacs[NUM_DACS]; +bool dac_power_down[NUM_DACS]; + +MCP4017 digipots[NUM_DIGIPOTS] = { + MCP4017(DIGIPOT_MAX_STEPS, DIGIPOT_MAX_OHMS), + MCP4017(DIGIPOT_MAX_STEPS, DIGIPOT_MAX_OHMS) +}; + +FlexCAN_T4 vCan; // bus: 1 +FlexCAN_T4 mCan; // bus: 2 +CAN_message_t recv_msg = { 0 }; +//----------------------------------------------------------------------------// + + +// Serial commands -----------------------------------------------------------// +enum SerialCommand : uint8_t { + READ_ID = 0, // command -> READ_ID, id + WRITE_GPIO = 1, // command, pin, value -> [] + HIZ_GPIO = 2, // command, pin -> [] + READ_GPIO = 3, // command, pin -> READ_GPIO, value + WRITE_DAC = 4, // command, pin/offset, value -> [] + HIZ_DAC = 5, // command, pin/offset -> [] + READ_ADC = 6, // command, pin -> READ_ADC, value high, value low + WRITE_POT = 7, // command, pin/offset, value -> [] + SEND_CAN = 8, // command, bus, signal high, signal low, length, data (8 bytes) -> [] + RECV_CAN = 9, // -> CAN_MESSAGE, bus, signal high, signal low, length, data (length bytes) + ERROR = 10, // -> ERROR, command }; -int TO_READ[] = { // Parrallel to GpioCommand - 2, // READ_ADC - command, pin - 2, // READ_GPIO - command, pin - 4, // WRITE_DAC - command, pin, value (2 bytes) - 3, // WRITE_GPIO - command, pin, value - 1, // READ_ID - command - 3, // WRITE_POT - command, pin, value +size_t TO_READ[] = { // Parrallel to SerialCommand + 1, // READ_ID + 3, // WRITE_GPIO + 2, // HIZ_GPIO + 2, // READ_GPIO + 3, // WRITE_DAC + 2, // HIZ_DAC + 2, // READ_ADC + 3, // WRITE_POT + 13, // SEND_CAN }; -// 4 = max(TO_READ) -uint8_t data[4] = {-1, -1, -1, -1}; -int data_index = 0; -bool data_ready = false; +// 13 = max(TO_READ) +uint8_t g_serial_data[13] = { 0 }; +size_t g_data_idx = 0; +bool g_data_ready = false; +//----------------------------------------------------------------------------// +// Setup ---------------------------------------------------------------------// void setup() { - SERIAL.begin(115200); - -#ifdef DIGIPOT_EN - // Setting up Digipot 1 - digipot1.setup(); - digipot1.begin(); - - // Setting up Digipot 2 - digipot2.setup(); - digipot2.begin(); -#endif -#ifdef DAC - dacs[0].init(0x62, dac_vref); - dacs[1].init(0x63, dac_vref); - dacs[0].setMode(MCP4725_POWER_DOWN_500KRES); - dacs[1].setMode(MCP4725_POWER_DOWN_500KRES); - dac_power_down[0] = 1; - dac_power_down[1] = 1; -#endif -} + // Serial setup + SERIAL_CON.begin(SERIAL_BAUDRATE); + + // DAC setup + DAC_WIRE.setSDA(DAC_SDA); + DAC_WIRE.setSCL(DAC_SCL); + + for (int i = 0; i < NUM_DACS; i++) { + uint8_t addr = DAC_BASE_ADDR + i; + dacs[i].begin(addr, DAC_WIRE); + + dacs[i].setMode(MCP4706_PWRDN_500K); + dac_power_down[i] = true; // start with power down + } -void error(String error_string) { - SERIAL.write(0xFF); - SERIAL.write(0xFF); - SERIAL.println(error_string); + // Digipot setup + DIGIPOT_0_WIRE.setSDA(DIGIPOT_0_SDA); + DIGIPOT_0_WIRE.setSCL(DIGIPOT_0_SCL); + digipots[0].begin(MCP4017ADDRESS, DIGIPOT_0_WIRE); + + DIGIPOT_1_WIRE.setSDA(DIGIPOT_1_SDA); + DIGIPOT_1_WIRE.setSCL(DIGIPOT_1_SCL); + digipots[1].begin(MCP4017ADDRESS, DIGIPOT_1_WIRE); + + // CAN setup + vCan.begin(); + vCan.setBaudRate(CAN_BAUDRATE); + vCan.enableFIFO(); + + mCan.begin(); + mCan.setBaudRate(CAN_BAUDRATE); + mCan.enableFIFO(); } +//----------------------------------------------------------------------------// +// Error handling ------------------------------------------------------------// +void send_error(uint8_t command) { + SERIAL_CON.write(SerialCommand::ERROR); + SERIAL_CON.write(command); +} +//----------------------------------------------------------------------------// +// Loop ----------------------------------------------------------------------// void loop() { - if (data_ready) { - data_ready = false; - data_index = 0; - - GpioCommand command = (GpioCommand) data[0]; - - switch (command) { - case GpioCommand::READ_ADC: { - int pin = data[1]; - // if (pin <= ANALOG_PIN_COUNT) - if (1) { - int val = analogRead(pin); - SERIAL.write((val >> 8) & 0xFF); - SERIAL.write(val & 0xFF); - } else { - error("ADC PIN COUNT EXCEEDED"); - } - break; - } - case GpioCommand::READ_GPIO: { - int pin = data[1]; - #ifdef DAC - if (pin >= 200 && pin < 200 + NUM_DACS) { - dacs[pin - 200].setMode(MCP4725_POWER_DOWN_500KRES); - dac_power_down[pin - 200] = 1; - SERIAL.write(0x01); - } else - #endif - { - pinMode(pin, INPUT); - int val = digitalRead(pin); - SERIAL.write(val & 0xFF); - } - break; - } - case GpioCommand::WRITE_DAC: { - int pin = data[1]; - int value = (data[2] << 8) | data[3]; - #ifdef DAC - if (pin >= 200 && pin < 200 + NUM_DACS) { - if (dac_power_down[pin-200]) { - dacs[pin-200].setMode(MCP4725_NORMAL_MODE); - dac_power_down[pin - 200] = 0; - } - dacs[pin - 200].outputVoltage(value); - } - #endif - #ifdef STM32 - // 4 and 5 have DAC on f407 - pinMode(pin, OUTPUT); - analogWrite(pin, value & 0xFF); // max val 255 - #endif - - break; - } - case GpioCommand::WRITE_GPIO: { - int pin = data[1]; - int value = data[2]; - pinMode(pin, OUTPUT); - digitalWrite(pin, value); - break; - } - case GpioCommand::READ_ID: { - SERIAL.write(TESTER_ID); - break; - } - case GpioCommand::WRITE_POT: { - int pin = data[1]; - int value = data[2]; - #ifdef DIGIPOT_EN - if (pin == 1) { - digipot1.setTap((uint8_t) value); - } else if (pin == 2) { - digipot2.setTap((uint8_t) value); - } else - #endif - { - error("POT PIN COUNT EXCEEDED"); - } - break; - } - } - } else { - if (SERIAL.available() > 0) { - data[data_index] = SERIAL.read(); - data_index++; - - uint8_t command = data[0]; - if (data_index == TO_READ[command]) { - data_ready = true; - } - } - } + if (g_data_ready) { + g_data_ready = false; + g_data_idx = 0; + + SerialCommand command = (SerialCommand) g_serial_data[0]; + + switch (command) { + case SerialCommand::READ_ID: { + SERIAL_CON.write(SerialCommand::READ_ID); + SERIAL_CON.write(TESTER_ID); + break; + } + case SerialCommand::WRITE_GPIO: { + uint8_t pin = g_serial_data[1]; + uint8_t value = g_serial_data[2]; + pinMode(pin, OUTPUT); + digitalWrite(pin, value); + break; + } + case SerialCommand::HIZ_GPIO: { + uint8_t pin = g_serial_data[1]; + pinMode(pin, INPUT); + break; + } + case SerialCommand::READ_GPIO: { + uint8_t pin = g_serial_data[1]; + pinMode(pin, INPUT); + int val = digitalRead(pin); + SERIAL_CON.write(SerialCommand::READ_GPIO); + SERIAL_CON.write(val & 0xFF); + break; + } + case SerialCommand::WRITE_DAC: { + uint8_t offset = g_serial_data[1]; + uint8_t value = g_serial_data[2]; + + if (offset >= NUM_DACS) { + send_error(command); + break; + } + + if (dac_power_down[offset]) { + dacs[offset].setMode(MCP4706_AWAKE); + dac_power_down[offset] = false; + } + dacs[offset].setVoltage(value); + break; + } + case SerialCommand::HIZ_DAC: { + uint8_t offset = g_serial_data[1]; + + if (offset >= NUM_DACS) { + send_error(command); + break; + } + + dacs[offset].setMode(MCP4706_PWRDN_500K); + dac_power_down[offset] = true; + break; + } + case SerialCommand::READ_ADC: { + uint8_t pin = g_serial_data[1]; + int val = analogRead(pin); + SERIAL_CON.write(SerialCommand::READ_ADC); + SERIAL_CON.write((val >> 8) & 0xFF); // high + SERIAL_CON.write(val & 0xFF); // low + break; + } + case SerialCommand::WRITE_POT: { + uint8_t offset = g_serial_data[1]; + uint8_t value = g_serial_data[2]; + + if (offset >= NUM_DIGIPOTS) { + send_error(command); + break; + } + + digipots[offset].setSteps(value); + break; + } + case SerialCommand::SEND_CAN: { + uint8_t bus = g_serial_data[1]; + uint16_t signal = (g_serial_data[2] << 8) | g_serial_data[3]; // 11-bit ID + uint8_t length = g_serial_data[4]; + CAN_message_t msg = { 0 }; + msg.id = signal; + msg.len = length; + memcpy(msg.buf, &g_serial_data[5], length); + msg.len = length; + msg.flags.extended = false; + + if (bus == VCAN_BUS) { + vCan.write(msg); + } else if (bus == MCAN_BUS) { + mCan.write(msg); + } else { + send_error(command); + break; + } + break; + } + default: { + send_error(command); + break; + } + } + } else if (SERIAL_CON.available() > 0) { + g_serial_data[g_data_idx] = SERIAL_CON.read(); + g_data_idx++; + + uint8_t command = g_serial_data[0]; + if (g_data_idx == TO_READ[command]) { + g_data_ready = true; + } + } else if (vCan.read(recv_msg)) { + SERIAL_CON.write(RECV_CAN); + SERIAL_CON.write(VCAN_BUS); // bus 1 + SERIAL_CON.write((recv_msg.id >> 8) & 0xFF); // signal high + SERIAL_CON.write(recv_msg.id & 0xFF); // signal low + SERIAL_CON.write(recv_msg.len); // length + SERIAL_CON.write(recv_msg.buf, recv_msg.len); // g_serial_data + } else if (mCan.read(recv_msg)) { + SERIAL_CON.write(RECV_CAN); + SERIAL_CON.write(MCAN_BUS); // bus 2 + SERIAL_CON.write((recv_msg.id >> 8) & 0xFF); // signal high + SERIAL_CON.write(recv_msg.id & 0xFF); // signal low + SERIAL_CON.write(recv_msg.len); // length + SERIAL_CON.write(recv_msg.buf, recv_msg.len); // data + } } +//----------------------------------------------------------------------------// \ No newline at end of file diff --git a/configurations/config_abox_bench.json b/configurations/config_abox_bench.json deleted file mode 100644 index d216025..0000000 --- a/configurations/config_abox_bench.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "./config_schema.json", - "dut_connections":[ - {"board":"a_box", "harness_connections":[ - {"dut":{"connector":"P9","pin":2}, "hil":{"device":"ABoxTester", "port":"AI1"}}, - {"dut":{"connector":"P9","pin":4}, "hil":{"device":"ABoxTester", "port":"AI2"}}, - {"dut":{"connector":"P12","pin":3}, "hil":{"device":"ABoxTester", "port":"AI2"}}, - {"dut":{"connector":"P12","pin":1}, "hil":{"device":"ABoxTester", "port":"AI3"}}, - {"dut":{"connector":"P9","pin":6}, "hil":{"device":"ABoxTester", "port":"AI4"}}, - {"dut":{"connector":"P9","pin":11}, "hil":{"device":"ABoxTester", "port":"RLY1"}}, - {"dut":{"connector":"P9","pin":5}, "hil":{"device":"ABoxTester", "port":"RLY2"}}, - {"dut":{"connector":"P7","pin":6}, "hil":{"device":"ABoxTester", "port":"RLY3"}}, - {"dut":{"connector":"P9","pin":3}, "hil":{"device":"ABoxTester", "port":"DI3"}}, - {"dut":{"connector":"P9","pin":7}, "hil":{"device":"ABoxTester", "port":"DI4"}}, - {"dut":{"connector":"P13","pin":3}, "hil":{"device":"ABoxTester", "port":"DI5"}}, - {"dut":{"connector":"P5","pin":3}, "hil":{"device":"ABoxTester", "port":"DI6"}}, - {"dut":{"connector":"P12","pin":11}, "hil":{"device":"ABoxTester", "port":"DAC1"}}, - {"dut":{"connector":"P4","pin":2}, "hil":{"device":"ArduinoUno", "port":"D2"}}, - {"dut":{"connector":"P4","pin":3}, "hil":{"device":"ArduinoUno", "port":"D3"}}, - {"dut":{"connector":"P4","pin":4}, "hil":{"device":"ArduinoUno", "port":"D4"}}, - {"dut":{"connector":"P4","pin":5}, "hil":{"device":"ArduinoUno", "port":"D5"}}, - {"dut":{"connector":"P4","pin":7}, "hil":{"device":"ArduinoUno", "port":"D6"}}, - {"dut":{"connector":"P3","pin":7}, "hil":{"device":"ArduinoUno", "port":"D7"}}, - {"dut":{"connector":"P2","pin":7}, "hil":{"device":"ArduinoUno", "port":"D8"}}, - {"dut":{"connector":"P1","pin":7}, "hil":{"device":"ArduinoUno", "port":"D9"}}, - {"dut":{"connector":"P10","pin":3}, "hil":{"device":"ABoxTester", "port":"RLY4"}}, - {"dut":{"connector":"P9","pin":8}, "hil":{"device":"ABoxTester", "port":"DI7"}} - ]} - ], - "hil_devices":[ - {"name":"ABoxTester", "type":"test_pcb", "id":2}, - {"name":"ArduinoUno", "type":"arduino_uno", "id":3} - ] -} diff --git a/configurations/config_charger.json b/configurations/config_charger.json deleted file mode 100644 index 0cb9f3f..0000000 --- a/configurations/config_charger.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "./config_schema.json", - "dut_connections":[ - {"board":"Charger", "harness_connections":[ - {"dut":{"connector":"P301","pin":8}, "hil":{"device":"RearTester", "port":"AI2"}}, - {"dut":{"connector":"P301","pin":6}, "hil":{"device":"RearTester", "port":"AI1"}}, - {"dut":{"connector":"P301","pin":9}, "hil":{"device":"RearTester", "port":"DI3"}} - ]} - ], - "hil_devices":[ - {"name":"RearTester", "type":"test_pcb", "id":2}, - {"name":"RearTesterArd", "type":"test_pcb", "id":4} - ] -} diff --git a/configurations/config_collector_bench.json b/configurations/config_collector_bench.json deleted file mode 100644 index 9a67bf7..0000000 --- a/configurations/config_collector_bench.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "./config_schema.json", - "dut_connections":[ - {"board":"Collector", "harness_connections":[ - {"dut":{"connector":"J4","pin":1}, "hil":{"device":"CollTester", "port":"D2"}}, - {"dut":{"connector":"J4","pin":2}, "hil":{"device":"CollTester", "port":"D3"}}, - {"dut":{"connector":"J4","pin":4}, "hil":{"device":"CollTester", "port":"D4"}}, - {"dut":{"connector":"J4","pin":5}, "hil":{"device":"CollTester", "port":"A0"}}, - {"dut":{"connector":"J4","pin":7}, "hil":{"device":"CollTester", "port":"D5"}} - ]} - ], - "hil_devices":[ - {"name":"CollTester", "type":"arduino_uno", "id":5} - ] -} diff --git a/configurations/config_dash.json b/configurations/config_dash.json deleted file mode 100644 index c923c55..0000000 --- a/configurations/config_dash.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "./config_schema.json", - "dut_connections":[ - {"board":"Dashboard", "harness_connections":[ - {"dut":{"connector":"J1","pin":9}, "hil":{"device":"FrontTester", "port":"DAC1"}}, - {"dut":{"connector":"J1","pin":10}, "hil":{"device":"FrontTester", "port":"DAC2"}}, - {"dut":{"connector":"J1","pin":11}, "hil":{"device":"FrontTester", "port":"POT1"}}, - {"dut":{"connector":"J1","pin":12}, "hil":{"device":"FrontTester", "port":"POT2"}} - ]} - ], - "hil_devices":[ - {"name":"FrontTester", "type":"test_pcb", "id":1} - ] -} diff --git a/configurations/config_main_base_bench.json b/configurations/config_main_base_bench.json deleted file mode 100644 index b550d09..0000000 --- a/configurations/config_main_base_bench.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "./config_schema.json", - "dut_connections":[ - {"board":"Main_Module", "harness_connections":[ - {"dut":{"connector":"J3","pin":7}, "hil":{"device":"Arduino", "port":"DAC1"}}, - {"dut":{"connector":"J3","pin":18}, "hil":{"device":"Arduino", "port":"DAC2"}}, - {"dut":{"connector":"J3","pin":5}, "hil":{"device":"Arduino", "port":"AI1"}}, - {"dut":{"connector":"J3","pin":6}, "hil":{"device":"Arduino", "port":"AI2"}}, - {"dut":{"connector":"J3","pin":10}, "hil":{"device":"Arduino", "port":"AI3"}}, - {"dut":{"connector":"J3","pin":9}, "hil":{"device":"Arduino", "port":"AI4"}}, - {"dut":{"connector":"J3","pin":2}, "hil":{"device":"Arduino", "port":"DI7"}}, - {"dut":{"connector":"J4","pin":15}, "hil":{"device":"Arduino2", "port":"DAC1"}}, - {"dut":{"connector":"J4","pin":8}, "hil":{"device":"Arduino2", "port":"DI5"}}, - {"dut":{"connector":"J4","pin":7}, "hil":{"device":"Arduino2", "port":"DI6"}}, - {"dut":{"connector":"J4","pin":21}, "hil":{"device":"Arduino", "port":"DI5"}}, - {"dut":{"connector":"J4","pin":22}, "hil":{"device":"Arduino", "port":"DI6"}}, - {"dut":{"connector":"J3","pin":8}, "hil":{"device":"Arduino2", "port":"DI3"}} - ]} - ], - "hil_devices":[ - {"name":"Arduino", "type":"test_pcb", "id":1}, - {"name":"Arduino2", "type":"test_pcb", "id":2} - ] -} diff --git a/configurations/config_main_sdc_bench.json b/configurations/config_main_sdc_bench.json deleted file mode 100644 index 21209a0..0000000 --- a/configurations/config_main_sdc_bench.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "./config_schema.json", - "dut_connections":[ - {"board":"MainSDC", "harness_connections":[ - {"dut":{"connector":"J1","pin":1}, "hil":{"device":"Arduino", "port":"D5"}}, - {"dut":{"connector":"J1","pin":2}, "hil":{"device":"Arduino", "port":"D4"}}, - {"dut":{"connector":"J1","pin":3}, "hil":{"device":"STM32", "port":"PA4"}}, - {"dut":{"connector":"J2","pin":2}, "hil":{"device":"Arduino", "port":"D3"}}, - {"dut":{"connector":"J2","pin":5}, "hil":{"device":"Arduino", "port":"D6"}} - ]} - ], - "hil_devices":[ - {"name":"Arduino", "type":"arduino_uno", "id":5}, - {"name":"STM32", "type":"disco_f4", "id":6} - ] -} diff --git a/configurations/config_schema.json b/configurations/config_schema.json deleted file mode 100644 index 42871c7..0000000 --- a/configurations/config_schema.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", - "required":["busses"], - "properties": { - "busses":{ - "type":"array", - "items":{"$ref":"#/$defs/bus"} - } - }, - "$defs":{ - "bus":{ - "type":"object", - "required":["bus_name", "nodes"], - "properties":{ - "bus_name":{ - "type":"string", - "description":"The name of the bus" - }, - "nodes":{ - "type":"array", - "description":"The nodes (MCUs) on the bus", - "items":{"$ref":"#/$defs/node"} - } - } - }, - "node":{ - "type":"object", - "required":["node_name", "node_ssa","tx", "rx"], - "properties":{ - "node_name":{ - "type":"string", - "description":"name of the node (MCU)" - }, - "can_peripheral":{ - "type":"string", - "enum": ["CAN1", "CAN2"] - }, - "node_ssa":{ - "type":"integer", - "description":"subsystem address, lower = higher priority, 0-63", - "minimum": 0, - "maximum": 63 - }, - "tx":{ - "type":"array", - "description":"messages transmitted by the node", - "items":{"$ref":"#/$defs/tx_message"} - }, - "rx":{ - "type":"array", - "description":"messages received by the node", - "items":{"$ref":"#/$defs/rx_message"} - }, - "accept_all_messages":{ - "type":"boolean", - "description":"Do not generate CAN RX filters and accept all messages. Only those messages configured in the RX field will be populated in the can_data structure." - } - - } - }, - "tx_message":{ - "type":"object", - "required":["msg_name", "msg_desc", "signals", "msg_period"], - "properties":{ - "msg_name":{"type":"string"}, - "msg_desc":{"type":"string"}, - "signals":{ - "type":"array", - "description":"variables within message", - "items":{"$ref":"#/$defs/signal"} - }, - "msg_period":{ - "type":"integer", - "description":"ms, leave 0 to disable stale checking" - }, - "msg_hlp":{ - "type":"integer", - "description":"message high level priority value 0-5, 0 being highest\n0 - System Critical Faults\n1 - System Critical Data\n2 - Non-critical Faults\n3 - High Priority Data\n4 - Low Priority Data\n5 - Data Acquisition Data", - "minimum":0, - "maximum":5 - }, - "msg_pgn":{ - "type":"integer", - "description":"parameter group number 0-1,048,575\nDescribes the data the message contains, not an indication of where data is coming and going.", - "minimum":0, - "maximum":1048575 - }, - "msg_id_override":{ - "type":"string", - "description":"sets the message to a specific id, ignoring the node's ssa\nTo use hex do \"0x########\"" - } - } - }, - "signal":{ - "type":"object", - "required":["sig_name","type"], - "properties":{ - "sig_name":{"type":"string"}, - "sig_desc":{"type":"string"}, - "type":{ - "type":"string", - "enum":["uint8_t", "uint16_t", "uint32_t", "uint64_t", - "int8_t", "int16_t", "int32_t", "int64_t", - "float"], - "description":"can only change length of uints, float defaults to 32 bits" - }, - "length":{ - "type":"integer", - "description":"length in bits, only valid if unsigned data type", - "minimum":1, - "maimum":64 - }, - "scale":{"type":"number", "description":"scale factor to apply, default of 1\nDecoding results in (x * scale)+offset"}, - "offset":{"type":"number", "description": "offset to apply, default of 0\nDecoding results in (x * scale)+offset"}, - "maximum":{"type":"number", "description": "maximum value of signal, defaults to none"}, - "minimum":{"type":"number", "description": "minimum value of signal, defaults to none"}, - "unit":{"type":"string", "description":"unit of the signal, defaults to none"}, - "choices":{"type":"array", "description":"enumeration values", "items":{"type":"string"}} - } - }, - "rx_message":{ - "type":"object", - "required":["msg_name"], - "properties":{ - "msg_name":{"type":"string"}, - "callback":{"type":"boolean"}, - "irq":{"type":"boolean"}, - "arg_type":{"type":"string", "enum":["msg_data", "header"]} - } - } - } - -} diff --git a/configurations/config_system_hil_attached.json b/configurations/config_system_hil_attached.json deleted file mode 100644 index a32921f..0000000 --- a/configurations/config_system_hil_attached.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "./config_schema.json", - "dut_connections":[ - {"board":"Main_Module", "harness_connections":[ - {"dut":{"connector":"J3","pin":1}, "hil":{"device":"RearTester", "port":"DI3"}}, - {"dut":{"connector":"J3","pin":2}, "hil":{"device":"RearTester", "port":"DI4"}}, - {"dut":{"connector":"J3","pin":5}, "hil":{"device":"RearTester", "port":"AI1"}}, - {"dut":{"connector":"J3","pin":6}, "hil":{"device":"RearTester", "port":"AI2"}}, - {"dut":{"connector":"J3","pin":7}, "hil":{"device":"RearTester", "port":"DAC1"}}, - {"dut":{"connector":"J3","pin":16}, "hil":{"device":"RearTester", "port":"DI5"}}, - {"dut":{"connector":"J3","pin":18}, "hil":{"device":"RearTester", "port":"DAC2"}}, - {"dut":{"connector":"J4","pin":1}, "hil":{"device":"RearTester", "port":"RLY3"}}, - {"dut":{"connector":"J4","pin":15}, "hil":{"device":"RearTesterArd", "port":"DAC2"}} - ]}, - {"board":"Dashboard", "harness_connections":[ - {"dut":{"connector":"J1","pin":9}, "hil":{"device":"FrontTester", "port":"DAC1"}}, - {"dut":{"connector":"J1","pin":10}, "hil":{"device":"FrontTester", "port":"DAC2"}}, - {"dut":{"connector":"J1","pin":11}, "hil":{"device":"FrontTester", "port":"POT1"}}, - {"dut":{"connector":"J1","pin":12}, "hil":{"device":"FrontTester", "port":"POT2"}} - ]} - ], - "hil_devices":[ - {"name":"FrontTester", "type":"test_pcb", "id":1}, - {"name":"RearTester", "type":"test_pcb", "id":2}, - {"name":"RearTesterArd", "type":"test_uno", "id":4} - ] -} diff --git a/configurations/config_testing.json b/configurations/config_testing.json deleted file mode 100644 index 15627a8..0000000 --- a/configurations/config_testing.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "./config_schema.json", - "dut_connections":[ - {"board":"Test", "harness_connections":[ - ]} - ], - "hil_devices":[ - {"name":"Test_HIL", "type":"test_pcb", "id":2} - ] -} diff --git a/device_configs/teensy_breadboard.json b/device_configs/teensy_breadboard.json new file mode 100644 index 0000000..d49a2fc --- /dev/null +++ b/device_configs/teensy_breadboard.json @@ -0,0 +1,55 @@ +{ + "ports": [ + { "port":0, "name":"DO@0", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":1, "name":"DO@1", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":2, "name":"DO@2", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":3, "name":"DO@3", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":4, "name":"DO@4", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":5, "name":"DO@5", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":6, "name":"DO@6", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":7, "name":"D0@7", "mode": "D0", "notes":"3.3v Digital Output" }, + { "port":8, "name":"D0@8", "mode": "D0", "notes":"3.3v Digital Output" }, + { "port":9, "name":"D0@9", "mode": "D0", "notes":"3.3v Digital Output" }, + { "port":10, "name":"D0@10", "mode": "D0", "notes":"3.3v Digital Output" }, + { "port":11, "name":"D0@11", "mode": "D0", "notes":"3.3v Digital Output" }, + { "port":12, "name":"D0@12", "mode": "D0", "notes":"3.3v Digital Output" }, + + { "port":14, "name":"AI@14", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":15, "name":"AI@15", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":16, "name":"AI@16", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":17, "name":"AI@17", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":18, "name":"AI@18", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":19, "name":"AI@19", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":20, "name":"AI@20", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":21, "name":"AI@21", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":22, "name":"AI@22", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":23, "name":"AI@23", "mode": "AI", "notes":"3.3v Analog Input" }, + + { "port":24, "name":"AI@24", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":25, "name":"AI@25", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":26, "name":"AI@26", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":27, "name":"AI@27", "mode": "AI", "notes":"3.3v Analog Input" }, + + { "port":38, "name":"AI@38", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":39, "name":"AI@39", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":40, "name":"AI@40", "mode": "AI", "notes":"3.3v Analog Input" }, + { "port":41, "name":"AI@41", "mode": "AI", "notes":"3.3v Analog Input" }, + + { "port":28, "name":"DI@28", "mode": "DI", "notes":"3.3v Digital Input" }, + { "port":29, "name":"DI@29", "mode": "DI", "notes":"3.3v Digital Input" }, + { "port":30, "name":"DI@30", "mode": "DI", "notes":"3.3v Digital Input" }, + { "port":31, "name":"DI@31", "mode": "DI", "notes":"3.3v Digital Input" }, + { "port":32, "name":"DI@32", "mode": "DI", "notes":"3.3v Digital Input" }, + + { "port":33, "name":"DI@33", "mode": "DI", "notes":"3.3v Digital Input" }, + { "port":34, "name":"DI@34", "mode": "DI", "notes":"3.3v Digital Input" }, + { "port":35, "name":"DI@35", "mode": "DI", "notes":"3.3v Digital Input" }, + { "port":36, "name":"DI@36", "mode": "DI", "notes":"3.3v Digital Input" }, + { "port":37, "name":"DI@37", "mode": "DI", "notes":"3.3v Digital Input" } + ], + + "adc_config": { + "bit_resolution": 10, + "adc_reference_v": 3.3 + } +} \ No newline at end of file diff --git a/device_configs/teensy_pcb.json b/device_configs/teensy_pcb.json new file mode 100644 index 0000000..287a2ca --- /dev/null +++ b/device_configs/teensy_pcb.json @@ -0,0 +1,66 @@ +{ + "mux_note": "Use ports like: [mux_name]_#, where # starts from 0", + "muxs": [ + { "name": "24vMUX", "mode": "AI24", "select_ports": [37, 36, 35, 34], "port": 38 }, + { "name": "5vMUX", "mode": "AI5", "select_ports": [27, 28, 29], "port": 26 }, + { "name": "DMUX", "mode": "DI", "select_ports": [2, 3, 4, 5], "port": 6 } + ], + + "ports": [ + { "port":20, "name":"RLY1", "mode": "DO", "notes":"Relay" }, + { "port":21, "name":"RLY2", "mode": "DO", "notes":"Relay" }, + { "port":22, "name":"RLY3", "mode": "DO", "notes":"Relay" }, + { "port":23, "name":"RLY4", "mode": "DO", "notes":"Relay" }, + + { "port":32, "name":"24vSW1", "mode": "DO", "notes":"24V switch" }, + { "port":33, "name":"24vSW2", "mode": "DO", "notes":"24V switch" }, + + { "port":15, "name":"DAI1", "mode": "AI24", "notes":"Direct 24v Scaled Analog Input" }, + { "port":14, "name":"DAI2", "mode": "AI5", "notes":"Direct 5v Scaled Analog Input" }, + + { "port":7, "name":"DO1", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":8, "name":"DO2", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":9, "name":"DO3", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":10, "name":"DO4", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":11, "name":"DO5", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":12, "name":"DO6", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":13, "name":"DO7", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":41, "name":"DO8", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":40, "name":"DO9", "mode": "DO", "notes":"3.3v Digital Output" }, + { "port":39, "name":"DO10", "mode": "DO", "notes":"3.3v Digital Output" }, + + { "port":0, "name":"DAC1", "mode": "AO", "notes":"5V 8-bit DAC" }, + { "port":1, "name":"DAC2", "mode": "AO", "notes":"5V 8-bit DAC" }, + { "port":2, "name":"DAC3", "mode": "AO", "notes":"5V 8-bit DAC" }, + { "port":3, "name":"DAC4", "mode": "AO", "notes":"5V 8-bit DAC" }, + { "port":4, "name":"DAC5", "mode": "AO", "notes":"5V 8-bit DAC" }, + { "port":5, "name":"DAC6", "mode": "AO", "notes":"5V 8-bit DAC" }, + { "port":6, "name":"DAC7", "mode": "AO", "notes":"5V 8-bit DAC" }, + { "port":7, "name":"DAC8", "mode": "AO", "notes":"5V 8-bit DAC" }, + + { "port":0, "name":"POT1", "mode": "POT", "notes":"10k Ω 7-bit Digipot" }, + { "port":1, "name":"POT2", "mode": "POT", "notes":"10k Ω 7-bit Digipot" } + ], + + "can": [ + { "bus":1, "name":"VCAN", "notes":"Vechicle CAN Bus" }, + { "bus":2, "name":"MCAN", "notes":"Motor CAN Bus" } + ], + + "adc_config": { + "bit_resolution": 10, + "adc_reference_v": 3.3, + "5v_reference_v": 1.5625, + "24v_reference_v": 2.18181818182, + "notes": "Xv reference is the voltage the teensy will see when the input is at X volts" + }, + "dac_config": { + "bit_resolution": 8, + "reference_v": 5.0 + }, + "pot_config": { + "bit_resolution": 7, + "reference_ohms": 10000, + "wiper_ohms": 20 + } +} \ No newline at end of file diff --git a/hil/communication/can_bus.py b/hil/communication/can_bus.py deleted file mode 100644 index fefa1f7..0000000 --- a/hil/communication/can_bus.py +++ /dev/null @@ -1,444 +0,0 @@ -from __future__ import annotations -from collections.abc import Callable - -from datetime import datetime -import can -import can.interfaces.gs_usb -import gs_usb -import socket -import usb -import cantools -from hil.communication.client import TCPBus, UDPBus -import hil.utils as utils -import time -import threading -import numpy as np - -CAN_READ_TIMEOUT_S = 1.0 - - -# ---------------------------------------------------------------------------- # -class CanBus(threading.Thread): - """ - Handles sending and receiving can bus messages, - tracks all degined signals (BusSignal) - """ - - def __init__(self, dbc_path: str, default_ip: str, can_config: dict): - super(CanBus, self).__init__() - self.db: cantools.database.can.database.Database = cantools.db.load_file(dbc_path) - - utils.log(f"CAN version: {can.__version__}") - utils.log(f"gs_usb version: {gs_usb.__version__}") - - self.connected: bool = False - self.bus: can.ThreadSafeBus | UDPBus = None - self.start_time_bus: float = -1 - self.start_date_time_str: str = "" - self.tcp: bool = False - self.tcpbus: TCPBus = None - - self.handle_daq_msg: Callable[[can.Message], None] = None - - # Bus Load Estimation - self.total_bits: int = 0 - self.last_estimate_time: float = 0 - - # Load Bus Signals - self.can_config: dict = can_config - self.updateSignals(self.can_config) - - self.is_importing: bool = False - - #self.port = 8080 - #self.ip = "10.42.0.1" - self.port: int = 5005 - self.ip: str = default_ip - - self.password: str | None = None - self.is_wireless: bool = False - - - def connect(self) -> None: - """ Connects to the bus """ - utils.log("Trying usb") - # Attempt usb connection first - dev = usb.core.find(idVendor=0x1D50, idProduct=0x606F) - if dev: - channel = dev.product - bus_num = dev.bus - addr = dev.address - del(dev) - self.bus = can.ThreadSafeBus(bustype="gs_usb", channel=channel, bus=bus_num, address=addr, bitrate=500000) - # Empty buffer of old messages - while(self.bus.recv(0)): pass - self.connected = True - self.is_wireless = False - # self.connect_sig.emit(self.connected) - utils.log("Usb successful") - return - - #USB Failed, trying UDP - utils.log("Trying UDP") - try: - self.bus = UDPBus(self.ip, self.port) - self.connected = True - self.is_wireless = True - # Empty buffer of old messages - time.sleep(3) - i=0 - while(self.bus.recv(0)): - i+=1 - utils.log(f"cleared {i} from buffer") - utils.log_warning("This does not gurantee a connection. Please make sure Raspberry Pi is broadcasting.") - # self.connect_sig.emit(self.connected) - self.connect_tcp() - return - except OSError as e: - utils.log(f"UDP connect error {e}") - - # # Usb failed, trying tcp - # utils.log("Trying tcp") - # try: - # self.bus = TCPBus(self.ip, self.port) - # self.connected = True - # self.is_wireless = True - # # Empty buffer of old messages - # time.sleep(3) - # i=0 - # while(self.bus.recv(0)): - # i+=1 - # utils.log(f"cleared {i} from buffer") - # utils.log("Tcp successful") - # self.connect_sig.emit(self.connected) - # return - # except OSError as e: - # utils.log(f"tcp connect error {e}") - - #Both Connections Failed - self.connected = False - utils.log_error("Failed to connect to a bus") - # self.connect_sig.emit(self.connected) - self.connectError() - - def connect_tcp(self) -> None: - # Usb failed, trying tcp - utils.log("Trying tcp") - self.connected_disp = 1 - # self.write_sig.emit(self.connected_disp) - try: - self.tcpbus = TCPBus(self.ip, self.port) - self.connected_tcp = True - # Empty buffer of old messages - # time.sleep(3) - i=0 - while(self.tcpbus.recv(0)): - i+=1 - utils.log(f"cleared {i} from buffer") - utils.log("Tcp successful") - # self.password = PasswordDialog.promptPassword(self.password) - # print(self.password) - # result = True - # if self.password == "": - # result = False - # elif self.password == None: - # self.tcpbus.shutdown(0) - # result = True - # else: - # result = self.tcpbus.handshake(self.password) - # while not result: - # self.password = None - # self.password = PasswordDialog.setText(self.password) - # if self.password == "": - # result = False - # elif self.password == None: - # self.tcpbus.shutdown(0) - # result = True - # else: - # result = self.tcpbus.handshake(self.password) - # # self.connect_sig.emit(self.connected) - self.connected_disp = 2 - # self.write_sig.emit(self.connected_disp) - self.tcp = True - return - except socket.timeout as e: - self.connected_disp = 0 - # self.write_sig.emit(self.connected_disp) - utils.log_error(e) - BindError.bindError() - self.tcpbus.close() - return - except Exception as e: - utils.log(f"Unknown Error: {e}") - # except OSError as e: - # utils.log(f"tcp connect error {e}") - - #TCP Connection is in Bind State - # self.connected_disp = False - utils.log_error("Failed to connect to the TCP") - # self.connect_sig.emit(self.connected_disp) - # BindError.bindError() - self.connectError() - self.connected_disp = 0 - # self.write_sig.emit(self.connected_disp) - - def disconnect_bus(self) -> None: - self.connected = False - # self.connect_sig.emit(self.connected) - if self.tcpbus: - self.disconnect_tcp() - if self.bus: - self.bus.shutdown() - if not self.is_wireless: usb.util.dispose_resources(self.bus.gs_usb.gs_usb) - del(self.bus) - self.bus = None - - def disconnect_tcp(self) -> None: - self.connected_disp = 0 - # self.write_sig.emit(self.connected_disp) - if self.tcpbus: - self.tcpbus.shutdown(1) - del(self.tcpbus) - self.tcpbus = None - - - def reconnect(self) -> None: - """ destroy usb connection, attempt to reconnect """ - self.connected = False - # while(not self.isFinished()): - # # wait for bus receive to finish - # pass - self.join() - self.disconnect_bus() - time.sleep(1.5) - self.connect() - utils.clearDictItems(utils.signals) - self.start_time_bus = -1 - self.start_time_cmp = 0 - self.start_date_time_str = datetime.now().strftime("%m-%d-%Y %H:%M:%S") - self.start() - - def sendLogCmd(self, option: bool) -> None: - """Send the start logging function""" - if option == True: - self.tcpbus.start_logging() - else: - self.tcpbus.stop_logging() - - def sendFormatMsg(self, msg_name: str, msg_data: dict) -> None: - """ Sends a message using a dictionary of its data """ - dbc_msg = self.db.get_message_by_name(msg_name) - data = dbc_msg.encode(msg_data) - msg = can.Message(arbitration_id=dbc_msg.frame_id, data=data, is_extended_id=True) - if not self.is_wireless: - self.bus.send(msg) - if self.tcp: - self.tcpbus.send(msg) - - def sendMsg(self, msg: can.Message) -> None: - """ Sends a can message over the bus """ - if self.connected: - if not self.is_wireless: - # print(f"sending {msg}") - try: - self.bus.send(msg, timeout=2) - except can.CanOperationError: - utils.log_error(f"Failed to send {msg}") - elif self.tcp: - self.tcpbus.send(msg) - else: - utils.log_error("Tried to send msg without connection") - - def onMessageReceived(self, msg: can.Message) -> None: - """ Emits new message signal and updates the corresponding signals """ - if self.start_time_bus == -1: - self.start_time_bus = msg.timestamp - self.start_time_cmp = time.time() - self.start_date_time_str = datetime.now().strftime("%m-%d-%Y %H:%M:%S") - utils.log_warning(f"Start time changed: {msg.timestamp}") - if msg.timestamp - self.start_time_bus < 0: - utils.log_warning("Out of order") - msg.timestamp -= self.start_time_bus - # self.new_msg_sig.emit(msg) # TODO: only emit signal if DAQ msg for daq_protocol, currently receives all msgs (low priority performance improvement) - # if (msg.arbitration_id & 0x3F == 60): self.bl_msg_sig.emit(msg) # emit for bootloader - if ((msg.arbitration_id >> 6) & 0xFFFFF == 0xFFFFF) and self.handle_daq_msg: self.handle_daq_msg(msg) # emit for daq - if not msg.is_error_frame: - dbc_msg = None - try: - dbc_msg = self.db.get_message_by_frame_id(msg.arbitration_id) - decode = dbc_msg.decode(msg.data) - for sig in decode.keys(): - sig_val = decode[sig] - if (type(sig_val) != str): - utils.signals[utils.b_str][dbc_msg.senders[0]][dbc_msg.name][sig].update(sig_val, msg.timestamp)#, not utils.logging_paused or self.is_importing) - except KeyError: - if dbc_msg and "daq" not in dbc_msg.name and "fault" not in dbc_msg.name: - if utils.debug_mode: utils.log_warning(f"Unrecognized signal key for {msg}") - # elif "fault" not in dbc_msg.name: - # if utils.debug_mode: utils.log_warning(f"unrecognized: {msg.arbitration_id}") - except ValueError as e: - if "daq" not in dbc_msg.name: - pass - #if utils.debug_mode: utils.log_warning(f"Failed to convert msg: {msg}") - #print(e) - # if (msg.is_error_frame): - # utils.log(msg) - - # bus load estimation - msg_bit_length_max = 64 + msg.dlc * 8 + 18 - self.total_bits += msg_bit_length_max - - def connectError(self) -> None: - """ Creates message box prompting to try to reconnect """ - # self.ip = ConnectionErrorDialog.connectionError(self.ip) - utils.log_error("Ip wrong") - # if self.ip: - # self.connect_tcp() - - - def updateSignals(self, can_config: dict) -> None: - """ Creates dictionary of BusSignals of all signals in can_config """ - utils.signals.clear() - for bus in can_config['busses']: - utils.signals[bus['bus_name']] = {} - for node in bus['nodes']: - utils.signals[bus['bus_name']][node['node_name']] = {} - for msg in node['tx']: - utils.signals[bus['bus_name']][node['node_name']][msg['msg_name']] = {} - for signal in msg['signals']: - utils.signals[bus['bus_name']][node['node_name']] \ - [msg['msg_name']][signal['sig_name']]\ - = BusSignal.fromCANMsg(signal, msg, node, bus) - - def run(self) -> None: - """ Thread loop to receive can messages """ - self.last_estimate_time = time.time() - loop_count = 0 - skips = 0 - avg_process_time = 0 - - #while self.connected: - while (not self.is_wireless or self.bus and self.bus._is_connected) and self.connected: - # TODO: detect when not connected (add with catching send error) - # would the connected variable need to be locked? - msg = self.bus.recv(0.25) - if msg: - delta = time.perf_counter() - if not self.is_importing: - self.onMessageReceived(msg) - avg_process_time += time.perf_counter() - delta - else: - skips += 1 - - loop_count += 1 - # Bus load estimation - if (time.time() - self.last_estimate_time) > 1: - self.last_estimate_time = time.time() - bus_load = self.total_bits / 500000.0 * 100 - self.total_bits = 0 - # self.bus_load_sig.emit(bus_load) - # if loop_count != 0 and loop_count-skips != 0 and utils.debug_mode: print(f"rx period (ms): {1/loop_count*1000}, skipped: {skips}, process time (ms): {avg_process_time / (loop_count-skips)*1000}") - loop_count = 0 - avg_process_time = 0 - skips = 0 - #self.connect_sig.emit(self.connected and self.bus.is_connected) - # if (self.connected and self.is_wireless): self.connect_sig.emit(self.bus and self.bus.is_connected) -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -class BusSignal(): - """ Signal that can be subscribed (connected) to for updates """ - - # update_sig = QtCore.pyqtSignal() - # history = 500000#240000 # for 0.015s update period 1 hour of data - # 1 now :) - # data_lock = threading.Lock() - # NOTE: don't need lock for now as long as only one writer - # However, when timestamp and data are read, it is possible an older timestamp is read for newer data - - def __init__( - self, - bus_name: str, - node_name: str, - msg_name: str, - sig_name: str, - dtype: np.dtype, - store_dtype: np.dtype | None = None, - unit: str = "", - msg_desc: str = "", - sig_desc: str = "", - msg_period = 0 - ): - self.bus_name: str = bus_name - self.node_name: str = node_name - self.message_name: str = msg_name - self.signal_name: str = sig_name - self.name: str = '.'.join([self.bus_name, self.node_name, self.message_name, self.signal_name]) - - self.unit: str = unit - self.msg_desc: str = msg_desc - self.sig_desc: str = sig_desc - self.msg_period = msg_period - - self.send_dtype: np.dtype = dtype - if not store_dtype: - self.store_dtype: np.dtype = self.send_dtype - else: - self.store_dtype: np.dtype = store_dtype - - self.data: int = 0 - self.time: float = 0 - self.stale_timestamp: float = time.time() - - @classmethod - def fromCANMsg(cls, sig: dict, msg: dict, node: dict, bus: dict) -> BusSignal: - send_dtype = utils.data_types[sig['type']] - # If there is scaling going on, don't store as an integer on accident - if ('scale' in sig and sig['scale'] != 1) or ('offset' in sig and sig['offset'] != 0): - parse_dtype = utils.data_types['float'] - else: - parse_dtype = send_dtype - return cls(bus['bus_name'], node['node_name'], msg['msg_name'], sig['sig_name'], - send_dtype, store_dtype=parse_dtype, - unit=(sig['unit'] if 'unit' in sig else ""), - msg_desc=(msg['msg_desc'] if 'msg_desc' in msg else ""), - sig_desc=(sig['sig_desc'] if 'sig_desc' in sig else ""), - msg_period=msg['msg_period']) - - def update(self, val: int, timestamp: float) -> None: - """ update the value of the signal """ - self.data = val - self.time = timestamp - self.stale_timestamp = time.time() - - def clear(self) -> None: - """ clears stored signal values """ - self.data = 0 - self.time = 0 - - @property - def curr_val(self) -> int: - """ last value recorded """ - return self.data - - @property - def last_update_time(self) -> float: - """ timestamp of last value recorded """ - return self.time - - @property - def is_stale(self) -> bool: - """ based on last receive time """ - if self.msg_period == 0: return False - else: - return ((time.time() - self.stale_timestamp) * 1000) > self.msg_period * 1.5 - - @property - def state(self) -> int: - start_t = time.time() - while (self.is_stale): - if (time.time() >= start_t + CAN_READ_TIMEOUT_S): - utils.log_warning(f"Timed out reading CAN var {self.signal_name} of msg {self.message_name} of node {self.node_name}") - break - return self.curr_val diff --git a/hil/communication/client.py b/hil/communication/client.py deleted file mode 100644 index b317f80..0000000 --- a/hil/communication/client.py +++ /dev/null @@ -1,376 +0,0 @@ -import socket -from queue import Queue -import queue -from queue import Empty as QueueEmpty -from threading import Thread -import can -from time import sleep -import hil.utils as utils -import datetime - -# modification of: -# https://github.com/teebr/socketsocketcan -# switched raspi to be the server instead of a client - - -# ---------------------------------------------------------------------------- # -class TCPBus(can.BusABC): - - RECV_FRAME_SZ = 29 - CAN_EFF_FLAG = 0x80000000 - CAN_RTR_FLAG = 0x40000000 - CAN_ERR_FLAG = 0x20000000 - - def __init__(self, ip: str, port: int, can_filters: can.typechecking.CanFilters | None = None,**kwargs): - super().__init__("whatever", can_filters) - self.port: int = port - self._is_connected: bool = False - self.recv_buffer: Queue = Queue() # Queue[can.Message] - self.send_buffer: Queue = Queue() # Queue[can.Message] - self._shutdown_flag: bool = False#Queue() - - print(f"IP: {ip}, port: {port}") - - #open socket and wait for connection to establish. - socket.setdefaulttimeout(3) # seconds - utils.log("attempting to connect to tcp") - self._conn: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) - utils.log("Connecting...") - self._conn.connect((ip, port)) - utils.log("connected") - self._is_connected: bool = True - self._conn.settimeout(0.5) #blocking makes exiting an infinite loop hard - - self.start_threads() - - - def start_threads(self) -> bool: - # self._conn.sendall(password.encode()) - # data_raw = self._conn.recv(1) - # data = int.from_bytes(data_raw,"little") - # print(f"recieved: {data}") - # if data == 0x00: - # print("returning false") - # return False - # print("returing true") - #now we're connected, kick off other threads. - self._tcp_listener = Thread(target=self._poll_socket) - self._tcp_listener.start() - - self._tcp_writer = Thread(target=self._poll_send) - self._tcp_writer.start() - - self.send_time_sync() - return True - - def send_time_sync(self) -> None: - # Sends the current time to update RTC - self.send_buffer.put(4) - t = datetime.datetime.now() - v = bytes([0,0,0,0,0,t.second, t.minute, t.hour, - t.day, t.month, (t.year-2000), 0, 0]) - self.send_buffer.put(v) - - def _recv_internal(self,timeout: float | None = None) -> tuple[can.Message | None, bool]: - #TODO: filtering - try: - return (self.recv_buffer.get(timeout=timeout), True) - except queue.Empty: - return None, True - - def send(self, msg: can.Message) -> None: - if msg.is_extended_id: - msg.arbitration_id |= self.CAN_EFF_FLAG - if msg.is_remote_frame: - msg.arbitration_id |= self.CAN_RTR_FLAG - if msg.is_error_frame: - msg.arbitration_id |= self.CAN_ERR_FLAG - self.send_buffer.put(0) - self.send_buffer.put(msg) - - def start_logging(self) -> None: - self.send_buffer.put(1) - self.send_buffer.put(0xFFFFFFFF) - - def stop_logging(self) -> None: - self.send_buffer.put(3) - self.send_buffer.put(0xFFFFFFFF) - - def _stop_threads(self) -> None: - #self._shutdown_flag.put(True) - self._shutdown_flag = True - self._is_connected = False - utils.log_warning("Bus Client Shutdown (TCP)") - - def shutdown(self, handshake: int) -> None: - """gracefully close TCP connection and exit threads""" - #handshake: 0: Currently in handshake mode, 1: In regular send mode - if handshake == 0: - msg = "exit" - self._conn.sendall(msg.encode()) - else: - msg = 4 - self._conn.sendall(msg.to_bytes(1, "little")) - if self.is_connected: - self._stop_threads() - #can't join threads because this might be called from that thread, so just wait... - while handshake == 1 and (self._tcp_listener.is_alive() or self._tcp_writer.is_alive()): - sleep(0.005) - self._conn.close() #shutdown might be faster but can be ugly and raise an exception - - def close(self) -> None: - self._conn.close() - - @property - def is_connected(self) -> bool: - """check that a TCP connection is active""" - return self._is_connected - - def _msg_to_bytes(self, msg: can.Message) -> bytes: - """convert Message object to bytes to be put on TCP socket""" - # print(msg) - arb_id = msg.arbitration_id.to_bytes(4,"little") #TODO: masks - dlc = msg.dlc.to_bytes(1,"little") - data = msg.data + bytes(8-msg.dlc) - # print(arb_id + dlc + data) - return arb_id+dlc+data - - def _bytes_to_message(self, b: bytes) -> can.Message: - """convert raw TCP bytes to can.Message object""" - #ts = int.from_bytes(b[:4],"little") + int.from_bytes(b[4:8],"little")/1e6 - ts = int.from_bytes(b[:8],"little") + int.from_bytes(b[8:16],"little")/1e6 - #print(f"len: {len(b)}, time: {ts}, data: {b}") - #can_id = int.from_bytes(b[8:12],"little") - can_id = int.from_bytes(b[16:20],"little") - dlc = b[20] #TODO: sanity check on these values in case of corrupted messages. - - #decompose ID - is_extended = bool(can_id & self.CAN_EFF_FLAG) #CAN_EFF_FLAG - if is_extended: - arb_id = can_id & 0x1FFFFFFF - else: - arb_id = can_id & 0x000007FF - - return can.Message( - timestamp = ts, - arbitration_id = arb_id, - is_extended_id = is_extended, - is_error_frame = bool(can_id & self.CAN_ERR_FLAG), #CAN_ERR_FLAG - is_remote_frame = bool(can_id & self.CAN_RTR_FLAG), #CAN_RTR_FLAG - dlc=dlc, - #data=b[13:13+dlc] - data=b[21:21+dlc] - ) - - def _poll_socket(self) -> None: - """background thread to check for new CAN messages on the TCP socket""" - part_formed_message = bytearray() # TCP transfer might off part way through sending a message - #with self._conn as conn: - conn = self._conn - while not self._shutdown_flag: #self._shutdown_flag.empty(): - try: - data = conn.recv(self.RECV_FRAME_SZ * 20) - except socket.timeout: - #no data, just try again. - continue - except OSError as e: - # socket's been closed. - utils.log_error(f"ERROR: connection closed (1): {e}") - self._stop_threads() - break - - if len(data): - # process the 1 or more messages we just received - - if len(part_formed_message): - data = part_formed_message + data #add on the previous remainder - - #check how many whole and incomplete messages we got through. - num_incomplete_bytes = len(data) % self.RECV_FRAME_SZ - num_frames = len(data) // self.RECV_FRAME_SZ - - #to pre-pend next time: - if num_incomplete_bytes: - part_formed_message = data[-num_incomplete_bytes:] - else: - part_formed_message = bytearray() - - c = 0 - for _ in range(num_frames): - self.recv_buffer.put(self._bytes_to_message(data[c:c+self.RECV_FRAME_SZ])) - c += self.RECV_FRAME_SZ - else: - #socket's been closed at the other end. - utils.log_error(f"ERROR: connection closed (2)") - self._stop_threads() - break - # utils.log("Exited poll socket") - - def _poll_send(self) -> None: - """background thread to send messages when they are put in the queue""" - #with self._conn as s: - s = self._conn - while not self._shutdown_flag: #self._shutdown_flag.empty(): - try: - cmd = self.send_buffer.get(timeout=0.002) - msg = self.send_buffer.get(timeout=0.002) - data = cmd.to_bytes(1,"little") - if (cmd == 0): - data += self._msg_to_bytes(msg) - elif (cmd == 4): - data += msg - else: - data += bytearray(13) - #data += msg.to_bytes(4, "little") - while not self.send_buffer.empty(): #we know there's one message, might be more. - data += self.send_buffer.get().to_bytes(1,"little") - data += self._msg_to_bytes(self.send_buffer.get()) - try: - s.sendall(data) - #print(f"sent {data}") - except OSError as e: - # socket's been closed. - utils.log_error(f"ERROR: connection closed (3): {e}") - self._stop_threads() - break - except QueueEmpty: - pass #NBD, just means nothing to send. - # utils.log("Exited poll send") -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -class UDPBus(can.BusABC): - #RECV_FRAME_SZ = 29 - RECV_FRAME_SZ = 18 - CAN_EFF_FLAG = 0x80000000 - CAN_RTR_FLAG = 0x40000000 - CAN_ERR_FLAG = 0x20000000 - - def __init__(self, ip: str, port: int, can_filters: can.typechecking.CanFilters | None = None, **kwargs): - super().__init__("whatever",can_filters) - self.port: int = port - self._is_connected: bool = False - self.recv_buffer: Queue = Queue() - self._shutdown_flag: bool = False#Queue() - - #open socket and wait for connection to establish. - socket.setdefaulttimeout(3) # seconds - utils.log("attempting to connect to udp") - self._conn: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - - self._conn.bind(("", 5005)) - utils.log("Listening to UDP Port") - self._is_connected: bool = True - self._conn.settimeout(0.5) #blocking makes exiting an infinite loop hard - - #now we're connected, kick off other threads. - self._udp_listener: Thread = Thread(target=self._poll_udp_socket) - self._udp_listener.start() - - def _bytes_to_message(self, b: bytes) -> can.Message: - """convert raw TCP bytes to can.Message object""" - #ts = int.from_bytes(b[:4],"little") + int.from_bytes(b[4:8],"little")/ e6 - # ts = int.from_bytes(b[:8],"little") + int.from_bytes(b[8:16],"little")/1e6 - ts = int.from_bytes(b[:4], "little")/1000.0 - #print(f"len: {len(b)}, time: {ts}, data: {b}") - #can_id = int.from_bytes(b[8:12],"little") - # can_id = int.from_bytes(b[16:20],"little") - can_id = int.from_bytes(b[4:8], 'little') - #dlc = b[20] #TODO: sanity check on these values in case of corrupted messages. - bus_id = b[8] - dlc = b[9] - - #decompose ID - is_extended = bool(can_id & self.CAN_EFF_FLAG) #CAN_EFF_FLAG - if is_extended: - arb_id = can_id & 0x1FFFFFFF - else: - arb_id = can_id & 0x000007FF - - return can.Message( - timestamp = ts, - arbitration_id = arb_id, - is_extended_id = is_extended, - is_error_frame = bool(can_id & self.CAN_ERR_FLAG), #CAN_ERR_FLAG - is_remote_frame = bool(can_id & self.CAN_RTR_FLAG), #CAN_RTR_FLAG - dlc=dlc, - channel=bus_id, - #data=b[13:13+dlc] - #data=b[21:21+dlc] - data=b[10:10+dlc] - ) - - def send(self, msg: can.Message) -> None: - if msg.is_extended_id: - msg.arbitration_id |= self.CAN_EFF_FLAG - if msg.is_remote_frame: - msg.arbitration_id |= self.CAN_RTR_FLAG - if msg.is_error_frame: - msg.arbitration_id |= self.CAN_ERR_FLAG - self.send_buffer.put(msg) - - def _recv_internal(self, timeout: float | None = None) -> tuple[can.Message | None, bool]: - #TODO: filtering - try: - return (self.recv_buffer.get(timeout=timeout), True) - except queue.Empty: - return None, True - - def _poll_udp_socket(self) -> None: - """background thread to check for new CAN messages on the UDP socket""" - part_formed_message = bytearray() # UDP transfer might off part way through sending a message - conn = self._conn - while not self._shutdown_flag: #self._shutdown_flag.empty(): - try: - data = conn.recv(self.RECV_FRAME_SZ * 20) - except socket.timeout: - #no data, just try again. - continue - except OSError as e: - # socket's been closed. - utils.log_error(f"ERROR: connection closed (1): {e}") - self._stop_threads() - break - - if len(data): - # process the 1 or more messages we just received - - if len(part_formed_message): - data = part_formed_message + data #add on the previous remainder - - #check how many whole and incomplete messages we got through. - num_incomplete_bytes = len(data) % self.RECV_FRAME_SZ - num_frames = len(data) // self.RECV_FRAME_SZ - - #to pre-pend next time: - if num_incomplete_bytes: - part_formed_message = data[-num_incomplete_bytes:] - else: - part_formed_message = bytearray() - - c = 0 - for _ in range(num_frames): - self.recv_buffer.put(self._bytes_to_message(data[c:c+self.RECV_FRAME_SZ])) - c += self.RECV_FRAME_SZ - else: - #socket's been closed at the other end. - utils.log_error(f"ERROR: connection closed (2)") - self._stop_threads() - break - - def _stop_threads(self) -> None: - #self._shutdown_flag.put(True) - self._shutdown_flag = True - self._is_connected = False - utils.log_warning("Bus Client Shutdown (UDP)") - - def shutdown(self) -> None: - """gracefully close UDP connection and exit threads""" - if self._is_connected: - self._stop_threads() - #can't join threads because this might be called from that thread, so just wait... - while self._udp_listener.is_alive(): - sleep(0.005) - self._conn.close() #shutdown might be faster but can be ugly and raise an exception -# ---------------------------------------------------------------------------- # \ No newline at end of file diff --git a/hil/communication/daq_protocol.py b/hil/communication/daq_protocol.py deleted file mode 100644 index 6d0aceb..0000000 --- a/hil/communication/daq_protocol.py +++ /dev/null @@ -1,503 +0,0 @@ -from __future__ import annotations -from hil.communication.can_bus import BusSignal, CanBus -# from PyQt5 import QtCore -import hil.utils as utils -import can -import math -import numpy as np -import time - -DAQ_CMD_LENGTH = 3 -DAQ_CMD_MASK = 0b111 -DAQ_CMD_READ = 0 -DAQ_CMD_WRITE = 1 -DAQ_CMD_LOAD = 2 -DAQ_CMD_SAVE = 3 -DAQ_CMD_PUB_START = 4 -DAQ_CMD_PUB_STOP = 5 -DAQ_CMD_READ_PIN = 6 - -DAQ_RPLY_READ = 0 -DAQ_RPLY_SAVE = 1 -DAQ_RPLY_READ_ERROR = 2 -DAQ_RPLY_WRITE_ERROR = 3 -DAQ_RPLY_SAVE_ERROR = 4 -DAQ_RPLY_LOAD_ERROR = 5 -DAQ_RPLY_PUB = 6 -DAQ_RPLY_READ_PIN = 7 - -DAQ_ID_LENGTH = 5 -DAQ_ID_MASK = 0b11111 - -DAQ_BANK_LENGTH = 4 # bits -DAQ_BANK_MASK = 0xF -DAQ_PIN_LENGTH = 4 # bits -DAQ_PIN_MASK = 0xF -DAQ_PIN_VAL_LENGTH = 2 # bits -DAQ_PIN_VAL_MASK = 0x3 -DAQ_PIN_VAL_ERROR = 0x2 - -DAQ_READ_TIMEOUT_S = 5.0 -DAQ_WRITE_TIMEOUT_S = 5.0 - -""" -TODO: parsing, import file vars as signals - load and save commands are now different - when you write to a file variable, mark the file as dirty -""" - - -# ---------------------------------------------------------------------------- # -class DAQVariable(BusSignal): - """ DAQ variable that can be subscribed (connected) to for receiving updates""" - def __init__(self, - bus_name: str, - node_name: str, - msg_name: str, - sig_name: str, - id: int, - read_only: bool, - bit_length: int, - dtype: np.dtype, - store_dtype: np.dtype | None = None, - unit: str = "", - msg_desc: str = "", - sig_desc: str = "", - msg_period: int = 0, - file_name: str | None = None, - file_lbl: str | None = None, - scale: int = 1, - offset: int = 0 - ): - super(DAQVariable, self).__init__( - bus_name, - node_name, - msg_name, - sig_name, - dtype, - store_dtype=store_dtype, - unit=unit, - msg_desc=msg_desc, - sig_desc=sig_desc, - msg_period=msg_period - ) - self.id: int = id - self.read_only: bool = read_only - self.bit_length: int = bit_length - self.file: str = file_name - self.file_lbl: str = file_lbl - - self.pub_period_ms: int = 0 - - self.scale: int = scale - self.offset: int = offset - - self.is_dirty: bool = False - - @classmethod - def fromDAQVar(cls, id: int, var: dict, node: dict, bus: dict) -> DAQVariable: - send_dtype = utils.data_types[var['type']] - # If there is scaling going on, don't store as an integer on accident - if ('scale' in var and var['scale'] != 1) or ('offset' in var and var['offset'] != 0): - parse_dtype = utils.data_types['float'] - else: - parse_dtype = send_dtype - # Calculate bit length - bit_length = utils.data_type_length[var['type']] - if ('length' in var): - if ('uint' not in var['type'] or var['length'] > bit_length): - utils.log_error(f"Invalid bit length defined for DAQ variable {var['var_name']}") - bit_length = var['length'] - - return cls(bus['bus_name'], node['node_name'], f"daq_response_{node['node_name'].upper()}", var['var_name'], - id, var['read_only'], bit_length, - send_dtype, store_dtype=parse_dtype, - unit=(var['unit'] if 'unit' in var else ""), - - sig_desc=(var['var_desc'] if 'var_desc' in var else ""), - scale=(var['scale'] if 'scale' in var else 1), - offset=(var['offset'] if 'offset' in var else 0)) - - @classmethod - def fromDAQFileVar(cls, id: int, var: dict, file_name: str, file_lbl: str, node: dict, bus: dict) -> DAQVariable: - send_dtype = utils.data_types[var['type']] - # If there is scaling going on, don't store as an integer on accident - if ('scale' in var and var['scale'] != 1) or ('offset' in var and var['offset'] != 0): - parse_dtype = utils.data_types['float'] - else: - parse_dtype = send_dtype - # Calculate bit length - bit_length = utils.data_type_length[var['type']] - - return cls(bus['bus_name'], node['node_name'], f"daq_response_{node['node_name'].upper()}", var['var_name'], - id, False, bit_length, - send_dtype, store_dtype=parse_dtype, - unit=(var['unit'] if 'unit' in var else ""), - sig_desc=(var['var_desc'] if 'var_desc' in var else ""), - file_name=file_name, file_lbl=file_lbl, - scale=(var['scale'] if 'scale' in var else 1), - offset=(var['offset'] if 'offset' in var else 0)) - - def update(self, bytes: int, timestamp: float) -> None: - val = np.frombuffer(bytes.to_bytes((self.bit_length + 7)//8, 'little'), dtype=self.send_dtype, count=1) - val = val * self.scale + self.offset - super().update(val, timestamp) - - def reverseScale(self, val: float) -> float: - return (val - self.offset) / self.scale - - def valueSendable(self, val: float) -> bool: - # TODO: check max and min from json config - val = self.reverseScale(val) - if 'uint' in str(self.send_dtype): - max_size = pow(2, self.bit_length) - 1 - if val > max_size or val < 0: - return False - elif 'int' in str(self.send_dtype): - s = np.iinfo(self.send_dtype) - if val < s.min or val > s.max: - return False - return True - - def reverseToBytes(self, val: float) -> bytes | bool: - if not self.valueSendable(val): return False # Value will not fit in the given dtype - return (np.array([self.reverseScale(val)], dtype=self.send_dtype).tobytes()) - - def getSendValue(self, val: float) -> float | bool: - if not self.valueSendable(val): return False # Value will not fit in the given dtype - # Convert to send - a = np.array([self.reverseScale(val)], dtype=self.send_dtype)[0] - # Convert back - return a * self.scale + self.offset - - def isDirty(self) -> bool: - if self.file_lbl == None: return False - return self.is_dirty - - def updateDirty(self, dirty: bool) -> None: - if self.file_lbl == None: return - self.is_dirty = dirty - - @property - def state(self) -> int: - """ Read the value in blocking manner """ - old_t = self.last_update_time - utils.daqProt.readVar(self) - start_t = time.time() - while(self.last_update_time == old_t): - if (time.time() >= start_t + DAQ_READ_TIMEOUT_S): - utils.log_warning(f"Timed out reading DAQ var {self.signal_name} of {self.node_name}") - return 0 - time.sleep(0.015) - return self.curr_val - - @state.setter - def state(self, s: int) -> None: - """ Writes the value in blocking manner """ - if (self.read_only): - utils.log_error(f"Can't write to read-only DAQ variable {self.signal_name} of {self.node_name}") - return 0 - utils.daqProt.writeVar(self, s) - time.sleep(0.001) - a = self.state - if (abs(s - a) > 0.0001): - utils.log_warning(f"Write failed for DAQ var {self.signal_name} of {self.node_name}") -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -class DaqProtocol(): - """ Implements CAN daq protocol for modifying and live tracking of variables """ - - # save_in_progress_sig = QtCore.pyqtSignal(bool) - - def __init__(self, bus: CanBus, daq_config: dict): - super(DaqProtocol, self).__init__() - self.can_bus: CanBus = bus - self.can_bus.handle_daq_msg = self.handleDaqMsg - - self.updateVarDict(daq_config) - - # eeprom saving (prevent a load while save taking place) - self.last_save_request_id: int = 0 - self.save_in_progress: bool = False - utils.daqProt = self - - self.curr_pin: int = 0 - self.curr_bank: int = 0 - self.curr_pin_val: int = 0 - self.pin_read_in_progress: bool = False - - def readPin(self, node: str, bank: int, pin: int) -> None: - """ Requests to read a GPIO pin, expects a reply """ - dbc_msg = self.can_bus.db.get_message_by_name(f"daq_command_{node.upper()}") - self.curr_bank = bank & DAQ_BANK_MASK - self.curr_pin = pin & DAQ_PIN_MASK - val = ((((self.curr_pin) << DAQ_BANK_LENGTH) | (self.curr_bank)) << DAQ_CMD_LENGTH) | DAQ_CMD_READ_PIN - data = [val & 0xFF, (val >> 8) & 0xFF] - self.pin_read_in_progress = True - self.can_bus.sendMsg(can.Message(arbitration_id=dbc_msg.frame_id, - is_extended_id=True, - data=data)) - - def readVar(self, var: DAQVariable) -> None: - """ Requests to read a variable, expects a reply """ - dbc_msg = self.can_bus.db.get_message_by_name(f"daq_command_{var.node_name.upper()}") - data = [((var.id & DAQ_ID_MASK) << DAQ_CMD_LENGTH) | DAQ_CMD_READ] - self.can_bus.sendMsg(can.Message(arbitration_id=dbc_msg.frame_id, - is_extended_id=True, - data=data)) - - def writeVar(self, var: DAQVariable, new_val: float) -> None: - """ Writes to a variable """ - dbc_msg = self.can_bus.db.get_message_by_name(f"daq_command_{var.node_name.upper()}") - data = [((var.id & DAQ_ID_MASK) << DAQ_CMD_LENGTH) | DAQ_CMD_WRITE] - bytes = var.reverseToBytes(new_val) - # LSB, add variable data to byte array - for i in range(math.ceil(var.bit_length / 8)): - data.append(bytes[i]) - var.updateDirty(True) - - self.can_bus.sendMsg(can.Message(arbitration_id=dbc_msg.frame_id, - is_extended_id=True, - data=data)) - - def saveFile(self, var: DAQVariable) -> None: - """ Saves variable state in eeprom, expects save complete reply """ - if var.file_lbl == None: - utils.log_error(f"Invalid save var operation for {var.signal_name}") - return - dbc_msg = self.can_bus.db.get_message_by_name(f"daq_command_{var.node_name.upper()}") - data = [((var.id & DAQ_ID_MASK) << DAQ_CMD_LENGTH) | DAQ_CMD_SAVE] - lbl = var.file_lbl - data.append(ord(lbl[0])) - data.append(ord(lbl[1])) - data.append(ord(lbl[2])) - data.append(ord(lbl[3])) - self.can_bus.sendMsg(can.Message(arbitration_id=dbc_msg.frame_id, - is_extended_id=True, - data=data)) - self.setFileClean(var) - # self.save_in_progress = True - # self.last_save_request_id = var.id - # self.save_in_progress_sig.emit(True) - - def loadFile(self, var: DAQVariable) -> None: - """ Loads a variable from eeprom, cannot be performed during save operation """ - if var.file_lbl == None: - utils.log_error(f"Invalid load var operation for {var.signal_name}") - return - # if self.save_in_progress: - # utils.log_error(f"Cannot load var during save operation ") - # return - dbc_msg = self.can_bus.db.get_message_by_name(f"daq_command_{var.node_name.upper()}") - data = [((var.id & DAQ_ID_MASK) << DAQ_CMD_LENGTH) | DAQ_CMD_LOAD] - lbl = var.file_lbl - data.append(ord(lbl[0])) - data.append(ord(lbl[1])) - data.append(ord(lbl[2])) - data.append(ord(lbl[3])) - self.can_bus.sendMsg(can.Message(arbitration_id=dbc_msg.frame_id, - is_extended_id=True, - data=data)) - self.setFileClean(var) - - def pubVar(self, var: DAQVariable, period_ms: int) -> None: - """ Requests to start publishing a variable at a specified period """ - var.pub_period_ms = period_ms - dbc_msg = self.can_bus.db.get_message_by_name(f"daq_command_{var.node_name.upper()}") - data = [((var.id & DAQ_ID_MASK) << DAQ_CMD_LENGTH) | DAQ_CMD_PUB_START] - data.append(int(period_ms / 15) & 0xFF) - self.can_bus.sendMsg(can.Message(arbitration_id=dbc_msg.frame_id, - is_extended_id=True, - data=data)) - - def forceFault(self, id: int, state: int) -> None: - print(f"Id: {id}, State: {state}") - fault_msg = self.can_bus.db.get_message_by_name(f"set_fault") - data = fault_msg.encode({"id": id, "value": state}) - self.can_bus.sendMsg(can.Message(arbitration_id=fault_msg.frame_id, - is_extended_id=True, - data=data)) - - def create_ids(self, fault_config: dict) -> dict: - num = 0 - idx = 0 - for node in fault_config['modules']: - for fault in node['faults']: - #id : Owner (MCU) = 4 bits, Index in fault array = 12 bits - id = ((num << 12) | (idx & 0x0fff)) - # print(hex(id)) - fault['id'] = id - idx += 1 - num += 1 - id = 0 - for node in fault_config['modules']: - node['name_interp'] = id - id += 1 - for node in fault_config['modules']: - try: - node['can_name'] - except KeyError: - node['can_name'] = node['node_name'] - except: - print("An error occured configuring a node.") - return fault_config - - def unforceFault(self, id: int) -> None: - print(f"Id: {id}. Returning control!") - fault_msg = self.can_bus.db.get_message_by_name(f"return_fault_control") - data = fault_msg.encode({"id": id}) - self.can_bus.sendMsg(can.Message(arbitration_id=fault_msg.frame_id, - is_extended_id=True, - data=data)) - - def pubVarStop(self, var: DAQVariable) -> None: - """ Requests to stop publishing a variable """ - var.pub_period_ms = 0 - dbc_msg = self.can_bus.db.get_message_by_name(f"daq_command_{var.node_name.upper()}") - data = [((var.id & DAQ_ID_MASK) << DAQ_CMD_LENGTH) | DAQ_CMD_PUB_STOP] - self.can_bus.sendMsg(can.Message(arbitration_id=dbc_msg.frame_id, - is_extended_id=True, - data=data)) - - def setFileClean(self, var_in_file: DAQVariable) -> None: - """ Sets all variables in a file to clean (usually after flushing) """ - if (var_in_file.file_lbl == None): return - node_d = utils.signals[var_in_file.bus_name][var_in_file.node_name] - contents = node_d['files'][var_in_file.file]['contents'] - vars = node_d[var_in_file.message_name] - for file_var in contents: - vars[file_var].updateDirty(False) - - - def setFileClean(self, var_in_file: DAQVariable) -> None: - """ Sets all variables in a file to clean (usually after flushing) """ - if (var_in_file.file_lbl == None): return - node_d = utils.signals[var_in_file.bus_name][var_in_file.node_name] - contents = node_d['files'][var_in_file.file]['contents'] - vars = node_d[var_in_file.message_name] - for file_var in contents: - vars[file_var].updateDirty(False) - - - def handleDaqMsg(self, msg: can.Message) -> None: - """ Interprets and runs commands from DAQ message """ - # Return if not a DAQ message - if (msg.arbitration_id >> 6) & 0xFFFFF != 0xFFFFF: return - - #utils.log("DAQ MESSAGE") - dbc_msg = self.can_bus.db.get_message_by_frame_id(msg.arbitration_id) - node_name = dbc_msg.senders[0] - data = int.from_bytes(msg.data, "little") - - curr_bit = 0 - while (curr_bit <= msg.dlc * 8 - DAQ_CMD_LENGTH - DAQ_ID_LENGTH): - cmd = (data >> curr_bit) & DAQ_CMD_MASK - curr_bit += DAQ_CMD_LENGTH - - if cmd == DAQ_RPLY_READ or cmd == DAQ_RPLY_PUB: - id = (data >> curr_bit) & DAQ_ID_MASK - curr_bit += DAQ_ID_LENGTH - var = list(utils.signals[utils.b_str][node_name][dbc_msg.name].values())[id] - if not (cmd == DAQ_RPLY_PUB and self.can_bus.is_paused): - var.update((data >> curr_bit) & ~(0xFFFFFFFFFFFFFFFF << var.bit_length), msg.timestamp)#, not utils.logging_paused) - utils.log("Updated " + var.signal_name) - curr_bit += var.bit_length - elif cmd == DAQ_RPLY_SAVE: - id = (data >> curr_bit) & DAQ_ID_MASK - curr_bit += DAQ_ID_LENGTH - if self.last_save_request_id == id: - self.save_in_progress = False - # self.save_in_progress_sig.emit(False) - elif cmd == DAQ_RPLY_READ_ERROR: - id = (data >> curr_bit) & DAQ_ID_MASK - curr_bit += DAQ_ID_LENGTH - utils.log(msg) - utils.log_error(f"Failed to read {list(utils.signals[utils.b_str][node_name][dbc_msg.name])[id]}") - elif cmd == DAQ_RPLY_WRITE_ERROR: - id = (data >> curr_bit) & DAQ_ID_MASK - curr_bit += DAQ_ID_LENGTH - utils.log_error(f"Failed to write to {list(utils.signals[utils.b_str][node_name][dbc_msg.name])[id]}") - elif cmd == DAQ_RPLY_SAVE_ERROR: - id = (data >> curr_bit) & DAQ_ID_MASK - curr_bit += DAQ_ID_LENGTH - utils.log_error(f"Failed to save {list(utils.signals[utils.b_str][node_name][dbc_msg.name])[id]}") - elif cmd == DAQ_RPLY_LOAD_ERROR: - id = (data >> curr_bit) & DAQ_ID_MASK - curr_bit += DAQ_ID_LENGTH - utils.log_error(f"Failed to load {list(utils.signals[utils.b_str][node_name][dbc_msg.name])[id]}") - elif cmd == DAQ_RPLY_READ_PIN: - bank = (data >> curr_bit) & DAQ_BANK_MASK - curr_bit += DAQ_BANK_LENGTH - pin = (data >> curr_bit) & DAQ_PIN_MASK - curr_bit += DAQ_PIN_LENGTH - val = (data >> curr_bit) & DAQ_PIN_VAL_MASK - curr_bit += DAQ_PIN_VAL_LENGTH - if (val == DAQ_PIN_VAL_ERROR): - utils.log_error(f"Failed to read {node_name} bank {bank} pin {pin}.") - else: - if (pin == self.curr_pin and bank == self.curr_bank): - self.curr_pin_val = val - self.pin_read_in_progress = False - else: - utils.log_warning(f"Got unexpected pin read response {node_name} bank {bank} pin {pin}") - - def updateVarDict(self, daq_config: dict) -> None: - """ Creates dictionary of variable objects from daq configuration""" - for bus in daq_config['busses']: - # create bus keys - if bus['bus_name'] not in utils.signals: utils.signals[bus['bus_name']] = {} - for node in bus['nodes']: - # create node keys - if node['node_name'] not in utils.signals[bus['bus_name']]: utils.signals[bus['bus_name']][node['node_name']] = {} - if f"daq_response_{node['node_name'].upper()}" not in utils.signals[bus['bus_name']][node['node_name']]: utils.signals[bus['bus_name']][node['node_name']][f"daq_response_{node['node_name'].upper()}"] = {} - id_counter = 0 - for var in node['variables']: - # create new variable - utils.signals[bus['bus_name']][node['node_name']][f"daq_response_{node['node_name'].upper()}"][var['var_name']] = DAQVariable.fromDAQVar( - id_counter, var, node, bus) - id_counter += 1 - # Check file variables - if 'files' in node: - if 'files' not in utils.signals[bus['bus_name']][node['node_name']]: utils.signals[bus['bus_name']][node['node_name']]['files'] = {} - # file_name:is_dirty - file_dict = utils.signals[bus['bus_name']][node['node_name']]['files'] - for file in node['files']: - file_dict[file['name']] = {} - file_dict[file['name']]['contents'] = [] - for var in file['contents']: - file_dict[file['name']]['contents'].append(var['var_name']) - utils.signals[bus['bus_name']][node['node_name']][f"daq_response_{node['node_name'].upper()}"][var['var_name']] = DAQVariable.fromDAQFileVar( - id_counter, var, file['name'], file['eeprom_lbl'], node, bus) - id_counter += 1 -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -class DAQPin(): - def __init__(self, pin_name: str, board: str, bank: int, pin: int): - self.name: str = pin_name - self.board: str = board - self.bank: int = bank - self.pin: int = pin - self.t_last: float = time.time() - - @property - def state(self) -> int: - self.t_last = time.time() - t_start = time.time() - utils.daqProt.readPin(self.board, self.bank, self.pin) - while (utils.daqProt.pin_read_in_progress): - time.sleep(0.015) # This sleep allows rx thread to run - if (time.time() > t_start + DAQ_READ_TIMEOUT_S): - - utils.log_error(f"Pin read timed out for {self.board} net {self.name} on bank {self.bank} pin {self.pin}") - utils.daqProt.pin_read_in_progress = False - return 0 - return utils.daqProt.curr_pin_val - - # @state.setter - # def state(self, s): - # TODO: not currently implemented - diff --git a/hil/components/component.py b/hil/components/component.py deleted file mode 100644 index b38d8fa..0000000 --- a/hil/components/component.py +++ /dev/null @@ -1,124 +0,0 @@ -from collections.abc import Callable -from typing import TYPE_CHECKING - -import hil.utils as utils -if TYPE_CHECKING: - from hil.hil import HIL - -class Component(): - """ - Generalized component of system - Can operate in NC, Measure, Emulate, or Hardware modes - When in measurement or emulation, a source must be specified. - """ - - def __init__(self, name: str, hil_con: tuple[str, str], mode: str, hil: 'HIL'): - self.name: str = name - - self._state = 0 - self.inv_meas: bool = False - self.inv_emul: bool = False - self.read_func: Callable[[], int] = None - self.write_func: Callable[[int], None] = None - self.hiZ_func: Callable[[], None] = None - - # TODO: allow both measure and emulation source - - # if "measure_source" in config: - # me_src = config['measure_source'] - # self.me = hil.get_hil_device(me_src['device']) - # if "inv" in me_src: - # self.inv_meas = me_src["inv"] - - # if (me_src['mode'] == "DI"): - # self.read_port = self.me.get_port_number(me_src['port'], me_src['mode']) - # if self.inv_meas: - # self.read_func = lambda : not self.me.read_gpio(self.read_port) - # else: - # self.read_func = lambda : self.me.read_gpio(self.read_port) - # elif (me_src['mode'] == "AI"): - # self.read_port = self.me.get_port_number(me_src['port'], me_src['mode']) - # self.read_func = lambda : self.me.read_analog(self.read_port) - # else: - # utils.log_error(f"Unrecognized measure mode {me_src['mode']} for component {self.name}") - - # if "emulation_source" in config: - # em_src = config['emulation_source'] - # self.em = hil.get_hil_device(em_src['device']) - - # if "inv" in em_src: - # self.inv_emul = em_src["inv"] - - # if (em_src['mode'] == "DO"): - # self.write_port = self.em.get_port_number(em_src['port'], em_src['mode']) - # if self.inv_emul: - # self.write_func = lambda s: self.em.write_gpio(self.write_port, not s) - # else: - # self.write_func = lambda s: self.em.write_gpio(self.write_port, s) - # self.state = self._state - # elif(em_src['mode'] == "AO"): - # self.write_port = self.em.get_port_number(em_src['port'], em_src['mode']) - # self.write_func = lambda s: self.em.write_dac(self.write_port, s) - # else: - # utils.log_error(f"Unrecognized emulation mode {em_src['mode']} for component {self.name}") - - dev = hil.get_hil_device(hil_con[0]) - hil_port_num = dev.get_port_number(hil_con[1], mode) - if (hil_port_num >= 0): - print(f"Creating new component '{self.name}' of type {mode} on {hil_con}") - if (mode == "DI"): - if self.inv_meas: - self.read_func = lambda : not dev.read_gpio(hil_port_num) - else: - self.read_func = lambda : dev.read_gpio(hil_port_num) - elif (mode == "AI"): - self.read_func = lambda : dev.read_analog(hil_port_num) - elif (mode == "DO"): - if self.inv_emul: - self.write_func = lambda s: dev.write_gpio(hil_port_num, not s) - else: - self.write_func = lambda s: dev.write_gpio(hil_port_num, s) - self.state = self._state - self.hiZ_func = lambda : dev.read_gpio(hil_port_num) - # TODO: check if hil port also has DI capability (i.e. relay can't hiZ) - elif(mode == "AO"): - self.write_func = lambda s: dev.write_dac(hil_port_num, s) - self.hiZ_func = lambda : dev.read_gpio(hil_port_num) - elif(mode == "POT"): - self.write_func = lambda s: dev.write_pot(hil_port_num, s) - else: - utils.log_error(f"Unrecognized emulation/measurement mode {mode} for component {self.name}") - else: - utils.log_error(f"Failed to get hil port for component {self.name}") - - self.hil = hil - - @property - def state(self) -> int: - if self.read_func: - self._state = self.read_func() - elif self.write_func == None: - utils.log_warning(f"Read from {self.name}, but no measurement source or emulation source was found") - return self._state - - @state.setter - def state(self, s: int) -> None: - if self.read_func == None: - self._state = s - if self.write_func: - self.write_func(s) - else: - utils.log_warning(f"Wrote to {self.name}, but no emulation source was found") - - def hiZ(self) -> None: - if (self.hiZ_func): - self.hiZ_func() - else: - utils.log_warning(f"hiZ is not supported for {self.name}") - - def shutdown(self) -> None: - if (self.hiZ_func): - self.hiZ_func() - elif self.write_func: - self.state = 0 - diff --git a/hil/hil.py b/hil/hil.py deleted file mode 100644 index 80e3bfd..0000000 --- a/hil/hil.py +++ /dev/null @@ -1,216 +0,0 @@ -from types import FrameType -import hil.utils as utils -import os -import signal -import sys -from hil.pin_mapper import PinMapper -from hil.hil_devices.hil_device import HilDevice -from hil.hil_devices.serial_manager import SerialManager -from hil.components.component import Component - -from hil.communication.can_bus import CanBus, BusSignal -from hil.communication.daq_protocol import DaqProtocol -from hil.communication.daq_protocol import DAQPin -from hil.communication.daq_protocol import DAQVariable - -""" HIL TESTER """ - -JSON_CONFIG_SCHEMA_PATH = "" -CONFIG_PATH = os.path.join("..", "configurations") - -NET_MAP_PATH = os.path.join("..", "net_maps") -PIN_MAP_PATH = os.path.join("..", "pin_maps") - -PARAMS_PATH = os.path.join("..", "hil_params.json") - -DAQ_CONFIG_PATH = os.path.join("common", "daq", "daq_config.json") -DAQ_SCHEMA_PATH = os.path.join("common", "daq", "daq_schema.json") -DBC_PATH = os.path.join("common", "daq", "per_dbc.dbc") -CAN_CONFIG_PATH = os.path.join("common", "daq", "can_config.json") -CAN_SCHEMA_PATH = os.path.join("common", "daq", "can_schema.json") -# FAULT_CONFIG_PATH = os.path.join("common", "faults", "fault_config.json") -# FAULT_SCHEMA_PATH = os.path.join("common", "faults", "fault_schema.json") - - -class HIL(): - def __init__(self): - utils.initGlobals() - self.components: dict[str, Component] = {} - self.dut_connections: dict[str, dict[str, dict[str, tuple[str, str]]]] = {} - self.hil_devices: dict[str, HilDevice] = {} - self.serial_manager: SerialManager = SerialManager() - self.hil_params: dict = utils.load_json_config(PARAMS_PATH, None) - self.can_bus: CanBus = None - utils.hilProt = self - signal.signal(signal.SIGINT, signal_int_handler) - - def init_can(self): - firmware_path = self.hil_params["firmware_path"] - - self.daq_config = utils.load_json_config(os.path.join(firmware_path, DAQ_CONFIG_PATH), os.path.join(firmware_path, DAQ_SCHEMA_PATH)) - self.can_config = utils.load_json_config(os.path.join(firmware_path, CAN_CONFIG_PATH), os.path.join(firmware_path, CAN_SCHEMA_PATH)) - - self.can_bus = CanBus(os.path.join(firmware_path, DBC_PATH), self.hil_params["default_ip"], self.can_config) - self.daq_protocol = DaqProtocol(self.can_bus, self.daq_config) - - self.can_bus.connect() - self.can_bus.start() - - def load_pin_map(self, net_map: str, pin_map: str) -> None: - net_map_f = os.path.join(NET_MAP_PATH, net_map) - pin_map_f = os.path.join(PIN_MAP_PATH, pin_map) - - self.pin_map = PinMapper(net_map_f) - self.pin_map.load_mcu_pin_map(pin_map_f) - - def clear_components(self) -> None: - """ Reset HIL""" - for c in self.components.values(): - c.shutdown() - self.components = {} - - def clear_hil_devices(self) -> None: - self.hil_devices = {} - self.serial_manager.close_devices() - - def shutdown(self) -> None: - self.clear_components() - self.clear_hil_devices() - self.stop_can() - - def stop_can(self) -> None: - if not self.can_bus: return - - if self.can_bus.connected: - self.can_bus.connected = False - self.can_bus.join() - # while(not self.can_bus.isFinished()): - # # wait for bus receive to finish - # pass - self.can_bus.disconnect_bus() - - def load_config(self, config_name: str) -> None: - config = utils.load_json_config(os.path.join(CONFIG_PATH, config_name), None) # TODO: validate w/ schema - - # TODO: support joining configs - - # Load hil_devices - self.load_hil_devices(config['hil_devices']) - - # Setup corresponding components - self.load_connections(config['dut_connections']) - - def load_connections(self, dut_connections: dict) -> None: - self.dut_connections = {} - # Dictionary format: - # [board][connector][pin] = (hil_device, port) - for board_connections in dut_connections: - board_name = board_connections['board'] - if not board_name in self.dut_connections: - self.dut_connections[board_name] = {} - for c in board_connections['harness_connections']: - connector = c['dut']['connector'] - pin = str(c['dut']['pin']) - hil_port = (c['hil']['device'], c['hil']['port']) - if not connector in self.dut_connections[board_name]: - self.dut_connections[board_name][connector] = {} - self.dut_connections[board_name][connector][pin] = hil_port - - def add_component(self, board: str, net: str, mode: str) -> Component: - # If board is a HIL device, net is expected to be port name - # If board is a DUT device, net is expected to be a net name from the board - if board in self.hil_devices: - hil_con = (board, net) - else: - hil_con = self.get_hil_device_connection(board, net) - comp_name = '.'.join([board, net]) - if not comp_name in self.components: - comp = Component(comp_name, hil_con, mode, self) - self.components[comp_name] = comp - else: - utils.log_warning(f"Component {comp_name} already exists") - return self.components[comp_name] - - def load_hil_devices(self, hil_devices: dict) -> None: - self.clear_hil_devices() - self.serial_manager.discover_devices() - for hil_device in hil_devices: - if self.serial_manager.port_exists(hil_device["id"]): - self.hil_devices[hil_device['name']] = HilDevice(hil_device['name'], hil_device['type'], hil_device['id'], self.serial_manager) - else: - self.handle_error(f"Failed to discover HIL device {hil_device['name']} with id {hil_device['id']}") - - def get_hil_device(self, name: str) -> HilDevice: - if name in self.hil_devices: - return self.hil_devices[name] - else: - self.handle_error(f"HIL device {name} not recognized") - - def get_hil_device_connection(self, board: str, net: str) -> tuple[str, str]: - """ Converts dut net to hil port name """ - if not board in self.dut_connections: - self.handle_error(f"No connections to {board} found in configuration.") - board_cons = self.dut_connections[board] - - net_cons = self.pin_map.get_net_connections(board, net) - for c in net_cons: - connector = c[0] - pin = c[1] - if connector in board_cons: - if pin in board_cons[connector]: - return board_cons[connector][pin] - utils.log_warning(f"Unable to find dut connection to net {net} on board {board}") - utils.log_warning(f"The net {net} is available on {board} via ...") - utils.log_warning(net_cons) - self.handle_error(f"Connect dut to {net} on {board}.") - - def din(self, board: str, net: str) -> Component: - return self.add_component(board, net, 'DI') - - def dout(self, board: str, net: str) -> Component: - return self.add_component(board, net, 'DO') - - def ain(self, board: str, net: str) -> Component: - return self.add_component(board, net, 'AI') - - def aout(self, board: str, net: str) -> Component: - return self.add_component(board, net, 'AO') - - def pot(self, board: str, net: str) -> Component: - return self.add_component(board, net, 'POT') - - def daq_var(self, board: str, var_name: str) -> DAQVariable: - try: - return utils.signals[utils.b_str][board][f"daq_response_{board.upper()}"][var_name] - except KeyError: - self.handle_error(f"Unable to locate DAQ variable {var_name} of {board}") - - def can_var(self, board: str, message_name: str, signal_name: str) -> BusSignal: - try: - return utils.signals[utils.b_str][board][message_name][signal_name] - except KeyError: - self.handle_error(f"Unable to locate CAN signal {signal_name} of message {message_name} of board {board}") - - def mcu_pin(self, board: str, net: str) -> DAQPin: - bank, pin = self.pin_map.get_mcu_pin(board, net) - if bank == None: - self.handle_error(f"Failed to get mcu pin for {board} net {net}") - return DAQPin(net, board, bank, pin) - - def handle_error(self, msg: str) -> None: - utils.log_error(msg) - self.shutdown() - exit(0) - - -def signal_int_handler(signum: int, frame: FrameType) -> None: - utils.log("Received signal interrupt, shutting down") - if utils.hilProt: - utils.hilProt.shutdown() - sys.exit(0) - - -# Old testing code. When run directly (python hil.py), this code will run. -# if __name__ == "__main__": -# hil = HIL() -# hil.load_config("config_test.json") diff --git a/hil/hil_devices/hil_device.py b/hil/hil_devices/hil_device.py deleted file mode 100644 index 2b4f04f..0000000 --- a/hil/hil_devices/hil_device.py +++ /dev/null @@ -1,110 +0,0 @@ -import os -from hil.hil_devices.serial_manager import SerialManager - -import hil.utils as utils - -HIL_CMD_READ_ADC = 0 # command, pin -HIL_CMD_READ_GPIO = 1 # command, pin -HIL_CMD_WRITE_DAC = 2 # command, pin, value (2 bytes) -HIL_CMD_WRITE_GPIO = 3 # command, pin, value -HIL_CMD_READ_ID = 4 # command -HIL_CMD_WRITE_POT = 5 # command, pin, value - -SERIAL_MASK = 0xFF # 2^8 - 1 -SERIAL_BITS = 8 # char - -HIL_DEVICES_PATH = os.path.join("..", "hil", "hil_devices") - - -class HilDevice(): - def __init__(self, name: str, type: str, id: int, serial_manager: SerialManager): - self.name: str = name - self.type: str = type - self.id: int = id - self.sm: SerialManager = serial_manager - - self.config = utils.load_json_config(os.path.join(HIL_DEVICES_PATH, f"hil_device_{self.type}.json"), None) # TODO: validate w/ schema - - self.rail_5v = 0 - if "calibrate_rail" in self.config and self.config['calibrate_rail']: - # Measure 3V3 rail and find dac reference - p = self.get_port_number('3v3ref', 'AI') - if p >= 0: - self.adc_to_volts = 1 - self.adc_max = pow(2, self.config['adc_config']['bit_resolution']) - 1 - meas_3v3 = self.read_analog(p) - # 3.3V = meas_3v3 / adc_max * 5V - # 5V = 3.3V * adc_max / meas_3v3 - self.rail_5v = 3.3 * self.adc_max / meas_3v3 - utils.log(f"5V rail measured to be {self.rail_5v:.3}V on {self.name}") - - self.adc_to_volts = 0.0 - self.adc_max = 0 - if "adc_config" in self.config: - self.adc_max = pow(2, self.config['adc_config']['bit_resolution']) - 1 - if self.rail_5v == 0: - self.adc_to_volts = float(self.config['adc_config']['reference_v']) / self.adc_max - else: - self.adc_to_volts = self.rail_5v / self.adc_max - - self.volts_to_dac = 0.0 - self.dac_max = 0 - if "dac_config" in self.config: - self.dac_max = pow(2, self.config['dac_config']['bit_resolution']) - 1 - if self.rail_5v == 0: - self.volts_to_dac = self.dac_max / float(self.config['dac_config']['reference_v']) - else: - self.volts_to_dac = self.dac_max / self.rail_5v - - self.pot_max = 0 - if "pot_config" in self.config: - self.pot_max = pow(2, self.config['pot_config']['bit_resolution']) - 1 - - def get_port_number(self, port_name: str, mode: str) -> int: - for p in self.config['ports']: - if port_name == p['name']: - if mode in p['capabilities']: - return p['port'] - else: - utils.log_warning(f"Port {port_name} on {self.name} does not have capability {mode}") - utils.log_warning(f"Ports with {mode} capability for {self.name} include:") - utils.log_warning([p['name'] for p in self.config['ports'] if mode in p['capabilities']]) - utils.log_warning("Change connection and try again.") - return -1 - utils.log_error(f"Port {port_name} not found for hil device {self.name}") - return -1 - - def write_gpio(self, pin: int, value: int) -> None: - data = [(HIL_CMD_WRITE_GPIO & SERIAL_MASK), (pin & SERIAL_MASK), value] - self.sm.send_data(self.id, data) - - def write_dac(self, pin: int, voltage: float) -> None: - value = int(voltage * self.volts_to_dac) - char_1 = (value >> SERIAL_BITS) & SERIAL_MASK - char_2 = value & SERIAL_MASK - data = [(HIL_CMD_WRITE_DAC & SERIAL_MASK), (pin & SERIAL_MASK), char_1, char_2] - self.sm.send_data(self.id, data) - - def read_gpio(self, pin: int) -> int: - data = [(HIL_CMD_READ_GPIO & SERIAL_MASK), (pin & SERIAL_MASK)] - self.sm.send_data(self.id, data) - d = self.sm.read_data(self.id, 1) - if len(d) == 1: - d = int.from_bytes(d, "big") - if (d <= 1): return d - utils.log_error(f"Failed to read gpio pin {pin} on {self.name}") - - def read_analog(self, pin: int) -> float: - data = [(HIL_CMD_READ_ADC & SERIAL_MASK), (pin & SERIAL_MASK)] - self.sm.send_data(self.id, data) - d = self.sm.read_data(self.id, 2) - if len(d) == 2: - d = int.from_bytes(d, "big") - if (d <= self.adc_max): return (d * self.adc_to_volts) - utils.log_error(f"Failed to read adc pin {pin} on {self.name}") - return 0 - - def write_pot(self, pin: int, value: float) -> None: - value = min(self.pot_max, max(0, int(value * self.pot_max))) - data = [(HIL_CMD_WRITE_POT & SERIAL_MASK), (pin & SERIAL_MASK), value] - self.sm.send_data(self.id, data) \ No newline at end of file diff --git a/hil/hil_devices/hil_device_arduino_micro.json b/hil/hil_devices/hil_device_arduino_micro.json deleted file mode 100644 index eb66a6d..0000000 --- a/hil/hil_devices/hil_device_arduino_micro.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "./emulator_schema.json", - "communication_mode":"serial", - "ports":[ - {"port":0, "name":"D0", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":1, "name":"D1", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":2, "name":"D2", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":3, "name":"D3", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":4, "name":"D4", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":5, "name":"D5", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":6, "name":"D6", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":7, "name":"D7", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":8, "name":"D8", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":9, "name":"D9", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":10, "name":"D10", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":11, "name":"D11", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":12, "name":"D12", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":13, "name":"D13", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":14, "name":"D14", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":15, "name":"D15", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":16, "name":"D16", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":17, "name":"D17", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":18, "name":"A0", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":19, "name":"A1", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":20, "name":"A2", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":21, "name":"A3", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":22, "name":"A4", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":23, "name":"A5", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"} - ], - "adc_config":{"bit_resolution":10, "reference_v":5.0} -} \ No newline at end of file diff --git a/hil/hil_devices/hil_device_arduino_uno.json b/hil/hil_devices/hil_device_arduino_uno.json deleted file mode 100644 index 0eecf82..0000000 --- a/hil/hil_devices/hil_device_arduino_uno.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "./emulator_schema.json", - "communication_mode":"serial", - "ports":[ - {"port":0, "name":"D0", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":1, "name":"D1", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":2, "name":"D2", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":3, "name":"D3", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":4, "name":"D4", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":5, "name":"D5", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":6, "name":"D6", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":7, "name":"D7", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":8, "name":"D8", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":9, "name":"D9", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":10, "name":"D10", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":11, "name":"D11", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":12, "name":"D12", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":13, "name":"D13", "capabilities":["DI", "DO"], "notes":"5V digital"}, - {"port":14, "name":"A0", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":15, "name":"A1", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":16, "name":"A2", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":17, "name":"A3", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":18, "name":"A4", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":19, "name":"A5", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"} - ], - "adc_config":{"bit_resolution":10, "reference_v":5.0} -} \ No newline at end of file diff --git a/hil/hil_devices/hil_device_disco_f4.json b/hil/hil_devices/hil_device_disco_f4.json deleted file mode 100644 index fb4b28f..0000000 --- a/hil/hil_devices/hil_device_disco_f4.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "$schema": "./emulator_schema.json", - "communication_mode":"serial", - "ports":[ - {"port":0, "name":"PA0", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":1, "name":"PA1", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":2, "name":"PA2", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":3, "name":"PA3", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":4, "name":"PA4", "capabilities":["DI", "AI", "DO", "AO"], "notes":"3V3 digital"}, - {"port":5, "name":"PA5", "capabilities":["DI", "AI", "DO", "AO"], "notes":"3V3 digital"}, - {"port":6, "name":"PA6", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":7, "name":"PA7", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":8, "name":"PA8", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":9, "name":"PA9", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":10, "name":"PA10", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":11, "name":"PA11", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":12, "name":"PA12", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":13, "name":"PA13", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":14, "name":"PA14", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":15, "name":"PA15", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - - {"port":16, "name":"PB0", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":17, "name":"PB1", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":18, "name":"PB2", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":19, "name":"PB3", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":20, "name":"PB4", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":21, "name":"PB5", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":22, "name":"PB6", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":23, "name":"PB7", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":24, "name":"PB8", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":25, "name":"PB9", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":26, "name":"PB10", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":27, "name":"PB11", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":28, "name":"PB12", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":29, "name":"PB13", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":30, "name":"PB14", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":31, "name":"PB15", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - - {"port":32, "name":"PC0", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":33, "name":"PC1", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":34, "name":"PC2", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":35, "name":"PC3", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":36, "name":"PC4", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":37, "name":"PC5", "capabilities":["DI", "AI", "DO"], "notes":"3V3 digital"}, - {"port":38, "name":"PC6", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":39, "name":"PC7", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":40, "name":"PC8", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":41, "name":"PC9", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":42, "name":"PC10", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":43, "name":"PC11", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":44, "name":"PC12", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":45, "name":"PC13", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":46, "name":"PC14", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":47, "name":"PC15", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - - {"port":48, "name":"PD0", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":49, "name":"PD1", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":50, "name":"PD2", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":51, "name":"PD3", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":52, "name":"PD4", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":53, "name":"PD5", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":54, "name":"PD6", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":55, "name":"PD7", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":56, "name":"PD8", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":57, "name":"PD9", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":58, "name":"PD10", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":59, "name":"PD11", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":60, "name":"PD12", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":61, "name":"PD13", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":62, "name":"PD14", "capabilities":["DI", "DO"], "notes":"3V3 digital"}, - {"port":63, "name":"PD15", "capabilities":["DI", "DO"], "notes":"3V3 digital"} - ], - "adc_config":{"bit_resolution":10, "reference_v":3.0}, - "dac_config":{"bit_resolution":8, "reference_v":3.0} -} \ No newline at end of file diff --git a/hil/hil_devices/hil_device_test_pcb.json b/hil/hil_devices/hil_device_test_pcb.json deleted file mode 100644 index c82537e..0000000 --- a/hil/hil_devices/hil_device_test_pcb.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "./emulator_schema.json", - "communication_mode":"serial", - "ports":[ - {"port":16, "name":"DI3", "capabilities":["DI"], "notes":"digital input 5V-30V"}, - {"port":14, "name":"DI4", "capabilities":["DI"], "notes":"digital input 5V-30V"}, - {"port":15, "name":"DI5", "capabilities":["DI"], "notes":"digital input 5V-30V"}, - {"port":8, "name":"DI6", "capabilities":["DI"], "notes":"digital input 5V-30V"}, - {"port":4, "name":"DI7", "capabilities":["DI"], "notes":"digital input 5V-30V"}, - {"port":18, "name":"AI1", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":19, "name":"AI2", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":20, "name":"AI3", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":21, "name":"AI4", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":10, "name":"RLY1", "capabilities":["DO"], "notes":"relay"}, - {"port":11, "name":"RLY2", "capabilities":["DO"], "notes":"relay"}, - {"port":12, "name":"RLY3", "capabilities":["DO"], "notes":"relay"}, - {"port":13, "name":"RLY4", "capabilities":["DO"], "notes":"relay"}, - {"port":200, "name":"DAC1", "capabilities":["AO"], "notes":"5V 8-bit DAC"}, - {"port":201, "name":"DAC2", "capabilities":["AO"], "notes":"5V 8-bit DAC"}, - {"port":1, "name":"POT1", "capabilities":["POT"], "notes":"6-bit Digipot"}, - {"port":2, "name":"POT2", "capabilities":["POT"], "notes":"6-bit Digipot"}, - {"port":9, "name":"3v3ref", "capabilities":["AI"], "notes":"3V3 input reference"} - ], - "adc_config":{"bit_resolution":10, "reference_v":5.0}, - "dac_config":{"bit_resolution":8, "reference_v":5.0}, - "pot_config":{"bit_resolution":6}, - "calibrate_rail":true -} \ No newline at end of file diff --git a/hil/hil_devices/hil_device_test_uno.json b/hil/hil_devices/hil_device_test_uno.json deleted file mode 100644 index b2a92e7..0000000 --- a/hil/hil_devices/hil_device_test_uno.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "./emulator_schema.json", - "communication_mode":"serial", - "ports":[ - {"port":16, "name":"DI3", "capabilities":["DI"], "notes":"digital input 5V-30V"}, - {"port":14, "name":"DI4", "capabilities":["DI"], "notes":"digital input 5V-30V"}, - {"port":15, "name":"DI5", "capabilities":["DI"], "notes":"digital input 5V-30V"}, - {"port":8, "name":"DI6", "capabilities":["DI"], "notes":"digital input 5V-30V"}, - {"port":4, "name":"DI7", "capabilities":["DI"], "notes":"digital input 5V-30V"}, - {"port":18, "name":"AI1", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":19, "name":"AI2", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":20, "name":"AI3", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":21, "name":"AI4", "capabilities":["DI", "AI", "DO"], "notes":"analog / digital input 5V"}, - {"port":10, "name":"RLY1", "capabilities":["DO"], "notes":"relay"}, - {"port":11, "name":"RLY2", "capabilities":["DO"], "notes":"relay"}, - {"port":12, "name":"RLY3", "capabilities":["DO"], "notes":"relay"}, - {"port":13, "name":"RLY4", "capabilities":["DO"], "notes":"relay"}, - {"port":200, "name":"DAC1", "capabilities":["AO"], "notes":"5V 8-bit DAC"}, - {"port":201, "name":"DAC2", "capabilities":["AO"], "notes":"5V 8-bit DAC"}, - {"port":17, "name":"3v3ref", "capabilities":["AI"], "notes":"3V3 input reference"} - ], - "adc_config":{"bit_resolution":10, "reference_v":5.0}, - "dac_config":{"bit_resolution":8, "reference_v":5.0}, - "calibrate_rail":true -} \ No newline at end of file diff --git a/hil/hil_devices/serial_manager.py b/hil/hil_devices/serial_manager.py deleted file mode 100644 index 4d47005..0000000 --- a/hil/hil_devices/serial_manager.py +++ /dev/null @@ -1,53 +0,0 @@ -import serial -import serial.tools.list_ports -import time - -class SerialManager(): - """ Manages hil device discovery and communication """ - - def __init__(self): - self.devices: dict[int, serial.Serial] = {} - - def discover_devices(self) -> None: - # print([a[0] for a in serial.tools.list_ports.comports()]) - ports = [a[0] for a in serial.tools.list_ports.comports() if ("Arduino" in a[1] or "USB Serial Device" in a[1])] - self.devices = {} - print('Arduinos found on ports ' + str(ports)) - for p in ports: - ard = serial.Serial(p,115200, timeout=0.1, - bytesize=serial.EIGHTBITS, - parity=serial.PARITY_NONE, - stopbits=serial.STOPBITS_ONE, - xonxoff=0, - rtscts=0) - ard.setDTR(False) - time.sleep(1) - ard.flushInput() - ard.setDTR(True) - # Uno takes a while startup, have to treat it nicely - for _ in range(5): - # 4 = HIL_CMD_READ_ID - ard.write(b'\x04') - i = ard.read(1) - if (len(i) == 1): - break - time.sleep(1) - if (len(i) == 1): - self.devices[int.from_bytes(i, "big")] = ard - else: - print('Failed to receive tester id on port ' + str(p) + ' ' + str(i)) - ard.close() - print('Tester ids: ' + str(list(self.devices.keys()))) - - def port_exists(self, id: int) -> bool: - return id in self.devices - - def send_data(self, id: int, data: list[int]) -> None: - self.devices[id].write(data) - - def read_data(self, id: int, length: int) -> bytes: - return self.devices[id].read(length) - - def close_devices(self) -> None: - for d in self.devices.values(): - d.close() diff --git a/hil/pin_mapper.py b/hil/pin_mapper.py deleted file mode 100644 index 16ed541..0000000 --- a/hil/pin_mapper.py +++ /dev/null @@ -1,90 +0,0 @@ -#import utils -import os -#from hil_devices.hil_device import HilDevice -import csv -import hil.utils as utils - -""" PIN MAPPER """ - -# TODO: support multiple MCU types - -class PinMapper(): - - def __init__(self, net_map: str): - #utils.initGlobals() - - # [board name][net name] = [(component, designator, connector name), ...] - self.net_map: dict[str, dict[str, list[tuple[str, str, str]]]] = {} - self.net_map_fname: str = "" - - # [designator] = (bank, pin) - self.mcu_pin_map: dict[int, tuple[int, int]] = {} - self.mcu_pin_name_fname: str = "" - - self.load_net_map(net_map) - - def load_net_map(self, fname: str) -> None: - self.net_map_fname = fname - # CSV format: Board,Net,Component,Designator,Connector Name,, - with open(self.net_map_fname, mode='r') as f: - csv_file = csv.DictReader(f) - for row in csv_file: - board = row['Board'] - net = row['Net'] - items = (row['Component'], row['Designator'], row['Connector Name']) - if not board in self.net_map: - self.net_map[board] = {} - net_map_board = self.net_map[board] - if not net in net_map_board: - net_map_board[net] = [] - self.net_map[board][net].append(items) - - def load_mcu_pin_map(self, fname: str) -> None: - self.mcu_pin_name_fname = fname - self.mcu_pin_map = {} - # CSV format: Designator,Pin Name,Type - with open(self.mcu_pin_name_fname, mode='r') as f: - csv_file = csv.DictReader(f) - for row in csv_file: - if row['Type'] != 'I/O' or row['Pin Name'][0] != 'P': - continue - designator = int(row['Designator']) - bank = int(ord(row['Pin Name'][1]) - ord('A')) - pin = int(row['Pin Name'][2:]) - self.mcu_pin_map[designator] = (bank, pin) - - def get_mcu_pin(self, board: str, net: str) -> tuple[int, int]: - """ Returns first MCU pin found that is connected """ - connections = self.get_net_connections(board, net) - for connection in connections: - if connection[2] == 'MCU': - designator = int(connection[1]) - if not designator in self.mcu_pin_map: - utils.log_error(f"Net {net} on board {board} on MCU designator {designator} is not a recognized I/O pin.") - return (None, None) - return self.mcu_pin_map[designator] - utils.log_error(f"Net {net} on board {board} is not connected to MCU.") - return (None, None) - - def get_net_connections(self, board: str, net: str) -> list[tuple[str, str, str]]: - """ [(component, designator, connector name), ...] """ - if not board in self.net_map: - utils.log_error(f"Unrecogniazed board {board}.") - return - if not net in self.net_map[board]: - utils.log_error(f"Unrecognized net {net} in board {board}.") - return - return self.net_map[board][net] - - -if __name__ == "__main__": - pm = PinMapper(os.path.join("..", "net_maps", "per_24_net_map.csv")) - pm.load_mcu_pin_map(os.path.join("..", "pin_maps", "stm32f407_pin_map.csv")) - - print(pm.get_mcu_pin('ur mom', 'idk')) - print(pm.get_mcu_pin('a_box', 'ur mom')) - print(pm.get_mcu_pin('a_box', 'Isense_Ch1_raw')) - print(pm.get_mcu_pin('a_box', 'GND')) - print(pm.get_mcu_pin('a_box', 'ISense Ch1')) - - \ No newline at end of file diff --git a/hil/utils.py b/hil/utils.py deleted file mode 100644 index 16af398..0000000 --- a/hil/utils.py +++ /dev/null @@ -1,212 +0,0 @@ -from typing import TYPE_CHECKING - -import sys -import time -import numpy as np - -import json -from jsonschema import validate -from jsonschema.exceptions import ValidationError - -from hil.components.component import Component -from hil.communication.daq_protocol import DaqProtocol - -if TYPE_CHECKING: - from hil.hil import HIL - - -signals: dict = {} -b_str: str = "" -data_types: dict[str, np.dtype] = {} -data_type_length: dict[str, int] = {} -debug_mode: bool = True -daqProt: DaqProtocol = None -hilProt: 'HIL' = None - - -def initGlobals(): - global signals - signals = {} - # Structure of signals based on daq_config and can_config - # signals = { - # 'bus_name': { # busses; ex: "Main", "Test" - # 'node_name': { # busses->nodes; ex: "Main_Module", "Dashboard" - # 'msg_name': { # busses->nodes->tx; ex: "main_hb", "coolant_temps" - # 'sig_name': BusSignal # busses->nodes->tx->signals; ex: "car_state", "battery_in_temp" - # }, - # "daq_response_{node['node_name'].upper()}": { # busses->nodes; ex: "daq_response_MAIN_MODULE", "daq_response_DASHBOARD" - # 'var_name': DAQVariable # busses->nodes->variables | busses->nodes->files->contents; ex: "cal_steer_angle", "sdc_main_status", "blue_on", "odometer" - # }, - # "files": { - # 'name': { # busses->nodes->files; ex: "config" - # "contents": [ - # 'var_name' # busses->nodes->files->contents; ex: "blue_on", "odometer" - # ] - # } - # } - # } - # } - # } - - - global b_str - b_str = "Main" - - global data_types - data_types = { - 'uint8_t': np.dtype(' None: - print(f"{bcolors.FAIL}ERROR: {phrase}{bcolors.ENDC}") - -def log_warning(phrase: str) -> None: - log(f"{bcolors.WARNING}WARNING: {phrase}{bcolors.ENDC}") - -def log_success(phrase: str) -> None: - log(f"{bcolors.OKGREEN}{phrase}{bcolors.ENDC}") - -def log(phrase: str) -> None: - global debug_mode - if debug_mode: print(phrase) - -def load_json_config(config_path: str, schema_path: str = None) -> dict: - """ loads config from json and validates with schema """ - config = json.load(open(config_path)) - if (schema_path == None): return config # Bypass schema check - schema = json.load(open(schema_path)) - - # compare with schema - try: - validate(config, schema) - except ValidationError as e: - log_error("Invalid JSON!") - print(e) - sys.exit(1) - - return config - -def clearDictItems(dictionary: dict) -> None: - """Recursively calls clear on items in multidimensional dict""" - for value in dictionary.values(): - if type(value) is dict: - clearDictItems(value) - else: - value.clear() - -def clear_term_line() -> None: - sys.stdout.write('\033[F\033[K') - #sys.stdout.flush() - -# Credit: https://stackoverflow.com/questions/1133857/how-accurate-is-pythons-time-sleep/76554895#76554895 -def high_precision_sleep(duration: float) -> None: - start_time = time.perf_counter() - while True: - elapsed_time = time.perf_counter() - start_time - remaining_time = duration - elapsed_time - if remaining_time <= 0: - break - if remaining_time > 0.02: # Sleep for 5ms if remaining time is greater - time.sleep(max(remaining_time/2, 0.0001)) # Sleep for the remaining time or minimum sleep interval - else: - pass - - -class VoltageDivider(): - def __init__(self, r1: float, r2: float): - self.r1 = float(r1) - self.r2 = float(r2) - self.ratio = (self.r2 / (self.r1 + self.r2)) - - def div(self, input: float) -> float: - return input * self.ratio - - def reverse(self, output: float) -> float: - return output / self.ratio - - -def measure_trip_time(trip_sig: Component, timeout: float, is_falling: bool = False) -> float: - t_start = time.time() - while(trip_sig.state == is_falling): - time.sleep(0.015) - if (t_start + timeout <= time.time()): - log_warning(f"Trip for {trip_sig.name} timed out") - return timeout - t_delt = time.time() - t_start - print(f"Trip time for {trip_sig.name} = {t_delt}s") - return t_delt - - -def measure_trip_thresh( - thresh_sig: Component, - start: float, - stop: float, - step: float, - period_s: float, - trip_sig: Component, - is_falling: bool = False -) -> float: - gain = 1000 - thresh = start - _start = int(start * gain) - _stop = int(stop * gain) - _step = int(step * gain) - thresh_sig.state = start - tripped = False - print(f"Start: {_start} Stop: {_stop} Step: {_step} Gain: {gain}") - for v in range(_start, _stop+_step, _step): - thresh_sig.state = v / gain - time.sleep(period_s) - if (trip_sig.state == (not is_falling)): - thresh = v / gain - tripped = True - break - if (not tripped): - log_warning(f"{trip_sig.name} did not trip at stop of {stop}.") - return stop - else: - return thresh - \ No newline at end of file diff --git a/hil2/__init__.py b/hil2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hil2/action.py b/hil2/action.py new file mode 100644 index 0000000..f28d09f --- /dev/null +++ b/hil2/action.py @@ -0,0 +1,149 @@ +from typing import Optional, Union + +import cantools.database.can.database as cantools_db + + +# Union type representing all possible actions ----------------------------------------# +ActionType = Union[ + "SetDo", + "HiZDo", + "GetDi", + "SetAo", + "HiZAo", + "GetAi", + "SetPot", + "SendCan", + "GetLastCan", + "GetAllCan", + "ClearCan", +] + + +# DO actions --------------------------------------------------------------------------# +class SetDo: + """Action to set a digital output""" + + __match_args__ = ("value",) + + def __init__(self, value: bool): + """ + :param value: The value to set the digital output to (low = false, high = true) + """ + self.value: bool = value + + +class HiZDo: + """Action to set a digital output to high impedance (HiZ)""" + + def __init__(self): + pass + + +# DI actions --------------------------------------------------------------------------# +class GetDi: + """Action to get a digital input""" + + def __init__(self): + pass + + +# AO actions --------------------------------------------------------------------------# +class SetAo: + """Action to set an analog output""" + + __match_args__ = ("value",) + + def __init__(self, value: float): + """ + :param value: The value (in volts) to set the analog output to + """ + self.value: float = value + + +class HiZAo: + """Action to set an analog output to high impedance (HiZ)""" + + def __init__(self): + pass + + +class GetAi: + """Action to get an analog input""" + + def __init__(self): + pass + + +# POT actions -------------------------------------------------------------------------# +class SetPot: + """Action to set a potentiometer""" + + __match_args__ = ("value",) + + def __init__(self, value: float): + """ + :param value: The value (in ohms) to set the potentiometer to + """ + self.value: float = value + + +# CAN actions -------------------------------------------------------------------------# +class SendCan: + """Action to send a CAN message""" + + __match_args__ = ("signal", "data", "can_dbc") + + def __init__(self, signal: str | int, data: dict, can_dbc: cantools_db.Database): + """ + :param signal: The signal name or message ID to send + :param data: The data to include in the CAN message. Will be encoded to bytes + :param can_dbc: The CAN database to use for encoding the message + """ + self.signal: str | int = signal + self.data: dict = data + self.can_dbc: cantools_db.Database = can_dbc + + +class GetLastCan: + """Action to get the last received CAN message""" + + __match_args__ = ("signal", "can_dbc") + + def __init__(self, signal: Optional[str | int], can_dbc: cantools_db.Database): + """ + :param signal: The signal name or message ID to get. If not specified, the last + message will be returned (if any) regardless of the signal/id + :param can_dbc: The CAN database to use for decoding the message + """ + self.signal: Optional[str | int] = signal + self.can_dbc: cantools_db.Database = can_dbc + + +class GetAllCan: + """Action to get all received CAN messages""" + + __match_args__ = ("signal", "can_dbc") + + def __init__(self, signal: Optional[str | int], can_dbc: cantools_db.Database): + """ + :param signal: The signal name or message ID to get. If not specified, all + messages will be returned (if any) regardless of the signal/id + :param can_dbc: The CAN database to use for decoding the messages + """ + self.signal: Optional[str | int] = signal + self.can_dbc: cantools_db.Database = can_dbc + + +class ClearCan: + """Action to clear a CAN message""" + + __match_args__ = ("signal", "can_dbc") + + def __init__(self, signal: Optional[str | int], can_dbc: cantools_db.Database): + """ + :param signal: The signal name or message ID to clear. If not specified, all + messages will be cleared (if any) regardless of the signal/id + :param can_dbc: The CAN database to use for decoding the messages + """ + self.signal: Optional[str | int] = signal + self.can_dbc: cantools_db.Database = can_dbc diff --git a/hil2/can_helper.py b/hil2/can_helper.py new file mode 100644 index 0000000..dd3d11e --- /dev/null +++ b/hil2/can_helper.py @@ -0,0 +1,96 @@ +from typing import Optional + +import os + +import cantools.database.can.database as cantools_db + + +# Helper functions --------------------------------------------------------------------# +def load_can_dbcs(dbc_fpath: str, recursive: bool = False) -> cantools_db.Database: + """ + Scans a folder (and optionally all subfolders) for DBC files and loads them into a + single CAN database. + + :param dbc_fpath: The path to the CAN DBC folder + :param recursive: Whether to search subdirectories recursively (default: False) + :return: The loaded CAN database + """ + db = cantools_db.Database() + + if not dbc_fpath or not os.path.isdir(dbc_fpath): + return db + + if recursive: + for root, _, files in os.walk(dbc_fpath): + for file in files: + if file.endswith(".dbc"): + db.add_dbc_file(os.path.join(root, file)) + else: + for file in os.listdir(dbc_fpath): + if file.endswith(".dbc"): + db.add_dbc_file(os.path.join(dbc_fpath, file)) + + return db + + +# CAN Message struct ------------------------------------------------------------------# +class CanMessage: + """Represents a parsed/decoded CAN message""" + + def __init__(self, signal: str | int, data: dict): + """ + :param signal: The signal name or message ID + :param data: The data contained in the CAN message + """ + + self.signal: str | int = signal + self.data: dict = data + + +# CAN Message Manager class -----------------------------------------------------------# +class CanMessageManager: + """Manages a collection of CAN messages""" + + def __init__(self): + self._messages: list[CanMessage] = [] + + def add_multiple(self, messages: list[CanMessage]) -> None: + """ + :param messages: The list of CAN messages to add + """ + self._messages.extend(messages) + + def get_last(self, signal: Optional[str | int]) -> Optional[CanMessage]: + """ + :param signal: The signal name or message ID to get. If None, the last message + will be returned (if any) regardless of the signal/id + :return: The last CAN message with the specified signal, or None if not found + """ + if signal is None: + return self._messages[-1] if self._messages else None + for msg in reversed(self._messages): + if msg.signal == signal: + return msg + return None + + def get_all(self, signal: Optional[str | int] = None) -> list[CanMessage]: + """ + :param signal: The signal name or message ID to get. If None, all messages will + be returned (if any) regardless of the signal/id + :return: A list of all CAN messages with the specified signal (or all) + """ + return list( + filter(lambda msg: signal is None or msg.signal == signal, self._messages) + ) + + def clear(self, signal: Optional[str | int] = None) -> None: + """ + :param signal: The signal name or message ID to clear. If None, all messages + will be cleared (if any) regardless of the signal/id + """ + if signal is None: + self._messages.clear() + else: + self._messages = list( + filter(lambda msg: msg.signal != signal, self._messages) + ) diff --git a/hil2/commands.py b/hil2/commands.py new file mode 100644 index 0000000..3b21da9 --- /dev/null +++ b/hil2/commands.py @@ -0,0 +1,270 @@ +from typing import Optional + +import logging + +import cantools.database.can.database as cantools_db +import serial + +from . import can_helper +from . import hil_errors +from . import serial_helper + + +# Command constants -------------------------------------------------------------------# +# fmt: off +READ_ID = 0 # command -> READ_ID, id +WRITE_GPIO = 1 # command, pin, value -> [] +HIZ_GPIO = 2 # command, pin -> [] +READ_GPIO = 3 # command, pin -> READ_GPIO, value +WRITE_DAC = 4 # command, pin/offset, value -> [] +HIZ_DAC = 5 # command, pin/offset -> [] +READ_ADC = 6 # command, pin -> READ_ADC, value high, value low +WRITE_POT = 7 # command, pin/offset, value -> [] +SEND_CAN = 8 # command, bus, signal high, signal low, length, data (8 bytes) -> [] +RECV_CAN = 9 # -> CAN_MESSAGE, bus, signal high, + # signal low, length, data (length bytes) +ERROR = 10 # -> ERROR, command +# fmt: on + +SERIAL_RESPONSES = [READ_ID, READ_GPIO, READ_ADC, RECV_CAN, ERROR] + + +# Simple commands ---------------------------------------------------------------------# +def read_id(ser_raw: serial.Serial) -> Optional[int]: + """ + Attempts to read the HIL ID from a device. + Sends a READ_ID command and waits for a response. + + :param ser_raw: The raw serial connection to use (raw Serial object). + :return: The HIL ID if read successfully, None otherwise. + """ + command = [READ_ID] + logging.debug(f"Sending - READ_ID: {command}") + ser_raw.write(bytearray(command)) + try: + response = ser_raw.read(2) # Read command byte and ID byte + if len(response) < 2 or response[0] != READ_ID: + return None + read_hil_id = response[1] + logging.debug(f"Received - READ_ID: {read_hil_id}") + return read_hil_id + except serial.SerialException as e: + logging.error(f"Serial exception occurred: {e}") + return None + + +def write_gpio(ser: serial_helper.ThreadedSerial, pin: int, value: bool) -> None: + """ + Writes a GPIO value to a device. + Sends a WRITE_GPIO command with the specified pin and value. + + :param ser: The serial connection to use. + :param pin: The GPIO pin number. + :param value: The value to write (low = false, high = true). + """ + command = [WRITE_GPIO, pin, int(value)] + logging.debug(f"Sending - WRITE_GPIO: {command}") + ser.write(bytearray(command)) + +def hiZ_gpio(ser: serial_helper.ThreadedSerial, pin: int) -> None: + """ + Set a GPIO pin to high impedance (HiZ). + (This is equivalent to setting the pin as an input.) + Sends a HIZ_GPIO command with the specified pin. + + :param ser: The serial connection to use. + :param pin: The GPIO pin number. + """ + command = [HIZ_GPIO, pin] + logging.debug(f"Sending - HIZ_GPIO: {command}") + ser.write(bytearray(command)) + + +def read_gpio(ser: serial_helper.ThreadedSerial, pin: int) -> bool: + """ + Reads a GPIO value from a device. + Sends a READ_GPIO command with the specified pin and waits for a response. + + :param ser: The serial connection to use. + :param pin: The GPIO pin number. + :return: The value read from the GPIO pin (low = false, high = true). + """ + + command = [READ_GPIO, pin] + logging.debug(f"Sending - READ_GPIO: {command}") + ser.write(bytearray(command)) + match ser.get_readings_with_timeout(READ_GPIO): + case None: + raise hil_errors.SerialError("Failed to read GPIO value, no response") + case [read_value]: + logging.debug(f"Received - READ_GPIO: {read_value}") + return bool(read_value) + case x: + error_msg = f"Failed to read GPIO value, expected 1 byte: {x}" + raise hil_errors.EngineError(error_msg) + + +def write_dac(ser: serial_helper.ThreadedSerial, pin: int, raw_value: int) -> None: + """ + Writes a DAC value to a device. + Sends a WRITE_DAC command with the specified pin and raw value. + + :param ser: The serial connection to use. + :param pin: The DAC pin number. + :param raw_value: The raw value to write (0-255). + """ + command = [WRITE_DAC, pin, raw_value] + logging.debug(f"Sending - WRITE_DAC: {command}") + ser.write(bytearray(command)) + + +def hiZ_dac(ser: serial_helper.ThreadedSerial, pin: int) -> None: + """ + Sets a DAC pin to high impedance mode. + Sends a HIZ_DAC command with the specified pin. + + :param ser: The serial connection to use. + :param pin: The DAC pin number. + """ + command = [HIZ_DAC, pin] + logging.debug(f"Sending - HIZ_DAC: {command}") + ser.write(bytearray(command)) + + +def read_adc(ser: serial_helper.ThreadedSerial, pin: int) -> int: + """ + Reads an ADC value from a device. + Sends a READ_ADC command with the specified pin and waits for a response. + + :param ser: The serial connection to use. + :param pin: The ADC pin number. + :return: The raw ADC value read from the specified pin. + """ + + command = [READ_ADC, pin] + logging.debug(f"Sending - READ_ADC: {command}") + ser.write(bytearray(command)) + match ser.get_readings_with_timeout(READ_ADC): + case None: + raise hil_errors.SerialError("Failed to read ADC value, no response") + case [read_value_high, read_value_low]: + logging.debug(f"Received - READ_ADC: {read_value_high}, {read_value_low}") + return (read_value_high << 8) | read_value_low + case x: + error_msg = f"Failed to read ADC value, expected 2 bytes: {x}" + raise hil_errors.EngineError(error_msg) + + +def write_pot(ser: serial_helper.ThreadedSerial, pin: int, raw_value: int) -> None: + """ + Writes a potentiometer value to a device. + Sends a WRITE_POT command with the specified pin and raw value. + + :param ser: The serial connection to use. + :param pin: The potentiometer pin number. + :param raw_value: The raw value to write (0-255). + """ + command = [WRITE_POT, pin, raw_value] + logging.debug(f"Sending - WRITE_POT: {command}") + ser.write(bytearray(command)) + + +# CAN commands/parsing ----------------------------------------------------------------# +def send_can( + ser: serial_helper.ThreadedSerial, + bus: int, + signal: int, + data: list[int], +) -> None: + """ + Sends a CAN message over the specified bus. + + :param ser: The serial connection to use. + :param bus: The CAN bus number. + :param signal: The CAN signal ID. + :param data: The data to send (up to 8 bytes). When sent, will be padded with zeros. + """ + signal_high = (signal >> 8) & 0xFF + signal_low = signal & 0xFF + length = len(data) + padding = [0] * (8 - len(data)) + command = [SEND_CAN, bus, signal_high, signal_low, length, *data, *padding] + logging.debug(f"Sending - SEND_CAN: {command}") + ser.write(bytearray(command)) + + +def parse_can_messages( + ser: serial_helper.ThreadedSerial, bus: int, can_dbc: cantools_db.Database +) -> list[can_helper.CanMessage]: + """ + Parses received CAN messages from the serial connection for the specified bus. + + :param ser: The serial connection to use. + :param bus: The CAN bus number. + :param can_dbc: The DBC database to use for decoding messages. + :return: A list of parsed CAN messages. + """ + return [ + can_helper.CanMessage(signal, can_dbc.decode_message(signal, data)) + for values in ser.get_parsed_can_messages(bus) + for signal, data in [((values[1] << 8) | values[2], values[4 : 4 + values[3]])] + ] + + +# Serial parsing/spliting -------------------------------------------------------------# +def parse_readings( + readings: list[int], + parsed_readings: dict[int, list[int]], + parsed_can_messages: dict[int, list[list[int]]], +) -> tuple[bool, list[int]]: + """ + Parse the first serial reading if possible. + Does not do conversion, just separates and saves the raw bytes for the corresponding + reading. + + :param readings: The entire list of readings (bytes received from serial) to parse + :param parsed_readings: The dictionary to store parsed readings. + :param parsed_can_messages: The dictionary to store parsed CAN messages. + :return: A tuple: + - A boolean indicating if anything was parsed (and maybe this function + should be called again) + - The remaining unparsed readings. + """ + + logging.debug(f"Current readings to parse: {readings}") + match readings: + case []: + return False, [] + case [cmd, value, *rest] if cmd == READ_ID: + logging.debug(f"Parsed - READ_ID: {value}") + parsed_readings[READ_ID] = [value] + return True, rest + case [cmd, value, *rest] if cmd == READ_GPIO: + logging.debug(f"Parsed - READ_GPIO: {value}") + parsed_readings[READ_GPIO] = [value] + return True, rest + case [cmd, value_high, value_low, *rest] if cmd == READ_ADC: + logging.debug(f"Parsed - READ_ADC: {value_high}, {value_low}") + parsed_readings[READ_ADC] = [value_high, value_low] + return True, rest + case [cmd, bus, signal_high, signal_low, length, *rest] if ( + cmd == RECV_CAN and len(rest) >= length + ): + logging.debug( + f"Parsed - RECV_CAN: {bus}, {signal_high}, {signal_low}, {length}" + ) + data, remaining = rest[:length], rest[length:] + if bus not in parsed_can_messages: + parsed_can_messages[bus] = [] + parsed_can_messages[bus].append( + [bus, signal_high, signal_low, length, *data] + ) + return True, remaining + case [cmd, command, *rest] if cmd == ERROR: + logging.critical(f"Parsed - ERROR for command: {command}. Rest={rest}") + raise hil_errors.SerialError(f"HIL reported error for command {command}") + case [first, *rest] if first not in SERIAL_RESPONSES: + logging.critical(f"Unexpected response: {first}. Rest={rest}") + raise hil_errors.SerialError(f"Unexpected response. Command error: {first}") + case _: + return False, readings diff --git a/hil2/component.py b/hil2/component.py new file mode 100644 index 0000000..0a05305 --- /dev/null +++ b/hil2/component.py @@ -0,0 +1,199 @@ +from typing import Callable, Optional + +from . import can_helper + + +# Shutdownable component interface ----------------------------------------------------# +class ShutdownableComponent: + """Interface for components that need to be 'shutdown' when HIL is stopped""" + + def shutdown(self) -> None: + raise NotImplementedError() + + +# DO ----------------------------------------------------------------------------------# +class DO(ShutdownableComponent): + """Digital Output""" + + def __init__(self, set_fn: Callable[[bool], None], hiZ_fn: Callable[[], None]): + """ + :param set_fn: Function to set the digital output value + :param hiZ_fn: Function to set the digital output to high impedance (HiZ) + """ + self._set_fn: Callable[[bool], None] = set_fn + self._hiZ_fn: Callable[[], None] = hiZ_fn + + def set(self, value: bool) -> None: + """ + Sets the digital output value. + + :param value: The value to set the digital output to (low = false, high = true) + """ + self._set_fn(value) + + def hiZ(self) -> None: + """ + Sets the digital output to high impedance (HiZ) mode. + """ + self._hiZ_fn() + + def shutdown(self) -> None: + """ + Shuts down the digital output by setting it to high impedance (HiZ) mode. + """ + self._hiZ_fn() + + +# DI ----------------------------------------------------------------------------------# +class DI: + """Digital Input""" + + def __init__(self, get_fn: Callable[[], bool]): + """ + :param get_fn: Function to get the digital input value + """ + self._get_fn: Callable[[], bool] = get_fn + + def get(self) -> bool: + """ + Gets the digital input value. + + :return: The digital input value + """ + return self._get_fn() + + +# AO ----------------------------------------------------------------------------------# +class AO(ShutdownableComponent): + """Analog Output""" + + def __init__(self, set_fn: Callable[[float], None], hiZ_fn: Callable[[], None]): + """ + :param set_fn: Function to set the analog output value + :param hiZ_fn: Function to set the analog output to high impedance (HiZ) + """ + self._set_fn: Callable[[float], None] = set_fn + self._hiZ_fn: Callable[[], None] = hiZ_fn + + def set(self, value: float) -> None: + """ + Sets the analog output value. + + :param value: The value to set the analog output to in volts + """ + self._set_fn(value) + + def hiZ(self) -> None: + """ + Sets the analog output to high impedance (HiZ) mode. + """ + self._hiZ_fn() + + def shutdown(self) -> None: + """ + Shuts down the analog output by setting it to high impedance (HiZ) mode. + """ + self._hiZ_fn() + + +# AI ----------------------------------------------------------------------------------# +class AI: + """Analog Input""" + + def __init__(self, get_fn: Callable[[], float]): + """ + :param get_fn: Function to get the analog input value + """ + self._get_fn: Callable[[], float] = get_fn + + def get(self) -> float: + """ + Gets the analog input value. + + :return: The analog input value in volts. + """ + return self._get_fn() + + +# POT ---------------------------------------------------------------------------------# +class POT: + """Potentiometer""" + + def __init__(self, set_fn: Callable[[float], None]): + """ + :param set_fn: Function to set the potentiometer value + """ + self._set_fn: Callable[[float], None] = set_fn + + def set(self, value: float) -> None: + """ + Sets the potentiometer value. + + :param value: The value to set the potentiometer to in ohms + """ + self._set_fn(value) + + +# CAN ---------------------------------------------------------------------------------# +class CAN: + """CAN Bus Interface""" + + def __init__( + self, + send_fn: Callable[[str | int, dict], None], + get_last_fn: Callable[[Optional[str | int]], Optional[can_helper.CanMessage]], + get_all_fn: Callable[[Optional[str | int]], list[can_helper.CanMessage]], + clear_fn: Callable[[Optional[str | int]], None], + ): + """ + :param send_fn: Function to send CAN messages + :param get_last_fn: Function to get the last received CAN message + :param get_all_fn: Function to get all received CAN messages + :param clear_fn: Function to clear CAN messages + """ + self._send_fn: Callable[[str | int, dict], None] = send_fn + self._get_last_fn: Callable[[Optional[str | int]], Optional[dict]] = get_last_fn + self._get_all_fn: Callable[[Optional[str | int]], list[dict]] = get_all_fn + self._clear_fn: Callable[[Optional[str | int]], None] = clear_fn + + def send(self, signal: str | int, data: dict) -> None: + """ + Sends a CAN message. + + :param signal: The signal identifier or message id + :param data: The data to send. Will later be encoded to raw bytes + """ + self._send_fn(signal, data) + + def get_last( + self, signal: Optional[str | int] = None + ) -> Optional[can_helper.CanMessage]: + """ + Gets the last received CAN message. + + :param signal: The signal identifier or message id. If not specified, the last + message for any signal will be returned. + :return: The last received CAN message or None if not found + """ + return self._get_last_fn(signal) + + def get_all( + self, signal: Optional[str | int] = None + ) -> list[can_helper.CanMessage]: + """ + Gets all received CAN messages. + + :param signal: The signal identifier or message id. If not specified, all + messages for any signal will be returned. + :return: A list of all received CAN messages + """ + return self._get_all_fn(signal) + + def clear(self, signal: Optional[str | int] = None) -> None: + """ + Clears the received CAN messages. + + :param signal: The signal identifier or message id. If not specified, all + messages for any signal will be cleared. + """ + self._clear_fn(signal) diff --git a/hil2/dut_cons.py b/hil2/dut_cons.py new file mode 100644 index 0000000..1f2a17c --- /dev/null +++ b/hil2/dut_cons.py @@ -0,0 +1,165 @@ +import json + +from . import hil_errors + + +# HIL DUT Connection ------------------------------------------------------------------# +class HilDutCon: + """The HIL side of a DUT connection""" + + def __init__(self, device: str, port: str): + """ + :param device: The name of the HIL device (ex: 'RearTester') + :param port: The port name on the HIL device (ex: 'DO7') + """ + self.device: str = device + self.port: str = port + + @classmethod + def from_json(cls, hil_dut_con: dict) -> "HilDutCon": + """ + Create a HilDutCon instance from a JSON dictionary. + + :param hil_dut_con: The JSON dictionary representing the HIL DUT connection + :return: A HilDutCon instance + """ + match hil_dut_con: + case {"device": device, "port": port}: + return cls(device, port) + case _: + error_msg = f"Invalid HIL DUT connection configuration: {hil_dut_con}" + raise hil_errors.ConfigurationError(error_msg) + + +# Test DUT Connection -----------------------------------------------------------------# +class DutCon: + """The tested side of a DUT connection""" + + def __init__(self, connector: str, pin: int): + """ + :param connector: The name of the DUT connector (ex: 'J3') + :param pin: The pin number on the DUT connector (ex: 9) + """ + self.connector: str = connector + self.pin: int = pin + + @classmethod + def from_json(cls, dut_con: dict) -> "DutCon": + """ + Create a DutCon instance from a JSON dictionary. + + :param dut_con: The JSON dictionary representing the DUT connection + :return: A DutCon instance + """ + match dut_con: + case {"connector": connector, "pin": pin}: + return cls(connector, pin) + case _: + error_msg = f"Invalid DUT connection configuration: {dut_con}" + raise hil_errors.ConfigurationError(error_msg) + + +# DUT Board Connections ---------------------------------------------------------------# +class DutBoardCons: + def __init__(self, harness_connections: dict[DutCon, HilDutCon]): + """ + :param harness_connections: A dictionary mapping DUT connections to HIL connections + """ + self._harness_connections: dict[DutCon, HilDutCon] = harness_connections + + @classmethod + def from_json(cls, harness_connections: list[dict]) -> "DutBoardCons": + """ + Create a DutBoardCons instance from a JSON dictionary. + + :param harness_connections: A list of dictionaries representing the harness connections + :return: A DutBoardCons instance + """ + parsed_connections: dict[DutCon, HilDutCon] = {} + + for con in harness_connections: + match con: + case {"dut": dut_con, "hil": hil_con}: + parsed_connections[DutCon.from_json(dut_con)] = HilDutCon.from_json( + hil_con + ) + case _: + error_msg = f"Invalid DUT board connection configuration: {con}" + raise hil_errors.ConfigurationError(error_msg) + + return cls(parsed_connections) + + def get_hil_device_connection(self, dut_con: DutCon) -> HilDutCon: + """ + Get the HIL device connection for a given DUT connection. + + :param dut_con: The DUT connection for which to retrieve the HIL device connection + :return: The corresponding HIL device connection + """ + if dut_con in self._harness_connections: + return self._harness_connections[dut_con] + else: + error_msg = ( + "No HIL connection found for DUT connection: " + f"({dut_con.connector}, {dut_con.pin})" + ) + raise hil_errors.ConnectionError(error_msg) + + +# All DUT Connections -----------------------------------------------------------------# +class DutCons: + def __init__(self, dut_connections: dict[str, DutBoardCons]): + """ + :param dut_connections: A dictionary mapping DUT board names to their connections + """ + self._dut_connections: dict[str, DutBoardCons] = dut_connections + + @classmethod + def from_json(cls, test_config_path: str) -> "DutCons": + """ + Create a DutCons instance from a JSON configuration file. + + :param test_config_path: The path to the JSON configuration file + :return: A DutCons instance + """ + with open(test_config_path, "r") as test_config_file: + test_config = json.load(test_config_file) + + board_cons = {} + match test_config: + case {"dut_connections": dut_connections}: + for board_cons in dut_connections: + ... + match board_cons: + case { + "board": board, + "harness_connections": harness_connections, + }: + board_cons[board] = DutBoardCons.from_json( + harness_connections + ) + case _: + error_msg = ( + "Invalid DUT connections configuration: " + f"{board_cons}" + ) + raise hil_errors.ConfigurationError(error_msg) + case _: + # Not an error to have no DUT connections + pass + + return cls(board_cons) + + def get_hil_device_connection(self, board: str, dut_con: DutCon) -> HilDutCon: + """ + Get the HIL device connection for a given DUT connection. + + :param board: The name of the DUT board + :param dut_con: The DUT connection for which to retrieve the HIL device connection + :return: The corresponding HIL device connection + """ + if board in self._dut_connections: + return self._dut_connections[board].get_hil_device_connection(dut_con) + else: + error_msg = f"No HIL connection found for DUT board: {board}" + raise hil_errors.ConnectionError(error_msg) diff --git a/hil2/hil2.py b/hil2/hil2.py new file mode 100644 index 0000000..3599e17 --- /dev/null +++ b/hil2/hil2.py @@ -0,0 +1,366 @@ +from typing import Optional + +import logging +import os + +import cantools.database.can.database as cantools_db + +from . import action +from . import can_helper +from . import component +from . import dut_cons +from . import hil_errors +from . import net_map +from . import test_device + + +class Hil2: + # Init ----------------------------------------------------------------------------# + def __init__( + self, + test_config_path: str, + device_config_fpath: str, + net_map_path: Optional[str] = None, + can_dbc_fpath: Optional[str] = None, + ): + """ + :param test_config_path: The path to the test configuration JSON file + :param device_config_fpath: The path to the device configuration JSON folder + :param net_map_path: The path to the net map (exported from Altium) file + (optional) + :param can_dbc_path: The path to the CAN DBC folder (optional) + """ + self._test_device_manager: test_device.TestDeviceManager = ( + test_device.TestDeviceManager.from_json( + test_config_path, device_config_fpath + ) + ) + self._dut_cons: dut_cons.DutCons = dut_cons.DutCons.from_json(test_config_path) + self._maybe_net_map: Optional[net_map.NetMap] = ( + None if net_map_path is None else net_map.NetMap.from_csv(net_map_path) + ) + self._can_dbc: Optional[cantools_db.Database] = ( + None + if can_dbc_fpath is None + else can_helper.load_can_dbcs(os.path.join(can_dbc_fpath)) + ) + # Components that need to be "shutdown" when HIL2 exits + self._shutdown_components: dict[ + net_map.BoardNet, component.ShutdownableComponent + ] = {} + + # Context -------------------------------------------------------------------------# + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, _traceback): + if exc_type is not None: + logging.critical(f"Hil2 exiting due to exception: {exc_value}") + + self.close() + self._test_device_manager.close() + return False + + # Soft close ----------------------------------------------------------------------# + def close(self) -> None: + """ + 'Shutdown' all the componets (hiZ currently configured outputs) + """ + for comp in self._shutdown_components.values(): + comp.shutdown() + self._shutdown_components.clear() + + # Map -----------------------------------------------------------------------------# + def _map_to_hil_device_con(self, board: str, net: str) -> dut_cons.HilDutCon: + """ + Map a DUT connection (board/net or hil device/port) to a HIL device connection. + If the board is a hil device (ex: 'RearTester'), return the corresponding HIL + device connection. + Otherwise, try to map from the test board and net name ('Dashboard'/'BRK_STAT') + to the HIL device/port it is connected to. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + :return: The corresponding HIL device connection + """ + maybe_hil_dut_con = self._test_device_manager.maybe_hil_con_from_net(board, net) + match (self._maybe_net_map, maybe_hil_dut_con): + case (None, None): + error_msg = ( + "No HIL device connection found for board/net, and no " + "net map available to resolve: " + f"({board}, {net})" + ) + raise hil_errors.ConnectionError(error_msg) + case (net_map, None): + net_map_entry = net_map.get_entry(board, net) + dut_con = dut_cons.DutCon( + net_map_entry.component, net_map_entry.designator + ) + return self._dut_cons.get_hil_device_connection(board, dut_con) + case (None, hil_dut_con): + return hil_dut_con + case _: + error_msg = ( + "Multiple methods to resolve HIL device connection for " + "board/net found; ambiguous: " + f"({board}, {net})" + ) + raise hil_errors.ConnectionError(error_msg) + + # DO ------------------------------------------------------------------------------# + def set_do(self, board: str, net: str, value: bool) -> None: + """ + Sets the digital output value. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + :param value: The value to set the digital output to (low = false, high = true) + """ + _ = self.do(board, net) # Ensure component is registered to shutdown + self._test_device_manager.do_action( + action.SetDo(value), self._map_to_hil_device_con(board, net) + ) + + def hiZ_do(self, board: str, net: str) -> None: + """ + Sets the digital output to high impedance (HiZ) mode. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + """ + _ = self.do(board, net) # Ensure component is registered to shutdown + self._test_device_manager.do_action( + action.HiZDo(), self._map_to_hil_device_con(board, net) + ) + + def do(self, board: str, net: str) -> component.DO: + """ + Create a DO component which has shortcuts to the set and HiZ functions. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + :return: The corresponding DO component + """ + comp = component.DO( + set_fn=lambda value: self.set_do(board, net, value), + hiZ_fn=lambda: self.hiZ_do(board, net), + ) + self._shutdown_components[net_map.BoardNet(board, net)] = comp + return comp + + # DI ------------------------------------------------------------------------------# + def get_di(self, board: str, net: str) -> bool: + """ + Gets the digital input value. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + :return: The digital input value + """ + return self._test_device_manager.do_action( + action.GetDi(), self._map_to_hil_device_con(board, net) + ) + + def di(self, board: str, net: str) -> component.DI: + """ + Create a DI component which has shortcuts to the get function. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + :return: The corresponding DI component + """ + return component.DI(get_fn=lambda: self.get_di(board, net)) + + # AO ------------------------------------------------------------------------------# + def set_ao(self, board: str, net: str, value: float) -> None: + """ + Sets the analog output value. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + :param value: The value to set the analog output to in volts + """ + _ = self.ao(board, net) # Ensure component is registered to shutdown + self._test_device_manager.do_action( + action.SetAo(value), self._map_to_hil_device_con(board, net) + ) + + def hiZ_ao(self, board: str, net: str) -> None: + """ + Sets the analog output to high impedance (HiZ) mode. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + """ + _ = self.ao(board, net) # Ensure component is registered to shutdown + self._test_device_manager.do_action( + action.HiZAo(), self._map_to_hil_device_con(board, net) + ) + + def ao(self, board: str, net: str) -> component.AO: + """ + Create an AO component which has shortcuts to the set and HiZ functions. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + :return: The corresponding AO component + """ + comp = component.AO( + set_fn=lambda value: self.set_ao(board, net, value), + hiZ_fn=lambda: self.hiZ_ao(board, net), + ) + self._shutdown_components[net_map.BoardNet(board, net)] = comp + return comp + + # AI ------------------------------------------------------------------------------# + def get_ai(self, board: str, net: str) -> float: + """ + Gets the analog input value. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + :return: The analog input value in volts. + """ + return self._test_device_manager.do_action( + action.GetAi(), self._map_to_hil_device_con(board, net) + ) + + def ai(self, board: str, net: str) -> component.AI: + """ + Create an AI component which has shortcuts to the get function. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + """ + return component.AI(get_fn=lambda: self.get_ai(board, net)) + + # POT -----------------------------------------------------------------------------# + def set_pot(self, board: str, net: str, value: float) -> None: + """ + Sets the potentiometer value. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + :param value: The value to set the potentiometer to in ohms + """ + self._test_device_manager.do_action( + action.SetPot(value), self._map_to_hil_device_con(board, net) + ) + + def pot(self, board: str, net: str) -> component.POT: + """ + Create a POT component which has shortcuts to the set function. + + :param board: The name of the board (DUT board or HIL device) + :param net: The name of the net (DUT net name or HIL device port) + :return: The corresponding POT component + """ + return component.POT(set_fn=lambda value: self.set_pot(board, net, value)) + + # CAN -----------------------------------------------------------------------------# + def send_can( + self, hil_board: str, can_bus: str, signal: str | int, data: dict + ) -> None: + """ + Send a CAN message out from a HIL device/can bus. + + :param hil_board: The name of the HIL board + :param can_bus: The name of the CAN bus (ex: 'VCAN') + :param signal: The signal identifier or message id + :param data: The data to send. Will be encoded to raw bytes + """ + match self._can_dbc: + case None: + raise hil_errors.ConfigurationError("CAN DBC not configured") + case can_dbc: + self._test_device_manager.do_action( + action.SendCan(signal, data, can_dbc), + self._test_device_manager.maybe_hil_con_from_net( + hil_board, can_bus + ), + ) + + def get_last_can( + self, hil_board: str, can_bus: str, signal: Optional[str | int] = None + ) -> Optional[can_helper.CanMessage]: + """ + Gets the last received CAN message on a HIL device/can bus. + + :param hil_board: The name of the HIL board + :param can_bus: The name of the CAN bus (ex: 'VCAN') + :param signal: The signal identifier or message id. If not specified, the last + message for any signal will be returned. + :return: The last received CAN message or None if not found + """ + match self._can_dbc: + case None: + raise hil_errors.ConfigurationError("CAN DBC not configured") + case can_dbc: + return self._test_device_manager.do_action( + action.GetLastCan(signal, can_dbc), + self._test_device_manager.maybe_hil_con_from_net( + hil_board, can_bus + ), + ) + + def get_all_can( + self, hil_board: str, can_bus: str, signal: Optional[str | int] = None + ) -> list[can_helper.CanMessage]: + """ + Gets all received CAN messages on a HIL device/can bus. + + :param hil_board: The name of the HIL board + :param can_bus: The name of the CAN bus (ex: 'VCAN') + :param signal: The signal identifier or message id. If not specified, all + messages for any signal will be returned. + :return: A list of all received CAN messages + """ + match self._can_dbc: + case None: + raise hil_errors.ConfigurationError("CAN DBC not configured") + case can_dbc: + return self._test_device_manager.do_action( + action.GetAllCan(signal, can_dbc), + self._test_device_manager.maybe_hil_con_from_net( + hil_board, can_bus + ), + ) + + def clear_can( + self, hil_board: str, can_bus: str, signal: Optional[str | int] = None + ) -> None: + """ + Clears the received CAN messages on a HIL device/can bus. + + :param hil_board: The name of the HIL board + :param can_bus: The name of the CAN bus (ex: 'VCAN') + :param signal: The signal identifier or message id. If not specified, all + messages for any signal will be cleared. + """ + match self._can_dbc: + case None: + raise hil_errors.ConfigurationError("CAN DBC not configured") + case can_dbc: + self._test_device_manager.do_action( + action.ClearCan(signal, can_dbc), + self._test_device_manager.maybe_hil_con_from_net( + hil_board, can_bus + ), + ) + + def can(self, hil_board: str, can_bus: str) -> component.CAN: + """ + Gets the CAN component for a specific HIL board and CAN bus which has shortcuts + to the send, get last, get all, and clear functions. + + :param hil_board: The name of the HIL board + :param can_bus: The name of the CAN bus (ex: 'VCAN') + :return: The corresponding CAN component + """ + return component.CAN( + lambda signal, data: self.send_can(hil_board, can_bus, signal, data), + lambda signal: self.get_last_can(hil_board, can_bus, signal), + lambda signal: self.get_all_can(hil_board, can_bus, signal), + lambda signal: self.clear_can(hil_board, can_bus, signal), + ) diff --git a/hil2/hil_errors.py b/hil2/hil_errors.py new file mode 100644 index 0000000..2d84798 --- /dev/null +++ b/hil2/hil_errors.py @@ -0,0 +1,40 @@ +class SerialError(Exception): + """ + Error representing something going wrong relating to the serial + connection/commands/responses + """ + + pass + + +class EngineError(Exception): + """ + Error representing something going wrong relating to the HIL2 engine + """ + + pass + + +class ConfigurationError(Exception): + """ + Error representing something wrong with the configuration + """ + + pass + + +class ConnectionError(Exception): + """ + Error representing something going wrong when trying to map between HIL and DUT + connections + """ + + pass + + +class RangeError(Exception): + """ + Error representing a value is out of range + """ + + pass diff --git a/hil2/net_map.py b/hil2/net_map.py new file mode 100644 index 0000000..58e770f --- /dev/null +++ b/hil2/net_map.py @@ -0,0 +1,100 @@ +import csv + +from . import hil_errors + + +# Board net pairing -------------------------------------------------------------------# +class BoardNet: + """ + Represents a board/net combination in the net map (ex: 'Dashboard/BRK_STAT'). + Can be used as a key in a dictionary. + """ + + def __init__(self, board: str, net: str): + """ + :param board: The name of the board (ex: 'Dashboard') + :param net: The name of the net (ex: 'BRK_STAT') + """ + self.board: str = board + self.net: str = net + + def __hash__(self): + return hash((self.board, self.net)) + + def __eq__(self, other): + return self.board == other.board and self.net == other.net + + def __neq__(self, other): + return not (self == other) + + +# CSV entry ---------------------------------------------------------------------------# +class NetMapEntry: + """Represents a row in the net map CSV file.""" + + def __init__(self, board: str, net: str, component: str, designator: int): + """ + :param board: The name of the board (ex: 'Dashboard') + :param net: The name of the net (ex: 'BRK_STAT') + :param component: The name of the component (ex: 'J3') + :param designator: The designator number (ex: 9) + """ + self.board = board + self.net = net + self.component = component + self.designator = designator + + +# Net map -----------------------------------------------------------------------------# +class NetMap: + def __init__(self, entries: dict[BoardNet, NetMapEntry]): + """ + :param entries: A dictionary mapping board/net combinations to their net map + entries + """ + self._entries: dict[BoardNet, NetMapEntry] = entries + + def get_entry(self, board: str, net: str) -> NetMapEntry: + """ + Retrieves a net map entry by board and net name. + + :param board: The name of the board (ex: 'Dashboard') + :param net: The name of the net (ex: 'BRK_STAT') + :return: The net map entry for the specified board and net + """ + board_net = BoardNet(board, net) + if board_net in self._entries: + return self._entries[board_net] + else: + raise hil_errors.ConnectionError(f"No net map entry found for: {board_net}") + + @classmethod + def from_csv(cls, file_path: str) -> "NetMap": + """ + Creates a NetMap instance from a CSV file. + + :param file_path: The path to the CSV file + :return: A NetMap instance + """ + entries = {} + with open(file_path, newline="", encoding="utf-8") as net_map_file: + reader = csv.DictReader(net_map_file) + for row in reader: + match row: + case { + "Board": board, + "Net": net, + "Component": component, + "Designator": designator_str, + }: + entry = NetMapEntry( + board=board, + net=net, + component=component, + designator=int(designator_str), + ) + board_net = BoardNet(entry.board, entry.net) + entries[board_net] = entry + case _: + raise hil_errors.NetMapParseError(f"Invalid net map row: {row}") + return cls(entries) diff --git a/hil2/serial_helper.py b/hil2/serial_helper.py new file mode 100644 index 0000000..1572450 --- /dev/null +++ b/hil2/serial_helper.py @@ -0,0 +1,209 @@ +from typing import Optional + +import logging +import threading +import time + +import serial +import serial.tools.list_ports + +from . import commands +from . import hil_errors + +SERIAL_BAUDRATE = 115200 +SERIAL_TIMEOUT = 0.1 +SERIAL_RETRIES = 5 + +GET_TIMEOUT = 0.1 +SLEEP_INTERVAL = 0.01 + + +# Discover ----------------------------------------------------------------------------# +def discover_devices(hil_ids: list[int]) -> dict[int, serial.Serial]: + """ + Attempts to find HIL devices by sending an identification command to each serial + port. + + :param hil_ids: A list of expected HIL device IDs + :return: A dictionary mapping discovered HIL device IDs to their serial connections + """ + + devices = {} + + com_ports = [ + port.device + for port in serial.tools.list_ports.comports() + if "USB Serial" in port.description + ] + + # For every serial port, try to see if it is a HIL device + for cp in com_ports: + logging.debug(f"Trying to discover HIL device on port {cp}") + serial_con = serial.Serial( + cp, + SERIAL_BAUDRATE, + timeout=SERIAL_TIMEOUT, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + xonxoff=0, + rtscts=0, + ) + serial_con.dtr = False + time.sleep(1) + serial_con.reset_input_buffer() + serial_con.dtr = True + + # Need to give a little time + for _ in range(SERIAL_RETRIES): + read_hil_id = commands.read_id(serial_con) + if read_hil_id is not None and read_hil_id in hil_ids: + devices[read_hil_id] = serial_con + logging.info( + f"Discovered HIL device with ID {read_hil_id} on port {cp}" + ) + break + time.sleep(1) + else: + # If it is not a HIL device, close it + serial_con.close() + + # Check we found all devices + for hil_id in hil_ids: + if hil_id not in devices: + error_msg = f"Failed to discover HIL device with ID {hil_id} on any port" + raise hil_errors.SerialError(error_msg) + + return devices + + +# Threaded serial ---------------------------------------------------------------------# +class ThreadedSerial: + """ + A class that handles serial communication in a separate thread. + This is needed because CAN messages are received asynchronously as opposed to + command and response. + """ + + def __init__(self, serial_con: serial.Serial, stop_event: threading.Event): + """ + :param serial_con: The serial connection to the HIL device + :param stop_event: The event used to signal the thread to stop + """ + self.serial_con: serial.Serial = serial_con + self.stop_event: threading.Event = stop_event + + # Raw readings from the serial port + self.readings: list[int] = [] + + # Parsed readings. The key is the command (ex: READ_GPIO) and the value is the + # list of bytes + self.parsed_readings: dict[int, list[int]] = {} + # Parsed CAN messages. The key is the bus number, the value is a list of the + # list of bytes for each message + self.parsed_can_messages: dict[int, list[list[int]]] = {} + + # Lock for synchronizing access to shared resources + self.lock = threading.Lock() + + def write(self, data: bytes) -> None: + """ + Write data to the serial port. Safe to be called from another thread. + + :param data: The data to write to the serial port + """ + self.serial_con.write(data) + + def _read(self): + """ + Attempt to read a single byte from the serial port. + """ + read_data = self.serial_con.read(1) + if len(read_data) < 1: + return + value = int.from_bytes(read_data, "big") + self.readings.append(value) + + def _process_readings(self): + """ + Attempt to process read bytes. + """ + processed = True + while processed: + # If something was processed, try to process again + processed, self.readings = commands.parse_readings( + self.readings, self.parsed_readings, self.parsed_can_messages + ) + + def _get_readings(self, command: int) -> Optional[list[int]]: + """ + Get the readings for a specific command. Safe to be called from a different thread. + + :param command: The command to get readings for (used as key) + :return: The readings for the command, or None if not found + """ + with self.lock: + val = self.parsed_readings.pop(command, None) + return val + + def get_readings_with_timeout( + self, + command: int, + timeout: float = GET_TIMEOUT, + sleep_interval: float = SLEEP_INTERVAL, + ) -> Optional[list[int]]: + """ + Get the readings for a command, with a delay. + Retries the reading at regular intervals until the timeout is reached. + Safe to be called from a different thread. + + :param command: The command to get readings for (used as key) + :param timeout: The maximum time to wait for readings (seconds) + :param sleep_interval: The time to wait between retries (seconds) + :return: The readings for the command, or None if not found + """ + + deadline = time.time() + timeout + while time.time() < deadline: + if (reading := self._get_readings(command)) is not None: + return reading + time.sleep(sleep_interval) + return None + + def get_parsed_can_messages(self, bus: int) -> list[list[int]]: + """ + Get the parsed CAN messages for a specific bus. + Safe to be called from a different thread. + + :param bus: The bus number to get messages for + :return: A list of parsed (but not decoded) CAN messages for the bus + """ + with self.lock: + return self.parsed_can_messages.pop(bus, []) + + def stop(self): + """ + Stop the serial helper thread. + Safe to be called from a different thread. + """ + self.stop_event.set() + + def _close(self): + """ + Close the serial connection. + """ + self.serial_con.close() + + def run(self): + """ + Run the serial helper thread. + Constantly tries to read from the serial port and process the readings. + Should be run in a separate thread. + """ + while not self.stop_event.is_set(): + self._read() + if len(self.readings) > 0: + with self.lock: + self._process_readings() + + self._close() diff --git a/hil2/test_device.py b/hil2/test_device.py new file mode 100644 index 0000000..889d6ff --- /dev/null +++ b/hil2/test_device.py @@ -0,0 +1,729 @@ +from typing import Any, Optional + +import json +import os +import threading + +import cantools.database.can.database as cantools_db + +from . import action +from . import can_helper +from . import commands +from . import dut_cons +from . import hil_errors +from . import serial_helper + + +# Peripheral configuration ------------------------------------------------------------# +class AdcConfig: + """Configuration for an ADC (Analog-to-Digital Converter).""" + + def __init__(self, adc_config: dict): + """ + :param adc_config: The ADC configuration dictionary + """ + match adc_config: + # PCB + case { + "bit_resolution": br, + "adc_reference_v": ar, + "5v_reference_v": v5r, + "24v_reference_v": v24r, + }: + self.bit_resolution: int = br + self.adc_reference_v: float = ar + self.five_v_reference_v: Optional[float] = v5r + self.twenty_four_v_reference_v: Optional[float] = v24r + # Breadboard + case { + "bit_resolution": br, + "adc_reference_v": ar, + }: + self.bit_resolution: int = br + self.adc_reference_v: float = ar + self.five_v_reference_v: Optional[float] = None + self.twenty_four_v_reference_v: Optional[float] = None + case _: + raise hil_errors.ConfigurationError("Invalid ADC configuration") + + def raw_to_v(self, raw_value: int) -> float: + """ + Convert a raw ADC value to a voltage. + + :param raw_value: The raw ADC value to convert + :return: The converted voltage value + """ + if raw_value < 0 or raw_value > (2**self.bit_resolution - 1): + raise hil_errors.RangeError(f"ADC raw value {raw_value} out of range") + return (raw_value / (2**self.bit_resolution - 1)) * self.adc_reference_v + + def raw_to_5v(self, raw_value: int) -> float: + """ + Convert a raw ADC value to a voltage using the 5V reference. + If on a PCB, AI5 means it when through a voltage divider. + + :param raw_value: The raw ADC value to convert + :return: The converted voltage value + """ + match self.five_v_reference_v: + case None: + error_msg = "5V reference voltage not configured" + raise hil_errors.ConfigurationError(error_msg) + case v5r: + return (self.raw_to_v(raw_value) / v5r) * 5.0 + + def raw_to_24v(self, raw_value: int) -> float: + """ + Convert a raw ADC value to a voltage using the 24V reference. + If on a PCB, AI24 means it when through a voltage divider. + + :param raw_value: The raw ADC value to convert + :return: The converted voltage value + """ + match self.twenty_four_v_reference_v: + case None: + error_msg = "24V reference voltage not configured" + raise hil_errors.ConfigurationError(error_msg) + case v24r: + return (self.raw_to_v(raw_value) / v24r) * 24.0 + + +class DacConfig: + """Configuration for a DAC (Digital-to-Analog Converter).""" + + def __init__(self, dac_config: dict): + """ + :param dac_config: The DAC configuration dictionary + """ + match dac_config: + case {"bit_resolution": br, "reference_v": rv}: + self.bit_resolution = br + self.reference_v = rv + case _: + raise hil_errors.ConfigurationError("Invalid DAC configuration") + + def v_to_raw(self, value: float) -> int: + """ + Convert a voltage value to a raw DAC value. + + :param value: The voltage value to convert + :return: The converted raw DAC value + """ + if value < 0 or value > self.reference_v: + raise hil_errors.RangeError(f"DAC value {value} out of range") + return int((value / self.reference_v) * (2**self.bit_resolution - 1)) + + +class PotConfig: + """Configuration for a POT (Potentiometer).""" + + def __init__(self, pot_config: dict): + """ + :param pot_config: The POT configuration dictionary + """ + match pot_config: + case {"bit_resolution": br, "reference_ohms": r, "wiper_ohms": w}: + self.bit_resolution = br + self.reference_ohms = r + self.wiper_ohms = w + case _: + raise hil_errors.ConfigurationError("Invalid POT configuration") + + def ohms_to_raw(self, value: float) -> int: + """ + Convert an ohm value to a raw POT value. + + :param value: The ohm value to convert + :return: The converted raw POT value + """ + if value < self.wiper_ohms or value > self.reference_ohms + self.wiper_ohms: + raise hil_errors.RangeError(f"POT value {value} out of range") + steps = self.bit_resolution**2 - 1 + return int((steps * (value - self.wiper_ohms)) / self.reference_ohms) + + +# Interface configuration -------------------------------------------------------------# +class Port: + """Configuration for a port.""" + + def __init__(self, port: dict): + """ + :param port: The port configuration dictionary + """ + match port: + case {"name": name, "port": port, "mode": mode}: + self.name: str = name + self.port: int = port + self.mode: str = mode + case _: + raise hil_errors.ConfigurationError("Invalid Port configuration") + + +class Mux: + """Configuration for a MUX (Multiplexer).""" + + def __init__(self, mux: dict): + """ + :param mux: The MUX configuration dictionary + """ + match mux: + case { + "name": name, + "mode": mode, + "select_ports": select_ports, + "port": port, + }: + self.name: str = name + self.mode: str = mode + self.select_ports: list[int] = select_ports + self.port: int = port + case _: + raise hil_errors.ConfigurationError("Invalid Mux configuration") + + def select_from_name(self, name: str) -> Optional["MuxSelect"]: + """ + Attempt to see if self is the base mux that is being referenced. + For example: DMUX_6 means DMUX is the base mux and 6 is the select line. + + :param name: The name of the MUX select line (ex: DMUX_6) + :return: The MuxSelect instance if found, None otherwise + """ + name_parts = name.rsplit("_", 1) + if len(name_parts) < 2: + return None + if name_parts[0] != self.name: + return None + try: + return MuxSelect(self, int(name_parts[1])) + except ValueError: + return None + + +class MuxSelect: + """Represents a selected MUX (Multiplexer) line.""" + + def __init__(self, mux: Mux, select: int): + """ + :param mux: The MUX instance + :param select: The selected line number (0 indexed) + """ + self.mux: Mux = mux + self.select: int = select + + +class CanBus: + """Configuration for a CAN (Controller Area Network) bus.""" + + def __init__(self, can_bus: dict): + """ + :param can_bus: The CAN bus configuration dictionary + """ + match can_bus: + case {"name": name, "bus": bus}: + self.name: str = name + self.bus: int = bus + case _: + raise hil_errors.ConfigurationError("Invalid CAN Bus configuration") + + +# Test device -------------------------------------------------------------------------# +class TestDevice: + # Init ----------------------------------------------------------------------------# + def __init__( + self, + hil_id: int, + name: str, + ports: dict[str, Port], + muxs: dict[str, Mux], + can_busses: dict[str, CanBus], + adc_config: AdcConfig, + dac_config: Optional[DacConfig], + pot_config: Optional[PotConfig], + ): + """ + :param hil_id: The HIL ID of the device + :param name: The name of the device + :param ports: The port configurations + :param muxs: The MUX configurations + :param can_busses: The CAN bus configurations + :param adc_config: The ADC configuration + :param dac_config: The DAC configuration + :param pot_config: The potentiometer configuration + """ + self.hil_id: int = hil_id + self._name: str = name + self._ports: dict[str, Port] = ports + self._muxs: dict[str, Mux] = muxs + self._can_busses: dict[str, CanBus] = can_busses + self._adc_config: AdcConfig = adc_config + self._dac_config: Optional[DacConfig] = dac_config + self._pot_config: Optional[PotConfig] = pot_config + + # Please use set_serial() to set the serial! + self._ser: Optional[serial_helper.ThreadedSerial] = None + + self.device_can_busses: dict[int, can_helper.CanMessageManager] = dict( + map( + lambda c: (c.bus, can_helper.CanMessageManager()), + self._can_busses.values(), + ) + ) + + @classmethod + def from_json(cls, hil_id: int, name: str, device_config_path: str): + """ + Create a TestDevice instance from a JSON configuration file. + + :param hil_id: The HIL ID of the device + :param name: The name of the device + :param device_config_path: The path to the device configuration JSON file + """ + with open(device_config_path, "r") as device_config_path: + device_config = json.load(device_config_path) + + ports = dict( + map(lambda p: (p.get("name"), Port(p)), device_config.get("ports", [])) + ) + muxs = dict( + map(lambda m: (m.get("name"), Mux(m)), device_config.get("muxs", [])) + ) + can_busses = dict( + map(lambda c: (c.get("name"), CanBus(c)), device_config.get("can", [])) + ) + + match device_config: + case {"adc_config": adc_config_data}: + adc_config = AdcConfig(adc_config_data) + case _: + error_msg = f"ADC configuration missing for device {name}" + raise hil_errors.ConfigurationError(error_msg) + + match device_config: + case {"dac_config": dac_config_data}: + dac_config = DacConfig(dac_config_data) + case _: + dac_config = None + + match device_config: + case {"pot_config": pot_config_data}: + pot_config = PotConfig(pot_config_data) + case _: + pot_config = None + + return cls( + hil_id, + name, + ports, + muxs, + can_busses, + adc_config, + dac_config, + pot_config, + ) + + def set_serial(self, ser: serial_helper.ThreadedSerial) -> None: + """ + Set the serial connection for the TestDevice. + The caller is responsible for starting the serial connection's thread. + + :param ser: The serial connection to set + """ + self._ser = ser + + def close(self) -> None: + """ + Close the serial connection for the TestDevice. + """ + match self._ser: + case None: + error_msg = f"Cannot close TestDevice {self._name}: serial not set" + raise hil_errors.EngineError(error_msg) + case ser: + ser.stop() + + # Command handling ----------------------------------------------------------------# + def _select_mux(self, mux_select: MuxSelect) -> None: + """ + Select a MUX (Multiplexer) line. + + :param mux_select: The MUX selection information + """ + for i, p in enumerate(mux_select.mux.select_ports): + select_bit = True if (mux_select.select & (1 << i)) else False + self._set_do(p, select_bit) + + def _set_do(self, pin: int, value: bool) -> None: + """ + Set a digital output (DO) pin. + + :param pin: The pin number to set + :param value: The value to set the pin to (low = False, high = True) + """ + match self._ser: + case None: + error_msg = f"Cannot set DO on TestDevice {self._name}: serial not set" + raise hil_errors.EngineError(error_msg) + case ser: + commands.write_gpio(ser, pin, value) + + def _hiZ_do(self, pin: int) -> None: + """ + Set a digital output (DO) pin to high impedance (HiZ). + + :param pin: The pin number to set + """ + match self._ser: + case None: + error_msg = ( + f"Cannot set HiZ DO on TestDevice {self._name}: serial not set" + ) + raise hil_errors.EngineError(error_msg) + case ser: + commands.hiZ_gpio(ser, pin) + + def _get_di(self, pin: int) -> bool: + """ + Get the digital input (DI) state of a pin. + + :param pin: The pin number to read + :return: The state of the pin (True for high, False for low) + """ + match self._ser: + case None: + error_msg = f"Cannot get DI on TestDevice {self._name}: serial not set" + raise hil_errors.EngineError(error_msg) + case ser: + return commands.read_gpio(ser, pin) + + def _set_ao(self, pin: int, value: float) -> None: + """ + Set an analog output (AO) pin after converting the volts value to raw. + + :param pin: The pin/offset number to set + :param value: The voltage value to set the pin to + """ + match (self._ser, self._dac_config): + case (ser, dac_config) if ser is not None and dac_config is not None: + raw_value = dac_config.v_to_raw(value) + commands.write_dac(ser, pin, raw_value) + case _: + error_msg = f"Cannot set AO on TestDevice {self._name}: serial or DAC config not set" + raise hil_errors.EngineError(error_msg) + + def _hiZ_ao(self, pin: int) -> None: + """ + Set an analog output (AO) pin to high impedance (HiZ). + + :param pin: The pin/offset number to set + """ + match self._ser: + case None: + error_msg = ( + f"Cannot set HiZ AO on TestDevice {self._name}: serial not set" + ) + raise hil_errors.EngineError(error_msg) + case ser: + commands.hiZ_dac(ser, pin) + + def _get_ai(self, pin: int, mode: str) -> float: + """ + Get an analog input (AI) reading from a pin and convert the reading to volts. + + :param pin: The pin number to read + :param mode: The mode to use for the reading (AI5, AI24, or AI) + :return: The voltage value read from the pin + """ + match self._ser: + case None: + error_msg = f"Cannot get AI on TestDevice {self._name}: serial not set" + raise hil_errors.EngineError(error_msg) + case ser: + raw_value = commands.read_adc(ser, pin) + + if mode == "AI5": + return self._adc_config.raw_to_5v(raw_value) + elif mode == "AI24": + return self._adc_config.raw_to_24v(raw_value) + elif mode == "AI": + return self._adc_config.raw_to_v(raw_value) + else: + raise ValueError(f"Unsupported AI mode: {mode}") + + def _set_pot(self, pin: int, value: float) -> None: + """ + Set a potentiometer (POT) pin after converting the ohms value to raw. + + :param pin: The pin/offset to set + :param value: The resistance value to set the pin to (in ohms) + """ + match (self._ser, self._pot_config): + case (ser, pot_config) if ser is not None and pot_config is not None: + raw_value = pot_config.ohms_to_raw(value) + commands.write_pot(ser, pin, raw_value) + case _: + error_msg = f"Cannot set POT on TestDevice {self._name}: serial not set" + raise hil_errors.EngineError(error_msg) + + def _update_can_messages(self, bus: int, can_dbc: cantools_db.Database) -> None: + """ + Update the CAN message store by decoding the saved parsed can messages from the Serial. + + :param bus: The CAN bus to update + :param can_dbc: The CAN database to use for decoding + """ + match self._ser: + case None: + error_msg = ( + f"Cannot update CAN messages on TestDevice {self._name}: " + "serial not set" + ) + raise hil_errors.EngineError(error_msg) + case ser: + self.device_can_busses[bus].add_multiple( + commands.parse_can_messages(ser, bus, can_dbc) + ) + + def _send_can( + self, bus: int, signal: str | int, data: dict, can_dbc: cantools_db.Database + ) -> None: + """ + Send a CAN message on the specified bus. + + :param bus: The CAN bus to send the message on + :param signal: The CAN signal to send + :param data: The data to include in the CAN message + :param can_dbc: The CAN database to use for encoding + """ + raw_data = list(can_dbc.encode_message(signal, data)) + msg_id = can_dbc.get_message_by_name(signal).frame_id + + match self._ser: + case None: + error_msg = ( + f"Cannot send CAN message on TestDevice {self._name}: " + "serial not set" + ) + raise hil_errors.EngineError(error_msg) + case ser: + commands.send_can(ser, bus, msg_id, raw_data) + + # Action --------------------------------------------------------------------------# + def do_action(self, action_type: action.ActionType, port: str) -> Any: + """ + Perform a HIL action on a specific port. + + :param action_type: The type of action to perform (+ includes all needed info) + :param port: The HIL port to perform the action on + :return: depends on the action type + """ + + maybe_port = self._ports.get(port, None) + maybe_mux_select = next( + ( + val + for m in self._muxs.values() + if (val := m.select_from_name(port)) is not None + ), + None, + ) + maybe_can_bus = self._can_busses.get(port, None) + + match (action_type, maybe_port, maybe_mux_select, maybe_can_bus): + # Set DO + direct port + case (action.SetDo(value), mp, _, _) if mp is not None and mp.mode == "DO": + self._set_do(mp.port, value) + # Set DO + mux select + case (action.SetDo(value), _, mms, _) if ( + mms is not None and mms.mux.mode == "DO" + ): + self._select_mux(mms) + self._set_do(mms.mux.port, value) + # HiZ DO + direct port + case (action.HiZDo(), mp, _, _) if mp is not None and mp.mode == "DO": + self._hiZ_do(mp.port) + # HiZ DO + mux select + case (action.HiZDo(), _, mms, _) if ( + mms is not None and mms.mux.mode == "DO" + ): + self._select_mux(mms) + self._hiZ_do(mms.mux.port) + # Get DI + direct port + case (action.GetDi(), mp, _, _) if mp is not None and mp.mode == "DI": + return self._get_di(mp.port) + # Get DI + mux select + case (action.GetDi(), _, mms, _) if ( + mms is not None and mms.mux.mode == "DI" + ): + self._select_mux(mms) + return self._get_di(mms.mux.port) + # Set AO + direct port + case (action.SetAo(value), mp, _, _) if mp is not None and mp.mode == "AO": + self._set_ao(mp.port, value) + # HiZ AO + direct port + case (action.HiZAo(), mp, _, _) if mp is not None and mp.mode == "AO": + self._hiZ_ao(mp.port) + # Get AI + direct port + case (action.GetAi(), mp, _, _) if mp is not None and mp.mode.startswith( + "AI" + ): + return self._get_ai(mp.port, mp.mode) + # Get AI + mux select + case ( + action.GetAi(), + _, + mms, + _, + ) if mms is not None and mms.mux.mode.startswith("AI"): + self._select_mux(mms) + return self._get_ai(mms.mux.port, mms.mux.mode) + # Set Pot + direct port + case (action.SetPot(value), mp, _, _) if ( + mp is not None and mp.mode == "POT" + ): + self._set_pot(mp.port, value) + # Send CAN msg + can bus name + case (action.SendCan(signal, data, can_dbc), _, _, mcb) if mcb is not None: + self._update_can_messages(mcb.bus, can_dbc) + self._send_can(mcb.bus, signal, data, can_dbc) + # Get last CAN msg + can bus name + case (action.GetLastCan(signal, can_dbc), _, _, mcb) if mcb is not None: + self._update_can_messages(mcb.bus, can_dbc) + return self.device_can_busses[mcb.bus].get_last(signal) + # Get all CAN msgs + can bus name + case (action.GetAllCan(signal, can_dbc), _, _, mcb) if mcb is not None: + self._update_can_messages(mcb.bus, can_dbc) + return self.device_can_busses[mcb.bus].get_all(signal) + # Clear CAN msgs + can bus name + case (action.ClearCan(signal, can_dbc), _, _, mcb) if mcb is not None: + self._update_can_messages(mcb.bus, can_dbc) + self.device_can_busses[mcb.bus].clear(signal) + # Unsupported action + case _: + error_msg = ( + f"Action {type(action)} not supported for " + f"port {port} on device {self._name}" + ) + raise hil_errors.EngineError(error_msg) + + +# Test device manager -----------------------------------------------------------------# +class TestDeviceManager: + """ + Manages test devices for HIL (Hardware-in-the-Loop) simulation. + """ + + def __init__(self, test_devices: dict[str, TestDevice]): + """ + :param test_devices: A dictionary of test devices managed by this manager. + key = device name, value = TestDevice instance + """ + self._test_devices: dict[str, TestDevice] = test_devices + + @classmethod + def from_json( + cls, test_config_path: str, device_config_fpath: str + ) -> "TestDeviceManager": + """ + Create a TestDeviceManager instance from JSON configuration files. + Is responsible for starting all of the ThreadedSerial instances. + + :param test_config_path: The path to the test configuration JSON file. + :param device_config_fpath: The file path to the directory containing device + configuration files. + :return: A TestDeviceManager instance. + """ + + with open(test_config_path, "r") as test_config_file: + test_config = json.load(test_config_file) + + hil_ids = [] + stop_events = {} + test_devices = {} + match test_config: + case {"hil_devices": hil_devices}: + for device in hil_devices: + match device: + case { + "id": hil_id, + "name": name, + "config": config_file_name, + } if ( + not hil_id in hil_ids + ): + hil_ids.append(hil_id) + stop_events[hil_id] = threading.Event() + test_devices[name] = TestDevice.from_json( + hil_id, + name, + os.path.join(device_config_fpath, config_file_name), + ) + case {"id": hil_id}: + error_msg = f"Duplicate HIL device ID found: {hil_id}" + raise hil_errors.ConfigurationError(error_msg) + case _: + error_msg = f"Invalid HIL device configuration: {device}" + raise hil_errors.ConfigurationError(error_msg) + case _: + error_msg = "Invalid test configuration: missing 'hil_devices' key" + raise hil_errors.ConfigurationError(error_msg) + + hil_devices = serial_helper.discover_devices(hil_ids) + + sers = dict( + map( + lambda hil_id: ( + hil_id, + serial_helper.ThreadedSerial( + hil_devices[hil_id], stop_events[hil_id] + ), + ), + hil_ids, + ) + ) + for test_device in test_devices.values(): + ser = sers[test_device.hil_id] + t = threading.Thread(target=ser.run) + t.start() + test_device.set_serial(ser) + + return cls(test_devices) + + def maybe_hil_con_from_net( + self, board: str, net: str + ) -> Optional[dut_cons.HilDutCon]: + """ + Check to see if a board is a HIL device. + + :param board: The name of the board to check. + :param net: The network to use for the connection. + :return: A HilDutCon instance if the board is a HIL device, None otherwise. + """ + if board in self._test_devices: + return dut_cons.HilDutCon(board, net) + else: + return None + + def do_action( + self, action_type: action.ActionType, hil_dut_con: dut_cons.HilDutCon + ) -> Any: + """ + Perform an action on a HIL device. + + :param action_type: The type of action to perform. + :param hil_dut_con: The HIL DUT connection information. + :return: The result of the action (if any). + """ + if hil_dut_con.device in self._test_devices: + return self._test_devices[hil_dut_con.device].do_action( + action_type, hil_dut_con.port + ) + else: + error_msg = f"Device {hil_dut_con.device} not found" + raise hil_errors.ConnectionError(error_msg) + + def close(self) -> None: + """ + Close all HIL devices. + """ + for device in self._test_devices.values(): + device.close() diff --git a/hil_params.json b/hil_params.json deleted file mode 100644 index 94bae44..0000000 --- a/hil_params.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "firmware_path": "/home/millankumar/Documents/PER/firmware", - "default_ip": "192.168.10.40", - "the wifi ip": "ubuntu.local", - "the ubiquiti ip:":"192.168.10.40", - "debug": true -} \ No newline at end of file diff --git a/mk_assert/__init__.py b/mk_assert/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mk_assert/mk_assert.py b/mk_assert/mk_assert.py new file mode 100644 index 0000000..cf6ec11 --- /dev/null +++ b/mk_assert/mk_assert.py @@ -0,0 +1,209 @@ +from typing import Callable, Any, Optional + +import logging + +from . import print_helper + + +# Global test state -------------------------------------------------------------------# +g_tests: list["TestFn"] = [] +g_active_test: Optional["ActiveTestContext"] = None +g_setup_fn: Optional[Callable[[], None]] = None +g_teardown_fn: Optional[Callable[[], None]] = None + + +class TestFn: + def __init__( + self, func: Callable[..., None], args: tuple[Any, ...], kwargs: dict[str, Any] + ): + """ + :param func: The test function to be called + :param args: Positional arguments to pass to the test function + :param kwargs: Keyword arguments to pass to the test function + """ + self.func: Callable[..., None] = func + self.args: tuple[Any, ...] = args + self.kwargs: dict[str, Any] = kwargs + + def run(self) -> None: + """ + Run the test function with the stored arguments. + """ + return self.func(*self.args, **self.kwargs) + + +class ActiveTestContext: + def __init__(self, test_fn: TestFn): + """ + :param test_fn: The TestFn instance representing the active test. + """ + self._test_fn = test_fn + self.passed: int = 0 + self.failed: int = 0 + + def __enter__(self): + global g_active_test + g_active_test = self + return self + + def __exit__(self, exc_type, exc_value, _traceback): + global g_active_test + g_active_test = None + + if exc_type is not None: + self.failure() + logging.critical( + f"Test '{self._test_fn.func.__name__}' raised an exception: {exc_value}" + ) + + return False + + def success(self): + """ + Record a successful assertion. + """ + self.passed += 1 + + def failure(self): + """ + Record a failed assertion. + """ + self.failed += 1 + + +def set_setup_fn(setup_fn: Callable[[], None]) -> None: + """ + Set a global setup function to be called before each test. + + :param setup_fn: The setup function to set. + """ + global g_setup_fn + g_setup_fn = setup_fn + + +def set_teardown_fn(teardown_fn: Callable[[], None]) -> None: + """ + Set a global teardown function to be called after each test. + + :param teardown_fn: The teardown function to set. + """ + global g_teardown_fn + g_teardown_fn = teardown_fn + + +def add_test(func, *args, run_now: bool = False, **kwargs): + """ + Register a test function to be run later or immediately. + + :param func: The test function to register. + :param args: Positional arguments to pass to the test function. + :param run_now: If True, run the test immediately instead of registering it. + :param kwargs: Keyword arguments to pass to the test function. + """ + + global g_tests + + logging.debug(f"Adding test: {func.__name__}, run_now={run_now}") + test_fn = TestFn(func, args, kwargs) + if run_now: + _run_single_test(test_fn) + else: + g_tests.append(test_fn) + + +def _run_single_test(test_fn: TestFn) -> None: + """ + Run a single test function within an active test context. + + :param test_fn: The TestFn instance representing the test to run. + """ + global g_setup_fn, g_teardown_fn + with ActiveTestContext(test_fn) as active_test: + if g_setup_fn is not None: + logging.debug( + f"Running setup function before test: {test_fn.func.__name__}" + ) + g_setup_fn() + + logging.debug(f"Running test: {test_fn.func.__name__}") + + print_helper.print_test_start(test_fn.func.__name__) + test_fn.run() + print_helper.print_test_summary( + test_fn.func.__name__, + active_test.passed, + active_test.failed, + ) + + if g_teardown_fn is not None: + logging.debug( + f"Running teardown function after test: {test_fn.func.__name__}" + ) + g_teardown_fn() + + +def run_tests(): + """ + Run all registered tests. + """ + + global g_tests + + logging.debug(f"Running {len(g_tests)} tests...") + for test_fn in g_tests: + _run_single_test(test_fn) + print() + + +def clear_tests(): + """ + Clear all registered tests. + """ + + global g_tests + logging.debug("Clearing all registered tests.") + g_tests.clear() + + +def assert_true(cond: bool, msg: str = "", negate: bool = False): + """ + Assert that a condition is true (or false if negate is True). + + :param cond: The condition to check. + :param msg: An optional message to display with the assertion result. + :param negate: If True, assert that the condition is false. + """ + + global g_active_test + if g_active_test is None: + raise RuntimeError("No active test context for assertion.") + + if cond != negate: + g_active_test.success() + else: + g_active_test.failure() + + print_helper.print_assert(msg, cond != negate) + +def assert_false(cond: bool, msg: str = "", negate: bool = False): + """ + Assert that a condition is false (or true if negate is True). + + :param cond: The condition to check. + :param msg: An optional message to display with the assertion result. + :param negate: If True, assert that the condition is true. + """ + assert_true(not cond, msg, negate) + + +def assert_eqf(a: float, b: float, tol: float, msg: str = "", negate: bool = False): + """ + Assert that two floating-point numbers are equal within a tolerance. + + :param a: The first floating-point number. + :param b: The second floating-point number. + :param tol: The tolerance within which the two numbers are considered equal. + :param msg: An optional message to display with the assertion result. + :param negate: If True, assert that the two numbers are not equal within the tolerance. + """ + assert_true(abs(a - b) <= tol, msg, negate) diff --git a/mk_assert/print_helper.py b/mk_assert/print_helper.py new file mode 100644 index 0000000..17c210c --- /dev/null +++ b/mk_assert/print_helper.py @@ -0,0 +1,29 @@ +import colorama + +colorama.just_fix_windows_console() + +RESET = colorama.Style.RESET_ALL + + +def print_assert(msg: str, passed: bool) -> None: + prefix = f"Check: {msg} " if msg else "Check: " + word = "SUCCESS" if passed else "FAILURE" + color = colorama.Fore.GREEN if passed else colorama.Fore.RED + print(f"{prefix}[{color}{word}{RESET}]") + + +def print_test_summary(test_name: str, passed: int, failed: int) -> None: + total = passed + failed + color = colorama.Fore.GREEN if failed == 0 else colorama.Fore.RED + print( + f"{colorama.Fore.BLUE}Test '{test_name}' finished:{RESET} ", + end="", + ) + if failed == 0: + print(f"{color}all passed{RESET} - {color}{passed}{RESET}/{total}") + else: + print(f"{color}{failed} failed{RESET} - {color}{passed}{RESET}/{total}") + + +def print_test_start(test_name: str) -> None: + print(f"{colorama.Fore.BLUE}Starting test '{test_name}'...{RESET}") diff --git a/net_maps/per_24_net_map.csv b/netmap/per24.csv similarity index 99% rename from net_maps/per_24_net_map.csv rename to netmap/per24.csv index 9cb7a6c..9e68d09 100644 --- a/net_maps/per_24_net_map.csv +++ b/netmap/per24.csv @@ -648,4 +648,4 @@ PDU,NetR7_2,U5,94,MCU,, PDU,FAN_2_TACH_C,U5,95,MCU,, PDU,5V_CRIT_NFLT,U5,98,MCU,, PDU,GND,U5,99,MCU,, -PDU,+3V3,U5,100,MCU,, +PDU,+3V3,U5,100,MCU,, \ No newline at end of file diff --git a/pin_maps/stm32f407_pin_map.csv b/pin_maps/stm32f407_pin_map.csv deleted file mode 100644 index a958904..0000000 --- a/pin_maps/stm32f407_pin_map.csv +++ /dev/null @@ -1,101 +0,0 @@ -Designator,Pin Name,Type -1,PE2,I/O -2,PE3,I/O -3,PE4,I/O -4,PE5,I/O -5,PE6,I/O -6,VBAT,Power -7,PC13,I/O -8,PC14,I/O -9,PC15,I/O -10,VSS,Power -11,VDD,Power -12,PH0,I/O -13,PH1,I/O -14,NRST,I/O -15,PC0,I/O -16,PC1,I/O -17,PC2,I/O -18,PC3,I/O -19,VDD,Power -20,VSSA,Power -21,VREF+,Power -22,VDDA,Power -23,PA0,I/O -24,PA1,I/O -25,PA2,I/O -26,PA3,I/O -27,VSS,Power -28,VDD,Power -29,PA4,I/O -30,PA5,I/O -31,PA6,I/O -32,PA7,I/O -33,PC4,I/O -34,PC5,I/O -35,PB0,I/O -36,PB1,I/O -37,PB2,I/O -38,PE7,I/O -39,PE8,I/O -40,PE9,I/O -41,PE10,I/O -42,PE11,I/O -43,PE12,I/O -44,PE13,I/O -45,PE14,I/O -46,PE15,I/O -47,PB10,I/O -48,PB11,I/O -49,VCAP_1,Power -50,VDD,Power -51,PB12,I/O -52,PB13,I/O -53,PB14,I/O -54,PB15,I/O -55,PD8,I/O -56,PD9,I/O -57,PD10,I/O -58,PD11,I/O -59,PD12,I/O -60,PD13,I/O -61,PD14,I/O -62,PD15,I/O -63,PC6,I/O -64,PC7,I/O -65,PC8,I/O -66,PC9,I/O -67,PA8,I/O -68,PA9,I/O -69,PA10,I/O -70,PA11,I/O -71,PA12,I/O -72,PA13,I/O -73,VCAP_2,Power -74,VSS,Power -75,VDD,Power -76,PA14,I/O -77,PA15,I/O -78,PC10,I/O -79,PC11,I/O -80,PC12,I/O -81,PD0,I/O -82,PD1,I/O -83,PD2,I/O -84,PD3,I/O -85,PD4,I/O -86,PD5,I/O -87,PD6,I/O -88,PD7,I/O -89,PB3,I/O -90,PB4,I/O -91,PB5,I/O -92,PB6,I/O -93,PB7,I/O -94,BOOT0,Input -95,PB8,I/O -96,PB9,I/O -97,PE0,I/O -98,PE1,I/O -99,VSS,Power -100,VDD,Power diff --git a/run_example.sh b/run_example.sh new file mode 100755 index 0000000..1e18f7d --- /dev/null +++ b/run_example.sh @@ -0,0 +1,3 @@ +#/bin/bash + +python3 -m tests.example.test \ No newline at end of file diff --git a/scripts/rules_constants.py b/scripts/rules_constants.py deleted file mode 100644 index 425beba..0000000 --- a/scripts/rules_constants.py +++ /dev/null @@ -1,16 +0,0 @@ -# NOTE: each constant in this file shall reference a rule - -# IMD -R_IMD_RESISTANCE_RATIO = 500 # EV.7.6.3 Ohms / Volt -R_IMD_MAX_TRIP_TIME_S = 30.0 # IN.4.4.2 -R_IMD_TRIP_TEST_RESISTANCE_PERCENT = 0.5 # IN.4.4.2 50% of response value - -# BSPD EV.7.7 -R_BSPD_MAX_TRIP_TIME_S = 0.5 # EV.7.7.2 -R_BSPD_POWER_THRESH_W = 5000 # EV.7.7.2 - -# Precharge and Discharge EV.5.6 -R_PCHG_V_BAT_THRESH = 0.90 # EV.5.6.1.a - -# TSAL EV.5.9 -R_TSAL_HV_V = 60.0 # T.9.1.1 diff --git a/scripts/test_abox.py b/scripts/test_abox.py deleted file mode 100644 index 99e9269..0000000 --- a/scripts/test_abox.py +++ /dev/null @@ -1,321 +0,0 @@ -from os import sys, path -# adds "./HIL-Testing" to the path, basically making it so these scripts were run one folder level higher -sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) - -from hil.hil import HIL -import hil.utils as utils -import time -from rules_constants import * -from vehicle_constants import * - - -import pytest_check as check -import pytest - - - -# ---------------------------------------------------------------------------- # -@pytest.fixture(scope="session") -def hil(): - hil_instance = HIL() - - hil_instance.load_config("config_abox_bench.json") - hil_instance.load_pin_map("per_24_net_map.csv", "stm32f407_pin_map.csv") - - hil_instance.init_can() - - yield hil_instance - - hil_instance.shutdown() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_abox_ams(hil): - # Begin the test - # hil.start_test(test_abox_ams.__name__) - - # Outputs - den = hil.dout("a_box", "Discharge Enable") - csafe = hil.dout("a_box", "Charger Safety") - bms_override = hil.daq_var("a_box", "bms_daq_override") - bms_stat = hil.daq_var("a_box", "bms_daq_stat") - - # Inputs - chrg_stat = hil.din("a_box", "BMS Status Charger") - main_stat = hil.din("a_box", "BMS Status PDU") - - bms_override.state = 1 - - for i in range(0, 8): - dchg_set = bool(i & 0x1) - chg_set = bool(i & 0x2) - bms_set = bool(i & 0x4) - exp_chrg = not (chg_set or bms_set) - exp_dchg = not (dchg_set or bms_set) - - den.state = dchg_set - csafe.state = chg_set - bms_stat.state = bms_set - print(f"Combo {i}") - time.sleep(0.1) - # hil.check(chrg_stat.state == exp_chrg, f"Chrg stat {exp_chrg}") - # hil.check(main_stat.state == exp_dchg, f"Main stat {exp_dchg}") - check.equal(chrg_stat.state, exp_chrg, f"Chrg stat {exp_chrg}") - check.equal(main_stat.state, exp_dchg, f"Main stat {exp_dchg}") - - bms_override.state = 0 - - # hil.end_test() -# ---------------------------------------------------------------------------- # - -# ---------------------------------------------------------------------------- # -def test_isense(hil): - # Begin the test - # hil.start_test(test_isense.__name__) - - # Outputs - ch1_raw = hil.aout("a_box", "Isense_Ch1_raw") - - # Inputs - ch1_filt = hil.ain("a_box", "ISense Ch1") - - # Need to test voltage divider transfer function correct - for v in [0.0, DHAB_S124_MIN_OUT_V, DHAB_S124_OFFSET_V, 3.2, DHAB_S124_MAX_OUT_V, 5.0]: - ch1_raw.state = v - time.sleep(1) - exp_out = ABOX_DHAB_CH1_DIV.div(v) - input(f"enter to meas, set to {v}, expected {exp_out}") - meas = ch1_filt.state - print(f"isense expected: {exp_out}V, measured: {meas}V") - # hil.check_within(meas, exp_out, 0.05, f"Isense v={v:.3}") - check.almost_equal(meas, exp_out, abs=0.05, rel=0.0, msg=f"Isense v={v:.3}") - - ch1_raw.hiZ() - time.sleep(0.01) - # hil.check_within(ch1_filt.state, 0.0, 0.05, f"Isense float pulled down") - check.almost_equal(ch1_filt.state, 0.0, abs=0.05, rel=0.0, msg="Isense float pulled down") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -RLY_ON = 0 -RLY_OFF = 1 -RLY_DLY = 0.01 # Mechanicl relay takes time to transition - -def test_precharge(hil): - # Begin the test - # hil.start_test(test_precharge.__name__) - - # Outputs - n_pchg_cmplt = hil.dout("a_box", "NotPrechargeComplete") - sdc = hil.dout("a_box", "SDC") - bat_p = hil.dout("a_box", "Batt+") - - # Inputs - resistor = hil.din("a_box", "NetK1_4") # To precharge resistor - - bat_p.state = RLY_ON - - print("Combo 1") - n_pchg_cmplt.state = 0 - sdc.state = RLY_OFF - time.sleep(RLY_DLY) - # hil.check(resistor.state == 0, "Resistor disconnected") - check.equal(resistor.state, 0, "Combo 1, resistor disconnected") - - print("Combo 2") - n_pchg_cmplt.state = 1 - sdc.state = RLY_OFF - time.sleep(RLY_DLY) - # hil.check(resistor.state == 0, "Resistor disconnected") - check.equal(resistor.state, 0, "Combo 2, resistor disconnected") - - print("Combo 3") - n_pchg_cmplt.state = 1 - sdc.state = RLY_ON - time.sleep(RLY_DLY) - # hil.check(resistor.state == 1, "Resistor connected") - check.equal(resistor.state, 1, "Combo 3, resistor connected") - - print("Combo 4") - n_pchg_cmplt.state = 0 - sdc.state = RLY_ON - time.sleep(RLY_DLY) - # hil.check(resistor.state == 0, "Resistor disconnected") - check.equal(resistor.state, 0, "Combo 4, resistor disconnected") - - # Duration test - time.sleep(1) - n_pchg_cmplt.state = 1 - sdc.state = RLY_ON - time.sleep(RLY_DLY) - # hil.check(resistor.state == 1, "Duration init") - check.equal(resistor.state, 1, "Duration init") - - time.sleep(9) - # hil.check(resistor.state == 1, "Duration mid") - check.equal(resistor.state, 1, "Duration mid") - - n_pchg_cmplt.state = 0 - time.sleep(RLY_DLY) - # hil.check(resistor.state == 0, "Duration end") - check.equal(resistor.state, 0, "Duration end") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -SUPPLY_VOLTAGE = 24.0 -TIFF_DLY = 0.3 - -def test_tiffomy(hil): - # Begin the test - # hil.start_test(test_tiffomy.__name__) - - # Outputs - bat_p = hil.dout("a_box", "Batt+") - - # Inputs - vbat = hil.ain("a_box", "VBatt") - imd_hv = hil.din("a_box", "Batt+_Fused") - - # NOTE: the IMD test confirms that the relay closed - # This is a bit redundant of the tiffomy voltage measurement - - utils.log_warning(f"Assuming supply = {SUPPLY_VOLTAGE} V") - utils.log_warning(f"Do not reverse polarity Vbat, it will kill Arduino ADC") - input("Click enter to acknowledge or ctrl+c to cancel") - - bat_p.state = RLY_OFF - time.sleep(TIFF_DLY) - # hil.check_within(vbat.state, 0.0, 0.1, "TIff off") - # hil.check(imd_hv.state == 0, "IMD HV off") - check.almost_equal(vbat.state, 0.0, abs=0.1, rel=0.0, msg="TIff off") - check.equal(imd_hv.state, 0, "IMD HV off") - - bat_p.state = RLY_ON - time.sleep(TIFF_DLY) - exp = SUPPLY_VOLTAGE - #input("press enter, tiff should be getting volts") - meas = tiff_lv_to_hv(vbat.state) - print(f"Tiff HV reading: {meas} V, expect: {SUPPLY_VOLTAGE} V") - # hil.check_within(meas, exp, 2.5, "Tiff on") - # hil.check(imd_hv.state == 1, "IMD HV on") - check.almost_equal(meas, exp, abs=2.5, rel=0.0, msg="Tiff on") - check.equal(imd_hv.state, 1, "IMD HV on") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - -# ---------------------------------------------------------------------------- # -def test_tmu(hil): - # Begin the test - # hil.start_test(test_tmu.__name__) - - # Outputs - tmu_a_do = hil.dout("a_box", "TMU_1") - tmu_b_do = hil.dout("a_box", "TMU_2") - tmu_c_do = hil.dout("a_box", "TMU_3") - tmu_d_do = hil.dout("a_box", "TMU_4") - - daq_override = hil.daq_var("a_box", "tmu_daq_override") - daq_therm = hil.daq_var("a_box", "tmu_daq_therm") - - # Inputs - mux_a = hil.din("a_box", "MUX_A_NON_ISO") - mux_b = hil.din("a_box", "MUX_B_NON_ISO") - mux_c = hil.din("a_box", "MUX_C_NON_ISO") - mux_d = hil.din("a_box", "MUX_D_NON_ISO") - - tmu_a_ai = hil.daq_var("a_box", "tmu_1") - tmu_b_ai = hil.daq_var("a_box", "tmu_2") - tmu_c_ai = hil.daq_var("a_box", "tmu_3") - tmu_d_ai = hil.daq_var("a_box", "tmu_4") - - daq_therm.state = 0 - daq_override.state = 1 - - # mux line test - for i in range(0,16): - daq_therm.state = i - time.sleep(0.05) - # hil.check(mux_a.state == bool(i & 0x1), f"Mux A test {i}") - # hil.check(mux_b.state == bool(i & 0x2), f"Mux B test {i}") - # hil.check(mux_c.state == bool(i & 0x4), f"Mux C test {i}") - # hil.check(mux_d.state == bool(i & 0x8), f"Mux D test {i}") - check.equal(mux_a.state, bool(i & 0x1), f"Mux A test {i}") - check.equal(mux_b.state, bool(i & 0x2), f"Mux B test {i}") - check.equal(mux_c.state, bool(i & 0x4), f"Mux C test {i}") - check.equal(mux_d.state, bool(i & 0x8), f"Mux D test {i}") - - daq_override.state = 0 - - TMU_TOLERANCE = 100 - TMU_HIGH_VALUE = 1970 #2148 - - # thermistors - for i in range(0,16): - tmu_a_do.state = bool(i & 0x1) - tmu_b_do.state = bool(i & 0x2) - tmu_c_do.state = bool(i & 0x4) - tmu_d_do.state = bool(i & 0x8) - time.sleep(1.0) - a = int(tmu_a_ai.state) - b = int(tmu_b_ai.state) - c = int(tmu_c_ai.state) - d = int(tmu_d_ai.state) - print(f"Readings at therm={i}: {a}, {b}, {c}, {d}") - # hil.check_within(a, TMU_HIGH_VALUE if (i & 0x1) else 0, TMU_TOLERANCE, f"TMU 1 test {i}") - # hil.check_within(b, TMU_HIGH_VALUE if (i & 0x2) else 0, TMU_TOLERANCE, f"TMU 2 test {i}") - # hil.check_within(c, TMU_HIGH_VALUE if (i & 0x4) else 0, TMU_TOLERANCE, f"TMU 3 test {i}") - # hil.check_within(d, TMU_HIGH_VALUE if (i & 0x8) else 0, TMU_TOLERANCE, f"TMU 4 test {i}") - check.almost_equal(a, TMU_HIGH_VALUE if (i & 0x1) else 0, abs=TMU_TOLERANCE, rel=0.0, msg=f"TMU 1 test {i}") - check.almost_equal(b, TMU_HIGH_VALUE if (i & 0x2) else 0, abs=TMU_TOLERANCE, rel=0.0, msg=f"TMU 2 test {i}") - check.almost_equal(c, TMU_HIGH_VALUE if (i & 0x4) else 0, abs=TMU_TOLERANCE, rel=0.0, msg=f"TMU 3 test {i}") - check.almost_equal(d, TMU_HIGH_VALUE if (i & 0x8) else 0, abs=TMU_TOLERANCE, rel=0.0, msg=f"TMU 4 test {i}") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - -# ---------------------------------------------------------------------------- # -def test_imd(hil): - # hil.start_test(test_imd.__name__) - - # Outputs - imd_out = hil.dout('a_box', 'IMD_Status') - - # Inputs - imd_in = hil.din('a_box', 'IMD_STATUS_LV_COMP') - imd_mcu = hil.mcu_pin('a_box', 'IMD_STATUS_LV_COMP') - - - imd_out.state = RLY_OFF - time.sleep(RLY_DLY) - - # hil.check(imd_in.state == 0, 'IMD LV OFF') - # hil.check(imd_mcu.state == 0, 'IMD MCU OFF') - check.equal(imd_in.state, 0, 'IMD LV OFF') - check.equal(imd_mcu.state, 0, 'IMD MCU OFF') - - imd_out.state = RLY_ON - time.sleep(RLY_DLY) - - # hil.check(imd_in.state == 1, 'IMD LV ON') - # hil.check(imd_mcu.state == 1, 'IMD MCU ON') - check.equal(imd_in.state, 1, 'IMD LV ON') - check.equal(imd_mcu.state, 1, 'IMD MCU ON') - - imd_out.state = RLY_OFF - time.sleep(RLY_DLY) - - # hil.check(imd_in.state == 0, 'IMD LV BACK OFF') - # hil.check(imd_mcu.state == 0, 'IMD MCU BACK OFF') - check.equal(imd_in.state, 0, 'IMD LV BACK OFF') - check.equal(imd_mcu.state, 0, 'IMD MCU BACK OFF') - - # hil.end_test() -# ---------------------------------------------------------------------------- # \ No newline at end of file diff --git a/scripts/test_charger.py b/scripts/test_charger.py deleted file mode 100644 index 4b7138c..0000000 --- a/scripts/test_charger.py +++ /dev/null @@ -1,186 +0,0 @@ -from os import sys, path -# adds "./HIL-Testing" to the path, basically making it so these scripts were run one folder level higher -sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) - -from hil.hil import HIL -import hil.utils as utils -import time -from rules_constants import * -from vehicle_constants import * - -import pytest_check as check -import pytest - - -# ---------------------------------------------------------------------------- # -AMS_STAT_OKAY = 1 -AMS_STAT_TRIP = 0 -AMS_CTRL_OKAY = 1 -AMS_CTRL_TRIP = 0 - -def reset_ams(ams_stat): - ams_stat.state = AMS_STAT_OKAY - -IMD_STAT_OKAY = 1 -IMD_STAT_TRIP = 0 -def reset_imd(imd_stat): - imd_stat.state = IMD_STAT_OKAY - -power = None - -CYCLE_POWER_OFF_DELAY = 2.0 -CYCLE_POWER_ON_DELAY = 3.0 - -def cycle_power(): - power.state = 1 - time.sleep(CYCLE_POWER_OFF_DELAY) - power.state = 0 - time.sleep(CYCLE_POWER_ON_DELAY) - - -IMD_RC_MIN_TRIP_TIME_S = IMD_STARTUP_TIME_S -IMD_RC_MAX_TRIP_TIME_S = R_IMD_MAX_TRIP_TIME_S - IMD_MEASURE_TIME_S -IMD_CTRL_OKAY = 1 -IMD_CTRL_TRIP = 0 -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -@pytest.fixture(scope="session") -def hil(): - global power - - hil_instance = HIL() - - hil_instance.load_config("config_charger.json") - hil_instance.load_pin_map("per_24_net_map.csv", "stm32f407_pin_map.csv") - - # hil_instance.init_can() - - power = hil_instance.dout("RearTester", "RLY1") - - yield hil_instance - - hil_instance.shutdown() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_imd(hil): - # Begin the test - # hil.start_test(test_imd.__name__) - - # Outputs - imd_stat = hil.dout("Charger", "IMD_STATUS") - - # Outputs to set SDC status to okay - ams_stat = hil.dout("Charger", "BMS_STATUS") - - # Inputs - imd_ctrl = hil.din("Main_Module", "SDC_FINAL") # assuming AMS closed - - # Set other SDC nodes to okay - reset_ams(ams_stat) - - # IMD Fault - reset_imd(imd_stat) - cycle_power() - # hil.check(imd_ctrl.state == IMD_CTRL_OKAY, "Power On") - check.equal(imd_ctrl.state, IMD_CTRL_OKAY, "Power On") - - time.sleep(1) - imd_stat.state = IMD_STAT_TRIP - t = utils.measure_trip_time(imd_ctrl, R_IMD_MAX_TRIP_TIME_S, is_falling=True) - print(f"Target trip time: [{IMD_RC_MIN_TRIP_TIME_S}, {IMD_RC_MAX_TRIP_TIME_S}]") - # hil.check(IMD_RC_MIN_TRIP_TIME_S < t < IMD_RC_MAX_TRIP_TIME_S, "IMD Trip Time") - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Trip") - check.between(t, IMD_RC_MIN_TRIP_TIME_S, IMD_RC_MAX_TRIP_TIME_S, "IMD Trip Time") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Trip") - - imd_stat.state = IMD_STAT_OKAY - time.sleep(IMD_RC_MAX_TRIP_TIME_S * 1.1) - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Fault Stays Latched") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Fault Stays Latched") - - - # IMD Fault on Power On - reset_imd(imd_stat) - imd_stat.state = IMD_STAT_TRIP - cycle_power() - time.sleep(IMD_RC_MAX_TRIP_TIME_S) - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Fault Power On") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Fault Power On") - - - # IMD Floating - reset_imd(imd_stat) - imd_stat.hiZ() - cycle_power() - t = utils.measure_trip_time(imd_ctrl, R_IMD_MAX_TRIP_TIME_S, is_falling=True) - # hil.check(t < R_IMD_MAX_TRIP_TIME_S, "IMD Floating Trip Time") - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Floating Trip") - check.less(t, R_IMD_MAX_TRIP_TIME_S, "IMD Floating Trip Time") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Floating Trip") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - -# ---------------------------------------------------------------------------- # -def test_ams(hil): - # Begin the test - hil.start_test(test_ams.__name__) - - # Outputs - ams_stat = hil.dout("Charger", "BMS_STATUS") - - - # Outputs to set SDC status to okay - imd_stat = hil.dout("Charger", "IMD_STATUS") - - # Inputs - ams_ctrl = hil.din("Main_Module", "SDC_FINAL") # assuming AMS closed - - # Set other SDC nodes to okay - reset_imd(imd_stat) - - # AMS Fault - reset_ams(ams_stat) - cycle_power() - # hil.check(ams_ctrl.state == AMS_CTRL_OKAY, "Power On") - check.equal(ams_ctrl.state, AMS_CTRL_OKAY, "Power On") - - time.sleep(1) - ams_stat.state = AMS_STAT_TRIP - t = utils.measure_trip_time(ams_ctrl, AMS_MAX_TRIP_DELAY_S * 2, is_falling=True) - # hil.check(0 < t < AMS_MAX_TRIP_DELAY_S, "AMS Trip Time") - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Trip") - check.between(t, 0, AMS_MAX_TRIP_DELAY_S, "AMS Trip Time") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Trip") - - ams_stat.state = AMS_STAT_OKAY - time.sleep(AMS_MAX_TRIP_DELAY_S * 1.1) - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Fault Stays Latched") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Fault Stays Latched") - - - # AMS Fault on Power On - reset_ams(ams_stat) - ams_stat.state = AMS_STAT_TRIP - cycle_power() - time.sleep(AMS_MAX_TRIP_DELAY_S) - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Fault Power On") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Fault Power On") - - - # AMS Floating - reset_ams(ams_stat) - ams_stat.hiZ() - cycle_power() - t = utils.measure_trip_time(ams_ctrl, AMS_MAX_TRIP_DELAY_S * 2, is_falling=True) - # hil.check(0 <= t < AMS_MAX_TRIP_DELAY_S, "AMS Floating Trip Time") - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Floating Trip") - check.between(t, 0, AMS_MAX_TRIP_DELAY_S, "AMS Floating Trip Time", ge=True) - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Floating Trip") - - # hil.end_test() -# ---------------------------------------------------------------------------- # \ No newline at end of file diff --git a/scripts/test_collector.py b/scripts/test_collector.py deleted file mode 100644 index 3065b1c..0000000 --- a/scripts/test_collector.py +++ /dev/null @@ -1,89 +0,0 @@ -from os import sys, path -# adds "./HIL-Testing" to the path, basically making it so these scripts were run one folder level higher -sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) - -from hil.hil import HIL -import hil.utils as utils -import time - -import pytest_check as check -import pytest - - -# ---------------------------------------------------------------------------- # -@pytest.fixture(scope="session") -def hil(): - hil_instance = HIL() - - hil_instance.load_config("config_collector_bench.json") - hil_instance.load_pin_map("per_24_net_map.csv", "stm32f407_pin_map.csv") - - # hil_instance.init_can() - - yield hil_instance - - hil_instance.shutdown() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_collector(hil): - # Begin the test - # hil.start_test(test_collector.__name__) - - # Outputs - m1 = hil.dout("Collector", "MUX_A") - m2 = hil.dout("Collector", "MUX_B") - m3 = hil.dout("Collector", "MUX_C") - m4 = hil.dout("Collector", "MUX_D") - - # Inputs - to = hil.ain("Collector", "TEMP_OUT") - - tolerance_v = 0.1 # volts - current_res = 9100.0 # ohms - pullup_res = 4700.0 # ohms - test_voltage = 3.3 # volts - pullup_voltage = 5 # volts - num_therm = 10 - - test_voltage = (pullup_voltage / (current_res + pullup_res)) * current_res - - utils.log_warning(test_voltage) - - for thermistor in range(num_therm): - print(f"\nPlace test input on thermistor {thermistor}.") - - # TODO: find some way to wait for user input - # input("Press Enter when ready...") - - for i in range(num_therm): - # MUX (multiplexer) = choose which output to return from the thermistor based on the input - # Like a giant switch statement (0 -> return thermistor 0, 1 -> return thermistor 1, etc.) - # Encode the current thermistor into binary where each bit corresponds to each pin being high or low - m1.state = i & 0x1 - m2.state = i & 0x2 - m3.state = i & 0x4 - m4.state = i & 0x8 - time.sleep(0.01) - - to_state = to.state - if i == thermistor: - expected_voltage = test_voltage - else: - expected_voltage = pullup_voltage - within = abs(to_state - expected_voltage) < tolerance_v - - print(f"({thermistor=}, {i=}) {to_state=} ?= {expected_voltage=} -> {within=}") - check.almost_equal(to_state, expected_voltage, abs=tolerance_v, rel=0.0, msg=f"Input on therm {thermistor}, selecting {i}") - - # if i == thermistor: - # # hil.check_within(to.state, test_voltage, tolerance_v, f"Input on therm {thermistor}, selecting {i}") - # # check.almost_equal(to.state, test_voltage, abs=tolerance_v, rel=0.0, msg=f"Input on therm {thermistor}, selecting {i}") - # else: - # # hil.check_within(to.state, pullup_voltage, tolerance_v, f"Input on therm {thermistor}, selecting {i}") - # # check.almost_equal(to.state, pullup_voltage, abs=tolerance_v, rel=0.0, msg=f"Input on therm {thermistor}, selecting {i}") - - # End the test - # hil.end_test() -# ---------------------------------------------------------------------------- # diff --git a/scripts/test_dash.py b/scripts/test_dash.py deleted file mode 100644 index 57501f2..0000000 --- a/scripts/test_dash.py +++ /dev/null @@ -1,181 +0,0 @@ -from os import sys, path -# adds "./HIL-Testing" to the path, basically making it so these scripts were run one folder level higher -sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) - -from hil.hil import HIL -import hil.utils as utils -import time -from rules_constants import * -from vehicle_constants import * - -import pytest_check as check -import pytest - - -# ---------------------------------------------------------------------------- # -@pytest.fixture(scope="session") -def hil(): - hil_instance = HIL() - - hil_instance.load_config("config_system_hil_attached.json") - hil_instance.load_pin_map("per_24_net_map.csv", "stm32f407_pin_map.csv") - - hil_instance.init_can() - - yield hil_instance - - hil_instance.shutdown() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -BRK_SWEEP_DELAY = 0.1 - -def test_bspd(hil): - # Begin the test - # hil.start_test(test_bspd.__name__) - - # Outputs - brk1 = hil.aout("Dashboard", "BRK1_RAW") - brk2 = hil.aout("Dashboard", "BRK2_RAW") - - # Inputs - brk_fail_tap = hil.mcu_pin("Dashboard", "BRK_FAIL_TAP") - brk_stat_tap = hil.mcu_pin("Dashboard", "BRK_STAT_TAP") - - # Brake threshold check - brk1.state = BRK_1_REST_V - brk2.state = BRK_2_REST_V - # hil.check(brk_stat_tap.state == 0, "Brake stat starts low") - check.equal(brk_stat_tap.state, 0, "Brake stat starts low") - - brk1.state = BRK_1_THRESH_V - time.sleep(0.1) - # hil.check(brk_stat_tap.state == 1, "Brake stat goes high at brk 1 thresh") - check.equal(brk_stat_tap.state, 1, "Brake stat goes high at brk 1 thresh") - - brk1.state = BRK_1_REST_V - # hil.check(brk_stat_tap.state == 0, "Brake stat starts low") - check.equal(brk_stat_tap.state, 0, "Brake stat starts low") - - brk2.state = BRK_2_THRESH_V - time.sleep(0.1) - # hil.check(brk_stat_tap.state == 1, "Brake stat goes high at brk 2 thresh") - check.equal(brk_stat_tap.state, 1, "Brake stat goes high at brk 2 thresh") - - brk1.state = BRK_1_THRESH_V - # hil.check(brk_stat_tap.state == 1, "Brake stat stays high for both brakes") - check.equal(brk_stat_tap.state, 1, "Brake stat stays high for both brakes") - - - # Brake threshold scan - brk1.state = BRK_MIN_OUT_V - brk2.state = BRK_MIN_OUT_V - time.sleep(0.1) - # hil.check(brk_stat_tap.state == 0, "Brake Stat Starts Low Brk 1") - check.equal(brk_stat_tap.state, 0, "Brake Stat Starts Low Brk 1") - - start = BRK_MIN_OUT_V - stop = BRK_MAX_OUT_V - step = 0.1 - - thresh = utils.measure_trip_thresh(brk1, start, stop, step, - BRK_SWEEP_DELAY, - brk_stat_tap, is_falling=False) - print(f"Brake 1 braking threshold: {thresh}") - # hil.check_within(thresh, BRK_1_THRESH_V, 0.2, "Brake 1 trip voltage") - # hil.check(brk_stat_tap.state == 1, "Brake Stat Tripped for Brk 1") - check.almost_equal( - thresh, BRK_1_THRESH_V, - abs=0.2, rel=0.0, - msg="Brake 1 trip voltage" - ) - check.equal(brk_stat_tap.state, 1, "Brake Stat Tripped for Brk 1") - - brk1.state = BRK_MIN_OUT_V - brk2.state = BRK_MIN_OUT_V - hil.check(brk_stat_tap.state == 0, "Brake Stat Starts Low Brk 2") - thresh = utils.measure_trip_thresh(brk2, start, stop, step, - BRK_SWEEP_DELAY, - brk_stat_tap, is_falling=False) - print(f"Brake 2 braking threshold: {thresh}") - # hil.check_within(thresh, BRK_2_THRESH_V, 0.2, "Brake 2 trip voltage") - # hil.check(brk_stat_tap.state == 1, "Brake Stat Tripped for Brk 2") - check.almost_equal( - thresh, BRK_2_THRESH_V, - abs=0.2, rel=0.0, - msg="Brake 2 trip voltage" - ) - check.equal(brk_stat_tap.state, 1, "Brake Stat Tripped for Brk 2") - - # Brake Fail scan - brk1.state = BRK_1_REST_V - brk2.state = BRK_2_REST_V - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 0, "Brake Fail Check 1 Starts 0") - check.equal(brk_fail_tap.state, 0, "Brake Fail Check 1 Starts 0") - - brk1.state = 0.0 # Force 0 - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 1, "Brake Fail Brk 1 Short GND") - check.equal(brk_fail_tap.state, 1, "Brake Fail Brk 1 Short GND") - - brk1.state = BRK_1_REST_V - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 0, "Brake Fail Check 2 Starts 0") - check.equal(brk_fail_tap.state, 0, "Brake Fail Check 2 Starts 0") - - brk2.state = 0.0 # Force 0 - time.sleep(0.1) - hil.check(brk_fail_tap.state == 1, "Brake Fail Brk 2 Short GND") - check.equal(brk_fail_tap.state, 1, "Brake Fail Brk 2 Short GND") - - brk2.state = BRK_2_REST_V - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 0, "Brake Fail Check 3 Starts 0") - check.equal(brk_fail_tap.state, 0, "Brake Fail Check 3 Starts 0") - - brk1.state = 5.0 # Short VCC - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 1, "Brake Fail Brk 1 Short VCC") - check.equal(brk_fail_tap.state, 1, "Brake Fail Brk 1 Short VCC") - - brk1.state = BRK_1_REST_V - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 0, "Brake Fail Check 4 Starts 0") - check.equal(brk_fail_tap.state, 0, "Brake Fail Check 4 Starts 0") - - brk2.state = 5.0 # Short VCC - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 1, "Brake Fail Brk 2 Short VCC") - check.equal(brk_fail_tap.state, 1, "Brake Fail Brk 2 Short VCC") - - brk2.state = BRK_2_REST_V - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 0, "Brake Fail Check 5 Starts 0") - check.equal(brk_fail_tap.state, 0, "Brake Fail Check 5 Starts 0") - - brk1.hiZ() - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 1, "Brake Fail Brk 1 Hi-Z") - check.equal(brk_fail_tap.state, 1, "Brake Fail Brk 1 Hi-Z") - - brk1.state = BRK_1_REST_V - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 0, "Brake Fail Check 6 Starts 0") - check.equal(brk_fail_tap.state, 0, "Brake Fail Check 6 Starts 0") - - brk2.hiZ() - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 1, "Brake Fail Brk 2 Hi-Z") - check.equal(brk_fail_tap.state, 1, "Brake Fail Brk 2 Hi-Z") - - brk2.state = BRK_2_REST_V - - # End the test - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# TODO: add throttle checks - diff --git a/scripts/test_main_base.py b/scripts/test_main_base.py deleted file mode 100644 index 3a50363..0000000 --- a/scripts/test_main_base.py +++ /dev/null @@ -1,554 +0,0 @@ -from os import sys, path -# adds "./HIL-Testing" to the path, basically making it so these scripts were run one folder level higher -sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) - -from hil.hil import HIL -import hil.utils as utils -import time -from rules_constants import * -from vehicle_constants import * - -import pytest_check as check -import pytest - - -# ---------------------------------------------------------------------------- # -AMS_STAT_OKAY = 1 -AMS_STAT_TRIP = 0 -AMS_CTRL_OKAY = 1 -AMS_CTRL_TRIP = 0 - -def reset_ams(ams_stat): - ams_stat.state = AMS_STAT_OKAY - -def set_bspd_current(ch1, current): - ch1.state = ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(current)) - -def reset_bspd(fail, stat, ch1): - fail.state = 0 - stat.state = 0 - set_bspd_current(ch1, 0.0) - -IMD_STAT_OKAY = 1 -IMD_STAT_TRIP = 0 -def reset_imd(imd_stat): - imd_stat.state = IMD_STAT_OKAY - -def reset_pchg(v_bat, v_mc): - print(f"Setting v_bat to {tiff_hv_to_lv(ACCUM_NOM_V)}") - v_bat.state = tiff_hv_to_lv(ACCUM_NOM_V) - print(f"Setting v_mc to {tiff_hv_to_lv(0.0)}") - v_mc.state = tiff_hv_to_lv(0.0) - -def reset_tsal(v_mc): - v_mc.state = tiff_hv_to_lv(0.0) - -power = None - -CYCLE_POWER_ON_DELAY = 0.1 - -def cycle_power(): - power.state = 1 - time.sleep(0.75) - power.state = 0 - time.sleep(CYCLE_POWER_ON_DELAY) -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -@pytest.fixture(scope="session") -def hil(): - global power - - hil_instance = HIL() - - hil_instance.load_config("config_main_base_bench.json") - hil_instance.load_pin_map("per_24_net_map.csv", "stm32f407_pin_map.csv") - - hil_instance.init_can() - - power = hil_instance.dout("Arduino2", "RLY1") - - yield hil_instance - - hil_instance.shutdown() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_precharge(hil): - # Begin the test - # hil.start_test(test_precharge.__name__) - - # Outputs - v_bat = hil.aout("Main_Module", "VBatt") - v_mc = hil.aout("Main_Module", "Voltage MC Transducer") - - # Inputs - pchg_cmplt = hil.mcu_pin("Main_Module", "PrechargeComplete_Prot") - not_pchg_cmplt_delayed = hil.din("Main_Module", "NotPrechargeCompleteSchmitt") - # v_bat_mcu = hil.daq_var("Main_Module", "Varname") # TODO - # v_mc_mcu = hil.daq_var("Main_Module", "Varname") # TODO - # pchg_mux = hil.daq_var("Main_Module", "Varname") # TODO - - # Initial State - reset_pchg(v_bat, v_mc) - time.sleep(2.5) - # hil.check(pchg_cmplt.state == 0, "Precharge not complete on startup") - # hil.check(not_pchg_cmplt_delayed.state == 1, "Not precharge complete delayed high on startup") - check.equal(pchg_cmplt.state, 0, "Precharge not complete on startup") - check.equal(not_pchg_cmplt_delayed.state, 1, "Not precharge complete delayed high on startup") - - # Check delay - v_mc.state = tiff_hv_to_lv(ACCUM_NOM_V) - t = utils.measure_trip_time(not_pchg_cmplt_delayed, PCHG_COMPLETE_DELAY_S*3, is_falling=True) - # hil.check(not_pchg_cmplt_delayed.state == 0, "Precharge complete delayed") - # hil.check_within(t, PCHG_COMPLETE_DELAY_S, 0.25, f"Precharge complete delay of {t:.3}s close to expected {PCHG_COMPLETE_DELAY_S}s") - check.equal(not_pchg_cmplt_delayed.state, 0, "Precharge complete delayed") - check.almost_equal(t, PCHG_COMPLETE_DELAY_S, abs=0.25, rel=0.0, msg=f"Precharge complete delay of {t:.3}s close to expected {PCHG_COMPLETE_DELAY_S}s") - - - # Find threshold at nominal pack voltage - for v in [ACCUM_MIN_V, ACCUM_NOM_V, ACCUM_MAX_V]: - reset_pchg(v_bat, v_mc) - print(f"Testing precharge threshold at V_bat = {v}") - v_bat.state = tiff_hv_to_lv(v) - v_mc.state = tiff_hv_to_lv(v*0.8) - time.sleep(0.01) - # hil.check(pchg_cmplt.state == 0, "Precharge Complete Low at Initial State") - check.equal(pchg_cmplt.state, 0, "Precharge Complete Low at Initial State") - - start = tiff_hv_to_lv(v*0.8) - stop = tiff_hv_to_lv(v) - step = tiff_hv_to_lv(1) - thresh = utils.measure_trip_thresh(v_mc, start, stop, step, 0.01, - pchg_cmplt, is_falling=0) - thresh_hv = tiff_lv_to_hv(thresh) - print(f"Precharge triggered at {thresh_hv / v * 100:.4}% ({thresh_hv:.5}V) of vbat={v}.") - # hil.check_within(thresh_hv / v, R_PCHG_V_BAT_THRESH, 0.03, f"Precharge threshold of {R_PCHG_V_BAT_THRESH*100}% at vbat = {v}V") - check.almost_equal(thresh_hv / v, R_PCHG_V_BAT_THRESH, abs=0.03, rel=0.0, msg=f"Precharge threshold of {R_PCHG_V_BAT_THRESH*100}% at vbat = {v}V") - - v_mc.state = tiff_hv_to_lv(v) - time.sleep(0.25) - # hil.check(pchg_cmplt.state == 1, f"Precharge completed at vbat = {v}V") - check.equal(pchg_cmplt.state, 1, f"Precharge completed at vbat = {v}V") - - - # Floating conditions (check never precharge complete) - reset_pchg(v_bat, v_mc) - v_bat.hiZ() - v_mc.state = tiff_hv_to_lv(0) - # hil.check(pchg_cmplt.state == 0, "Precharge not complete on v_bat float, v_mc 0V") - check.equal(pchg_cmplt.state, 0, "Precharge not complete on v_bat float, v_mc 0V") - - v_mc.state = tiff_hv_to_lv(ACCUM_MAX_V) - # hil.check(pchg_cmplt.state == 0, "Precharge not complete on v_bat float, v_mc max V") - check.equal(pchg_cmplt.state, 0, "Precharge not complete on v_bat float, v_mc max V") - - reset_pchg(v_bat, v_mc) - v_mc.hiZ() - v_bat.state = tiff_hv_to_lv(ACCUM_MIN_V) - # hil.check(pchg_cmplt.state == 0, "Precharge not complete on v_bat min, v_mc float") - check.equal(pchg_cmplt.state, 0, "Precharge not complete on v_bat min, v_mc float") - v_bat.state = tiff_hv_to_lv(ACCUM_MAX_V) - # hil.check(pchg_cmplt.state == 0, "Precharge not complete on v_bat max, v_mc float") - check.equal(pchg_cmplt.state, 0, "Precharge not complete on v_bat max, v_mc float") - - reset_pchg(v_bat, v_mc) - v_bat.hiZ() - v_mc.hiZ() - # hil.check(pchg_cmplt.state == 0, "Precharge not complete on v_bat float, v_mc float") - check.equal(pchg_cmplt.state, 0, "Precharge not complete on v_bat float, v_mc float") - - # TODO: software precharge validity checks (make precharge take forever) - # hil.end_test() -# ---------------------------------------------------------------------------- # - -# ---------------------------------------------------------------------------- # -def test_bspd(hil): - # Begin the test - # hil.start_test(test_bspd.__name__) - - # Outputs - brk_fail = hil.dout("Main_Module", "Brake Fail") - brk_stat = hil.dout("Main_Module", "Brake Status") - c_sense = hil.aout("Main_Module", "Current Sense C1") - - # Outputs to set SDC status to okay - ams_stat = hil.dout("Main_Module", "BMS-Status-Main") - imd_stat = hil.dout("Main_Module", "IMD_Status") - - # Inputs - bspd_ctrl = hil.din("Main_Module", "SDC3") # Assuming IMD and AMS closed - - # Set other SDC nodes to okay - reset_ams(ams_stat) - reset_imd(imd_stat) - - # Brake Fail - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power() - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - brk_fail.state = 1 - t = utils.measure_trip_time(bspd_ctrl, 5.0, is_falling=True) - # hil.check(t < R_BSPD_MAX_TRIP_TIME_S, "Brake Fail") - check.less(t, R_BSPD_MAX_TRIP_TIME_S, "Brake Fail") - brk_fail.state = 0 - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Brake Fail Stays Latched") - check.equal(bspd_ctrl.state, 0, "Brake Fail Stays Latched") - - # Brake Fail on Power On - reset_bspd(brk_fail, brk_stat, c_sense) - # Manual power cycle, setting brk_fail on before - # Will cause power to feed back into system - power.state = 1 - time.sleep(2) - brk_fail.state = 1 - power.state = 0 - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Power On Brake Fail") - check.equal(bspd_ctrl.state, 0, "Power On Brake Fail") - - # Current no brake - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power() - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - time.sleep(2) # TODO: I am not sure why this fails, but oh well - set_bspd_current(c_sense, 75) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # time.sleep(100) - - # hil.check(bspd_ctrl.state == 1, "Current no brake") - check.equal(bspd_ctrl.state, 1, "Current no brake") - - # Current Sense Short to Ground - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power() - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - c_sense.state = 0.0 - t = utils.measure_trip_time(bspd_ctrl, 5.0, is_falling=True) - # hil.check(t < R_BSPD_MAX_TRIP_TIME_S, "Current short to ground") - check.less(t, R_BSPD_MAX_TRIP_TIME_S, "Current short to ground") - set_bspd_current(c_sense, 0.0) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Current short to ground stays latched") - check.equal(bspd_ctrl.state, 0, "Current short to ground stays latched") - - # Current Sense Short to 5V - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power() - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - c_sense.state = ABOX_DHAB_CH1_DIV.div(5.0) - t = utils.measure_trip_time(bspd_ctrl, 5.0, is_falling=True) - # hil.check(t < R_BSPD_MAX_TRIP_TIME_S, "Current short to 5V") - check.less(t, R_BSPD_MAX_TRIP_TIME_S, "Current short to 5V") - set_bspd_current(c_sense, 0.0) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Current short to 5V stays latched") - check.equal(bspd_ctrl.state, 0, "Current short to 5V stays latched") - - # Braking - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power() - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - brk_stat.state = 1 - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 1, "Brake no current") - check.equal(bspd_ctrl.state, 1, "Brake no current") - - # Lowest current required to trip at - min_trip_current = R_BSPD_POWER_THRESH_W / ACCUM_MAX_V - set_bspd_current(c_sense, min_trip_current) - t = utils.measure_trip_time(bspd_ctrl, 5.0, is_falling=True) - # hil.check(t < R_BSPD_MAX_TRIP_TIME_S, "Braking with current") - check.less(t, R_BSPD_MAX_TRIP_TIME_S, "Braking with current") - - # Measure braking with current threshold - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power() - brk_stat.state = 1 - - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - start = ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(0.0)) - stop = ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(DHAB_S124_CH1_MAX_A)) - step = 0.1 - thresh = utils.measure_trip_thresh(c_sense, start, stop, step, - R_BSPD_MAX_TRIP_TIME_S, - bspd_ctrl, is_falling=True) - thresh_amps = dhab_ch1_v_to_a(ABOX_DHAB_CH1_DIV.reverse(thresh)) - print(f"Current while braking threshold: {thresh}V = {thresh_amps}A") - # hil.check_within(thresh, ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(min_trip_current)), 0.1, "Current while braking threshold") - check.almost_equal( - thresh, ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(min_trip_current)), - abs=0.1, rel=0.0, - msg="Current while braking threshold" - ) - - # Determine the current sense short to gnd threshold - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power() - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - set_bspd_current(c_sense, DHAB_S124_CH1_MIN_A) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 1, "Min output current okay") - check.equal(bspd_ctrl.state, 1, "Min output current okay") - start = ABOX_DHAB_CH1_DIV.div(DHAB_S124_MIN_OUT_V) - stop = 0.0 - step = -0.1 - thresh = utils.measure_trip_thresh(c_sense, start, stop, step, - R_BSPD_MAX_TRIP_TIME_S, - bspd_ctrl, is_falling=True) - print(f"Short to ground threshold: {thresh}V") - # hil.check(stop < (thresh) < start, "Current short to ground threshold") - check.between(thresh, stop, start, "Current short to ground threshold") - - # Determine the current sense short to 5V threshold - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power() - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - time.sleep(2) - - set_bspd_current(c_sense, DHAB_S124_CH1_MAX_A) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 1, "Max output current okay") - check.equal(bspd_ctrl.state, 1, "Max output current okay") - start = ABOX_DHAB_CH1_DIV.div(DHAB_S124_MAX_OUT_V) - stop = ABOX_DHAB_CH1_DIV.div(5.0) - step = 0.01 - - thresh = utils.measure_trip_thresh(c_sense, start, stop, step, - R_BSPD_MAX_TRIP_TIME_S, - bspd_ctrl, is_falling=True) - print(f"Short to 5V threshold: {thresh}V") - # hil.check(bspd_ctrl.state == 0, "Short to 5V trips") - check.equal(bspd_ctrl.state, 0, "Short to 5V trips") - print(stop) - print(start) - # hil.check(start < (thresh) <= stop, "Current short to 5V threshold") - check.between(thresh, stop, start, "Current short to 5V threshold", le=True) - - # Floating current - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power() - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - c_sense.hiZ() - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Floating current") - check.equal(bspd_ctrl.state, 0, "Floating current") - - # Floating brake_fail - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power() - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - brk_fail.hiZ() - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Floating brake fail") - check.equal(bspd_ctrl.state, 0, "Floating brake fail") - - # Floating brake_stat - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power() - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - brk_stat.hiZ() - set_bspd_current(c_sense, min_trip_current) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Floating brake status") - check.equal(bspd_ctrl.state, 0, "Floating brake status") - - # End the test - # hil.end_test() -# ---------------------------------------------------------------------------- # - -# ---------------------------------------------------------------------------- # -IMD_RC_MIN_TRIP_TIME_S = IMD_STARTUP_TIME_S -IMD_RC_MAX_TRIP_TIME_S = R_IMD_MAX_TRIP_TIME_S - IMD_MEASURE_TIME_S -IMD_CTRL_OKAY = 1 -IMD_CTRL_TRIP = 0 - -def test_imd(hil): - # Begin the test - # hil.start_test(test_imd.__name__) - - # Outputs - imd_stat = hil.dout("Main_Module", "IMD_Status") - - # Outputs to set SDC status to okay - ams_stat = hil.dout("Main_Module", "BMS-Status-Main") - brk_fail = hil.dout("Main_Module", "Brake Fail") - brk_stat = hil.dout("Main_Module", "Brake Status") - c_sense = hil.aout("Main_Module", "Current Sense C1") - - # Inputs - imd_ctrl = hil.din("Main_Module", "SDC3") # assuming AMS and BSPD closed - - # Set other SDC nodes to okay - reset_ams(ams_stat) - reset_bspd(brk_fail, brk_stat, c_sense) - - # IMD Fault - reset_imd(imd_stat) - cycle_power() - # hil.check(imd_ctrl.state == IMD_CTRL_OKAY, "Power On") - check.equal(imd_ctrl.state, IMD_CTRL_OKAY, "Power On") - time.sleep(1) - imd_stat.state = IMD_STAT_TRIP - t = utils.measure_trip_time(imd_ctrl, R_IMD_MAX_TRIP_TIME_S, is_falling=True) - print(f"Target trip time: [{IMD_RC_MIN_TRIP_TIME_S}, {IMD_RC_MAX_TRIP_TIME_S}]") - # hil.check(IMD_RC_MIN_TRIP_TIME_S < t < IMD_RC_MAX_TRIP_TIME_S, "IMD Trip Time") - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Trip") - check.between(t, IMD_RC_MIN_TRIP_TIME_S, IMD_RC_MAX_TRIP_TIME_S, "IMD Trip Time") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Trip") - imd_stat.state = IMD_STAT_OKAY - time.sleep(IMD_RC_MAX_TRIP_TIME_S * 1.1) - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Fault Stays Latched") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Fault Stays Latched") - - # IMD Fault on Power On - reset_imd(imd_stat) - imd_stat.state = IMD_STAT_TRIP - cycle_power() - time.sleep(IMD_RC_MAX_TRIP_TIME_S) - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Fault Power On") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Fault Power On") - - # IMD Floating - reset_imd(imd_stat) - imd_stat.hiZ() - cycle_power() - t = utils.measure_trip_time(imd_ctrl, R_IMD_MAX_TRIP_TIME_S, is_falling=True) - # hil.check(t < R_IMD_MAX_TRIP_TIME_S, "IMD Floating Trip Time") - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Floating Trip") - check.between(t, 0, R_IMD_MAX_TRIP_TIME_S, "IMD Floating Trip Time") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Floating Trip") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - -# ---------------------------------------------------------------------------- # -def test_ams(hil): - # Begin the test - # hil.start_test(test_ams.__name__) - - # Outputs - ams_stat = hil.dout("Main_Module", "BMS-Status-Main") - - # Outputs to set SDC status to okay - imd_stat = hil.dout("Main_Module", "IMD_Status") - brk_fail = hil.dout("Main_Module", "Brake Fail") - brk_stat = hil.dout("Main_Module", "Brake Status") - c_sense = hil.aout("Main_Module", "Current Sense C1") - - # Inputs - ams_ctrl = hil.din("Main_Module", "SDC3") # assumes IMD and BSPD closed - - # Set other SDC nodes to okay - reset_imd(imd_stat) - reset_bspd(brk_fail, brk_stat, c_sense) - - # AMS Fault - reset_ams(ams_stat) - cycle_power() - # hil.check(ams_ctrl.state == AMS_CTRL_OKAY, "Power On") - check.equal(ams_ctrl.state, AMS_CTRL_OKAY, "Power On") - time.sleep(1) - ams_stat.state = AMS_STAT_TRIP - t = utils.measure_trip_time(ams_ctrl, AMS_MAX_TRIP_DELAY_S * 2, is_falling=True) - # hil.check(0 < t < AMS_MAX_TRIP_DELAY_S, "AMS Trip Time") - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Trip") - check.between(t, 0, AMS_MAX_TRIP_DELAY_S, "AMS Trip Time") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Trip") - ams_stat.state = AMS_STAT_OKAY - time.sleep(AMS_MAX_TRIP_DELAY_S * 1.1) - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Fault Stays Latched") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Fault Stays Latched") - - # AMS Fault on Power On - reset_ams(ams_stat) - ams_stat.state = AMS_STAT_TRIP - cycle_power() - time.sleep(AMS_MAX_TRIP_DELAY_S) - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Fault Power On") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Fault Power On") - - # AMS Floating - reset_ams(ams_stat) - ams_stat.hiZ() - cycle_power() - t = utils.measure_trip_time(ams_ctrl, AMS_MAX_TRIP_DELAY_S * 2, is_falling=True) - # hil.check(0 < t < AMS_MAX_TRIP_DELAY_S, "AMS Floating Trip Time") - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Floating Trip") - check.between(t, 0, AMS_MAX_TRIP_DELAY_S, "AMS Floating Trip Time") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Floating Trip") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - -# ---------------------------------------------------------------------------- # -def test_tsal(hil): - - # Begin the test - # hil.start_test(test_tsal.__name__) - - # Outputs - v_mc = hil.aout("Main_Module", "Voltage MC Transducer") - - # Inputs - tsal = hil.din("Main_Module", "TSAL+") - lval = hil.din("Main_Module", "LVAL+") - - # Initial State - reset_tsal(v_mc) - time.sleep(0.2) - # No need to power cycle - - # hil.check(lval.state == 1, "LVAL on at v_mc = 0") - # hil.check(tsal.state == 0, "TSAL off at v_mc = 0") - check.equal(lval.state, 1, "LVAL on at v_mc = 0") - check.equal(tsal.state, 0, "TSAL off at v_mc = 0") - - time.sleep(5) - # hil.check(lval.state == 1, "LVAL stays on") - check.equal(lval.state, 1, "LVAL stays on") - - v_mc.state = tiff_hv_to_lv(ACCUM_MIN_V) - time.sleep(0.1) - - # hil.check(lval.state == 0, f"LVAL off at {ACCUM_MIN_V:.4} V") - # hil.check(tsal.state == 1, f"TSAL on at {ACCUM_MIN_V:.4} V") - check.equal(lval.state, 0, f"LVAL off at {ACCUM_MIN_V:.4} V") - check.equal(tsal.state, 1, f"TSAL on at {ACCUM_MIN_V:.4} V") - - reset_tsal(v_mc) - time.sleep(0.2) - # hil.check(lval.state == 1, f"LVAL turns back on") - check.equal(lval.state, 1, f"LVAL turns back on") - - start = tiff_hv_to_lv(0.0) - stop = tiff_hv_to_lv(R_TSAL_HV_V * 1.5) - step = tiff_hv_to_lv(1) - thresh = utils.measure_trip_thresh(v_mc, start, stop, step, - 0.01, - tsal, is_falling=False) - thresh = tiff_lv_to_hv(thresh) - print(f"TSAL on at {thresh:.4} V") - # hil.check_within(thresh, R_TSAL_HV_V, 4, f"TSAL trips at {R_TSAL_HV_V:.4} +-4") - # hil.check(lval.state == 0, f"LVAL off V") - # hil.check(tsal.state == 1, f"TSAL on V") - check.almost_equal(thresh, R_TSAL_HV_V, abs=4, rel=0.0, msg=f"TSAL trips at {R_TSAL_HV_V:.4} +-4") - check.equal(lval.state, 0, f"LVAL off V") - check.equal(tsal.state, 1, f"TSAL on V") - - # hil.end_test() -# ---------------------------------------------------------------------------- # \ No newline at end of file diff --git a/scripts/test_main_sdc.py b/scripts/test_main_sdc.py deleted file mode 100644 index c084436..0000000 --- a/scripts/test_main_sdc.py +++ /dev/null @@ -1,367 +0,0 @@ -from os import sys, path -# adds "./HIL-Testing" to the path, basically making it so these scripts were run one folder level higher -sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) - -from hil.hil import HIL -import hil.utils as utils -import time -from rules_constants import * -from vehicle_constants import * - -import pytest_check as check -import pytest - - -# ---------------------------------------------------------------------------- # -@pytest.fixture(scope="session") -def hil(): - hil_instance = HIL() - - hil_instance.load_config("config_main_sdc_bench.json") - hil_instance.load_pin_map("per_24_net_map.csv", "stm32f407_pin_map.csv") - - # hil_instance.init_can() - - yield hil_instance - - hil_instance.shutdown() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -# SETUP for CSENSE -# Current Sensor (DHAB) -> ABOX V Divider -> MAIN_SDC - -DAC_GAIN = 1 + 4.7 / 4.7 - 0.05 - -def cycle_power(pow): - pow.state = 0 - time.sleep(0.75) - pow.state = 1 - -def set_bspd_current(ch1, current): - v = ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(current)) - ch1.state = v / DAC_GAIN - -def reset_bspd(fail, stat, ch1): - fail.state = 0 - stat.state = 0 - set_bspd_current(ch1, 0.0) - - -# ---------------------------------------------------------------------------- # -def test_bspd(hil): - # Begin the test - # hil.start_test(test_bspd.__name__) - - # Outputs - brk_fail = hil.dout("MainSDC", "Brake Fail") - brk_stat = hil.dout("MainSDC", "Brake Status") - c_sense = hil.aout("MainSDC", "Current Sense C1") - pow = hil.dout("MainSDC", "5V_Crit") - - # Inputs - bspd_ctrl = hil.din("MainSDC", "BSPD_Control") - - # Brake Fail - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power(pow) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - brk_fail.state = 1 - t = utils.measure_trip_time(bspd_ctrl, 5.0, is_falling=True) - # hil.check(t < R_BSPD_MAX_TRIP_TIME_S, "Brake Fail") - check.below(t, R_BSPD_MAX_TRIP_TIME_S, "Brake Fail") - brk_fail.state = 0 - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Brake Fail Stays Latched") - check.equal(bspd_ctrl.state, 0, "Brake Fail Stays Latched") - - # Brake Fail on Power On - reset_bspd(brk_fail, brk_stat, c_sense) - brk_fail.state = 1 - cycle_power(pow) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Power On Brake Fail") - check.equal(bspd_ctrl.state, 0, "Power On Brake Fail") - - # Current no brake - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power(pow) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - time.sleep(2) # TODO: I am not sure why this fails, but oh well - set_bspd_current(c_sense, 75) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # time.sleep(100) - - # hil.check(bspd_ctrl.state == 1, "Current no brake") - check.equal(bspd_ctrl.state, 1, "Current no brake") - - # Current Sense Short to Ground - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power(pow) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - c_sense.state = 0.0 - t = utils.measure_trip_time(bspd_ctrl, 5.0, is_falling=True) - # hil.check(t < R_BSPD_MAX_TRIP_TIME_S, "Current short to ground") - check.below(t, R_BSPD_MAX_TRIP_TIME_S, "Current short to ground") - set_bspd_current(c_sense, 0.0) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Current short to ground stays latched") - check.equal(bspd_ctrl.state, 0, "Current short to ground stays latched") - - # Current Sense Short to 5V - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power(pow) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - c_sense.state = ABOX_DHAB_CH1_DIV.div(5.0) / DAC_GAIN - t = utils.measure_trip_time(bspd_ctrl, 5.0, is_falling=True) - # hil.check(t < R_BSPD_MAX_TRIP_TIME_S, "Current short to 5V") - check.below(t, R_BSPD_MAX_TRIP_TIME_S, "Current short to 5V") - set_bspd_current(c_sense, 0.0) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Current short to 5V stays latched") - check.equal(bspd_ctrl.state, 0, "Current short to 5V stays latched") - - # Braking - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power(pow) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - brk_stat.state = 1 - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 1, "Brake no current") - check.equal(bspd_ctrl.state, 1, "Brake no current") - - # Lowest current required to trip at - min_trip_current = R_BSPD_POWER_THRESH_W / ACCUM_MAX_V - set_bspd_current(c_sense, min_trip_current) - t = utils.measure_trip_time(bspd_ctrl, 5.0, is_falling=True) - # hil.check(t < R_BSPD_MAX_TRIP_TIME_S, "Braking with current") - check.below(t, R_BSPD_MAX_TRIP_TIME_S, "Braking with current") - - # Measure braking with current threshold - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power(pow) - brk_stat.state = 1 - - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - start = ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(0.0)) / DAC_GAIN - stop = ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(DHAB_S124_CH1_MAX_A)) / DAC_GAIN - step = 0.1 / DAC_GAIN - thresh = utils.measure_trip_thresh(c_sense, start, stop, step, - R_BSPD_MAX_TRIP_TIME_S, - bspd_ctrl, is_falling=True) - thresh *= DAC_GAIN - thresh_amps = dhab_ch1_v_to_a(ABOX_DHAB_CH1_DIV.reverse(thresh)) - print(f"Current while braking threshold: {thresh}V = {thresh_amps}A") - # hil.check_within(thresh, ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(min_trip_current)), 0.1, "Current while braking threshold") - check.within(thresh, ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(min_trip_current)), 0.1, "Current while braking threshold") - - # Determine the current sense short to gnd threshold - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power(pow) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - set_bspd_current(c_sense, DHAB_S124_CH1_MIN_A) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 1, "Min output current okay") - check.equal(bspd_ctrl.state, 1, "Min output current okay") - start = ABOX_DHAB_CH1_DIV.div(DHAB_S124_MIN_OUT_V) / DAC_GAIN - stop = 0.0 - step = -0.1 / DAC_GAIN - thresh = utils.measure_trip_thresh(c_sense, start, stop, step, - R_BSPD_MAX_TRIP_TIME_S, - bspd_ctrl, is_falling=True) - thresh *= DAC_GAIN - print(f"Short to ground threshold: {thresh}V") - # hil.check(stop < (thresh / DAC_GAIN) < start, "Current short to ground threshold") - check.between(thresh / DAC_GAIN, stop, start, "Current short to ground threshold") - - # Determine the current sense short to 5V threshold - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power(pow) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - time.sleep(2) - - set_bspd_current(c_sense, DHAB_S124_CH1_MAX_A) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 1, "Max output current okay") - check.equal(bspd_ctrl.state, 1, "Max output current okay") - start = ABOX_DHAB_CH1_DIV.div(DHAB_S124_MAX_OUT_V) / DAC_GAIN - stop = ABOX_DHAB_CH1_DIV.div(5.0) / DAC_GAIN - step = 0.01 / DAC_GAIN - - thresh = utils.measure_trip_thresh(c_sense, start, stop, step, - R_BSPD_MAX_TRIP_TIME_S, - bspd_ctrl, is_falling=True) - thresh *= DAC_GAIN - print(f"Short to 5V threshold: {thresh}V") - # hil.check(bspd_ctrl.state == 0, "Short to 5V trips") - check.equal(bspd_ctrl.state, 0, "Short to 5V trips") - print(stop * DAC_GAIN) - print(start * DAC_GAIN) - # hil.check(start < (thresh / DAC_GAIN) <= stop, "Current short to 5V threshold") - check.between(thresh / DAC_GAIN, stop, start, "Current short to 5V threshold") - - # Floating current - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power(pow) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - c_sense.hiZ() - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Floating current") - check.equal(bspd_ctrl.state, 0, "Floating current") - - # Floating brake_fail - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power(pow) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - brk_fail.hiZ() - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Floating brake fail") - check.equal(bspd_ctrl.state, 0, "Floating brake fail") - - # Floating brake_stat - reset_bspd(brk_fail, brk_stat, c_sense) - cycle_power(pow) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - brk_stat.hiZ() - set_bspd_current(c_sense, min_trip_current) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Floating brake status") - check.equal(bspd_ctrl.state, 0, "Floating brake status") - - # End the test - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -IMD_RC_MIN_TRIP_TIME_S = IMD_STARTUP_TIME_S -IMD_RC_MAX_TRIP_TIME_S = R_IMD_MAX_TRIP_TIME_S - IMD_MEASURE_TIME_S -IMD_STAT_OKAY = 1 -IMD_STAT_TRIP = 0 -IMD_CTRL_OKAY = 1 -IMD_CTRL_TRIP = 0 - -def reset_imd(imd_stat): - imd_stat.state = IMD_STAT_OKAY - - -def test_imd(hil): - # Begin the test - # hil.start_test(test_imd.__name__) - - # Outputs - imd_stat = hil.dout("MainSDC", "IMD_Status") - pow = hil.dout("MainSDC", "5V_Crit") - - # Inputs - imd_ctrl = hil.din("MainSDC", "IMD_Control") - - # IMD Fault - reset_imd(imd_stat) - cycle_power(pow) - # hil.check(imd_ctrl.state == IMD_CTRL_OKAY, "Power On") - check.equal(imd_ctrl.state, IMD_CTRL_OKAY, "Power On") - time.sleep(1) - imd_stat.state = IMD_STAT_TRIP - t = utils.measure_trip_time(imd_ctrl, R_IMD_MAX_TRIP_TIME_S, is_falling=True) - # hil.check(IMD_RC_MIN_TRIP_TIME_S < t < IMD_RC_MAX_TRIP_TIME_S, "IMD Trip Time") - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Trip") - check.between(t, IMD_RC_MIN_TRIP_TIME_S, IMD_RC_MAX_TRIP_TIME_S, "IMD Trip Time") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Trip") - imd_stat.state = IMD_STAT_OKAY - time.sleep(IMD_RC_MAX_TRIP_TIME_S * 1.1) - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Fault Stays Latched") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Fault Stays Latched") - - # IMD Fault on Power On - reset_imd(imd_stat) - imd_stat.state = IMD_STAT_TRIP - cycle_power(pow) - time.sleep(IMD_RC_MAX_TRIP_TIME_S) - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Fault Power On") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Fault Power On") - - # IMD Floating - reset_imd(imd_stat) - imd_stat.hiZ() - cycle_power(pow) - t = utils.measure_trip_time(imd_ctrl, R_IMD_MAX_TRIP_TIME_S, is_falling=True) - # hil.check(t < R_IMD_MAX_TRIP_TIME_S, "IMD Floating Trip Time") - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Floating Trip") - check.below(t, R_IMD_MAX_TRIP_TIME_S, "IMD Floating Trip Time") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Floating Trip") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -AMS_STAT_OKAY = 1 -AMS_STAT_TRIP = 0 -AMS_CTRL_OKAY = 1 -AMS_CTRL_TRIP = 0 - -def reset_ams(ams_stat): - ams_stat.state = AMS_STAT_OKAY - - -def test_ams(hil): - # Begin the test - # hil.start_test(test_ams.__name__) - - # Outputs - ams_stat = hil.dout("MainSDC", "BMS-Status-Main") - pow = hil.dout("MainSDC", "5V_Crit") - - # Inputs - ams_ctrl = hil.din("MainSDC", "BMS_Control") - - # AMS Fault - reset_ams(ams_stat) - cycle_power(pow) - # hil.check(ams_ctrl.state == AMS_CTRL_OKAY, "Power On") - check.equal(ams_ctrl.state, AMS_CTRL_OKAY, "Power On") - time.sleep(1) - ams_stat.state = AMS_STAT_TRIP - t = utils.measure_trip_time(ams_ctrl, AMS_MAX_TRIP_DELAY_S * 2, is_falling=True) - # hil.check(0 < t < AMS_MAX_TRIP_DELAY_S, "AMS Trip Time") - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Trip") - check.between(t, 0, AMS_MAX_TRIP_DELAY_S, "AMS Trip Time") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Trip") - ams_stat.state = AMS_STAT_OKAY - time.sleep(AMS_MAX_TRIP_DELAY_S * 1.1) - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Fault Stays Latched") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Fault Stays Latched") - - # AMS Fault on Power On - reset_ams(ams_stat) - ams_stat.state = AMS_STAT_TRIP - cycle_power(pow) - time.sleep(AMS_MAX_TRIP_DELAY_S) - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Fault Power On") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Fault Power On") - - # AMS Floating - reset_ams(ams_stat) - ams_stat.hiZ() - cycle_power(pow) - t = utils.measure_trip_time(ams_ctrl, AMS_MAX_TRIP_DELAY_S * 2, is_falling=True) - # hil.check(0 < t < AMS_MAX_TRIP_DELAY_S, "AMS Floating Trip Time") - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Floating Trip") - check.between(t, 0, AMS_MAX_TRIP_DELAY_S, "AMS Floating Trip Time", ge=True) - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Floating Trip") - - # hil.end_test() -# ---------------------------------------------------------------------------- # \ No newline at end of file diff --git a/scripts/test_system.py b/scripts/test_system.py deleted file mode 100644 index 62202b2..0000000 --- a/scripts/test_system.py +++ /dev/null @@ -1,859 +0,0 @@ -from os import sys, path -# adds "./HIL-Testing" to the path, basically making it so these scripts were run one folder level higher -sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) - -from hil.hil import HIL -import hil.utils as utils -import time -from rules_constants import * -from vehicle_constants import * - -import pytest_check as check -import pytest - - -# ---------------------------------------------------------------------------- # -AMS_STAT_OKAY = 1 -AMS_STAT_TRIP = 0 -AMS_CTRL_OKAY = 1 -AMS_CTRL_TRIP = 0 - -def reset_ams(ams_stat): - ams_stat.state = AMS_STAT_OKAY - -def set_bspd_current(ch1, current): - c = ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(current)) - #print(f"bspd current: {current} voltage: {c}") - ch1.state = c - -def reset_bspd(brk1, brk2, ch1): - brk1.state = BRK_1_REST_V - brk2.state = BRK_2_REST_V - set_bspd_current(ch1, 0.0) - -IMD_STAT_OKAY = 1 -IMD_STAT_TRIP = 0 -def reset_imd(imd_stat): - imd_stat.state = IMD_STAT_OKAY - -def reset_pchg(v_bat, v_mc): - print(f"Setting v_bat to {tiff_hv_to_lv(ACCUM_NOM_V)}") - v_bat.state = tiff_hv_to_lv(ACCUM_NOM_V) - print(f"Setting v_mc to {tiff_hv_to_lv(0.0)}") - v_mc.state = tiff_hv_to_lv(0.0) - -def reset_tsal(v_mc): - v_mc.state = tiff_hv_to_lv(0.0) - -power = None - -CYCLE_POWER_OFF_DELAY = 2.0 -CYCLE_POWER_ON_DELAY = 3.0 - -def cycle_power(): - power.state = 1 - time.sleep(CYCLE_POWER_OFF_DELAY) - power.state = 0 - time.sleep(CYCLE_POWER_ON_DELAY) -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -@pytest.fixture(scope="session") -def hil(): - global power - - hil_instance = HIL() - - hil_instance.load_config("config_charger.json") - hil_instance.load_pin_map("per_24_net_map.csv", "stm32f407_pin_map.csv") - - # hil_instance.init_can() - - power = hil_instance.dout("RearTester", "RLY1") - - yield hil_instance - - hil_instance.shutdown() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_precharge(hil): - # Begin the test - # hil.start_test(test_precharge.__name__) - - # Outputs - v_bat = hil.aout("Main_Module", "VBatt") - v_mc = hil.aout("Main_Module", "Voltage MC Transducer") - - # Inputs - pchg_cmplt = hil.mcu_pin("Main_Module", "PrechargeComplete_Prot") - not_pchg_cmplt_delayed = hil.din("Main_Module", "NotPrechargeCompleteSchmitt") - # v_bat_mcu = hil.daq_var("Main_Module", "Varname") # TODO - # v_mc_mcu = hil.daq_var("Main_Module", "Varname") # TODO - # pchg_mux = hil.daq_var("Main_Module", "Varname") # TODO - - # Outputs to set SDC status to okay - imd_stat = hil.dout("Main_Module", "IMD_Status") - brk1 = hil.aout("Dashboard", "BRK1_RAW") - brk2 = hil.aout("Dashboard", "BRK2_RAW") - c_sense = hil.aout("Main_Module", "Current Sense C1") - ams_stat = hil.dout("Main_Module", "BMS-Status-Main") - reset_imd(imd_stat) - reset_bspd(brk1, brk2, c_sense) - reset_ams(ams_stat) - - cycle_power() - - # Initial State - reset_pchg(v_bat, v_mc) - time.sleep(2.5) - # hil.check(pchg_cmplt.state == 0, "Precharge not complete on startup") - # hil.check(not_pchg_cmplt_delayed.state == 1, "Not precharge complete delayed high on startup") - check.equal(pchg_cmplt.state, 0, "Precharge not complete on startup") - check.equal(not_pchg_cmplt_delayed.state, 1, "Not precharge complete delayed high on startup") - # Check delay - v_mc.state = tiff_hv_to_lv(ACCUM_NOM_V) - t = utils.measure_trip_time(not_pchg_cmplt_delayed, PCHG_COMPLETE_DELAY_S*3, is_falling=True) - # hil.check(not_pchg_cmplt_delayed.state == 0, "Precharge complete delayed") - # hil.check_within(t, PCHG_COMPLETE_DELAY_S, 0.25, f"Precharge complete delay of {t:.3}s close to expected {PCHG_COMPLETE_DELAY_S}s") - check.equal(not_pchg_cmplt_delayed.state, 0, "Precharge complete delayed") - check.almost_equal(t, PCHG_COMPLETE_DELAY_S, abs=0.25, rel=0.0, msg=f"Precharge complete delay of {t:.3}s close to expected {PCHG_COMPLETE_DELAY_S}s") - - # Find threshold at nominal pack voltage - for v in [ACCUM_MIN_V, ACCUM_NOM_V, ACCUM_MAX_V]: - reset_pchg(v_bat, v_mc) - print(f"Testing precharge threshold at V_bat = {v}") - v_bat.state = tiff_hv_to_lv(v) - v_mc.state = tiff_hv_to_lv(v*0.8) - #time.sleep(0.01) - time.sleep(0.5) - # hil.check(pchg_cmplt.state == 0, "Precharge Complete Low at Initial State") - check.equal(pchg_cmplt.state, 0, "Precharge Complete Low at Initial State") - - start = tiff_hv_to_lv(v*0.8) - stop = tiff_hv_to_lv(v) - step = tiff_hv_to_lv(1) - thresh = utils.measure_trip_thresh(v_mc, start, stop, step, 0.1, - pchg_cmplt, is_falling=0) - thresh_hv = tiff_lv_to_hv(thresh) - print(f"Precharge triggered at {thresh_hv / v * 100:.4}% ({thresh_hv:.5}V) of vbat={v}.") - # hil.check_within(thresh_hv / v, R_PCHG_V_BAT_THRESH, 0.03, f"Precharge threshold of {R_PCHG_V_BAT_THRESH*100}% at vbat = {v}V") - check.almost_equal(thresh_hv / v, R_PCHG_V_BAT_THRESH, abs=0.03, rel=0.0, msg=f"Precharge threshold of {R_PCHG_V_BAT_THRESH*100}% at vbat = {v}V") - v_mc.state = tiff_hv_to_lv(v) - #time.sleep(0.25) - time.sleep(8) - # hil.check(pchg_cmplt.state == 1, f"Precharge completed at vbat = {v}V") - check.equal(pchg_cmplt.state, 1, f"Precharge completed at vbat = {v}V") - - - # Floating conditions (check never precharge complete) - reset_pchg(v_bat, v_mc) - v_bat.hiZ() - v_mc.state = tiff_hv_to_lv(0) - # hil.check(pchg_cmplt.state == 0, "Precharge not complete on v_bat float, v_mc 0V") - check.equal(pchg_cmplt.state, 0, "Precharge not complete on v_bat float, v_mc 0V") - v_mc.state = tiff_hv_to_lv(ACCUM_MAX_V) - # hil.check(pchg_cmplt.state == 0, "Precharge not complete on v_bat float, v_mc max V") - check.equal(pchg_cmplt.state, 0, "Precharge not complete on v_bat float, v_mc max V") - - reset_pchg(v_bat, v_mc) - v_mc.hiZ() - v_bat.state = tiff_hv_to_lv(ACCUM_MIN_V) - # hil.check(pchg_cmplt.state == 0, "Precharge not complete on v_bat min, v_mc float") - check.equal(pchg_cmplt.state, 0, "Precharge not complete on v_bat min, v_mc float") - v_bat.state = tiff_hv_to_lv(ACCUM_MAX_V) - # hil.check(pchg_cmplt.state == 0, "Precharge not complete on v_bat max, v_mc float") - check.equal(pchg_cmplt.state, 0, "Precharge not complete on v_bat max, v_mc float") - - reset_pchg(v_bat, v_mc) - v_bat.hiZ() - v_mc.hiZ() - # hil.check(pchg_cmplt.state == 0, "Precharge not complete on v_bat float, v_mc float") - check.equal(pchg_cmplt.state, 0, "Precharge not complete on v_bat float, v_mc float") - - # TODO: software precharge validity checks (make precharge take forever) - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -BRK_SWEEP_DELAY = 0.1 -BSPD_DASH_ON_TIME = 0 - -def test_bspd(hil): - # Begin the test - # hil.start_test(test_bspd.__name__) - - # Outputs - brk1 = hil.aout("Dashboard", "BRK1_RAW") - brk2 = hil.aout("Dashboard", "BRK2_RAW") - c_sense = hil.aout("Main_Module", "Current Sense C1") - - # Outputs to set SDC status to okay - ams_stat = hil.dout("Main_Module", "BMS-Status-Main") - imd_stat = hil.dout("Main_Module", "IMD_Status") - - # Inputs - bspd_ctrl = hil.din("Main_Module", "SDC15") # assuming AMS and BSPD closed - - brk_fail_tap = hil.mcu_pin("Dashboard", "BRK_FAIL_TAP") - brk_stat_tap = hil.mcu_pin("Dashboard", "BRK_STAT_TAP") - - # Set other SDC nodes to okay - reset_ams(ams_stat) - reset_imd(imd_stat) - # BOTS assumed to be good - - # Brake threshold check - brk1.state = BRK_1_REST_V - brk2.state = BRK_2_REST_V - # hil.check(brk_stat_tap.state == 0, "Brake stat starts low") - check.equal(brk_stat_tap.state, 0, "Brake stat starts low") - brk1.state = BRK_1_THRESH_V - time.sleep(0.1) - # hil.check(brk_stat_tap.state == 1, "Brake stat goes high at brk 1 thresh") - check.equal(brk_stat_tap.state, 1, "Brake stat goes high at brk 1 thresh") - brk1.state = BRK_1_REST_V - # hil.check(brk_stat_tap.state == 0, "Brake stat starts low") - check.equal(brk_stat_tap.state, 0, "Brake stat starts low") - brk2.state = BRK_2_THRESH_V - time.sleep(0.1) - # hil.check(brk_stat_tap.state == 1, "Brake stat goes high at brk 2 thresh") - check.equal(brk_stat_tap.state, 1, "Brake stat goes high at brk 2 thresh") - brk1.state = BRK_1_THRESH_V - # hil.check(brk_stat_tap.state == 1, "Brake stat stays high for both brakes") - check.equal(brk_stat_tap.state, 1, "Brake stat stays high for both brakes") - - # Brake threshold scan - brk1.state = BRK_MIN_OUT_V - brk2.state = BRK_MIN_OUT_V - time.sleep(0.1) - # hil.check(brk_stat_tap.state == 0, "Brake Stat Starts Low Brk 1") - check.equal(brk_stat_tap.state, 0, "Brake Stat Starts Low Brk 1") - - start = BRK_MIN_OUT_V - stop = BRK_MAX_OUT_V - step = 0.1 - - thresh = utils.measure_trip_thresh(brk1, start, stop, step, - BRK_SWEEP_DELAY, - brk_stat_tap, is_falling=False) - print(f"Brake 1 braking threshold: {thresh}") - # hil.check_within(thresh, BRK_1_THRESH_V, 0.2, "Brake 1 trip voltage") - # hil.check(brk_stat_tap.state == 1, "Brake Stat Tripped for Brk 1") - check.almost_equal(thresh, BRK_1_THRESH_V, abs=0.2, rel=0.0, msg="Brake 1 trip voltage") - check.equal(brk_stat_tap.state, 1, "Brake Stat Tripped for Brk 1") - - brk1.state = BRK_MIN_OUT_V - brk2.state = BRK_MIN_OUT_V - # hil.check(brk_stat_tap.state == 0, "Brake Stat Starts Low Brk 2") - check.equal(brk_stat_tap.state, 0, "Brake Stat Starts Low Brk 2") - thresh = utils.measure_trip_thresh(brk2, start, stop, step, - BRK_SWEEP_DELAY, - brk_stat_tap, is_falling=False) - print(f"Brake 2 braking threshold: {thresh}") - # hil.check_within(thresh, BRK_2_THRESH_V, 0.2, "Brake 2 trip voltage") - # hil.check(brk_stat_tap.state == 1, "Brake Stat Tripped for Brk 2") - check.almost_equal(thresh, BRK_2_THRESH_V, abs=0.2, rel=0.0, msg="Brake 2 trip voltage") - check.equal(brk_stat_tap.state, 1, "Brake Stat Tripped for Brk 2") - - # Brake Fail scan - brk1.state = BRK_1_REST_V - brk2.state = BRK_2_REST_V - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 0, "Brake Fail Check 1 Starts 0") - check.equal(brk_fail_tap.state, 0, "Brake Fail Check 1 Starts 0") - - brk1.state = 0.0 # Force 0 - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 1, "Brake Fail Brk 1 Short GND") - check.equal(brk_fail_tap.state, 1, "Brake Fail Brk 1 Short GND") - - brk1.state = BRK_1_REST_V - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 0, "Brake Fail Check 2 Starts 0") - check.equal(brk_fail_tap.state, 0, "Brake Fail Check 2 Starts 0") - - brk2.state = 0.0 # Force 0 - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 1, "Brake Fail Brk 2 Short GND") - check.equal(brk_fail_tap.state, 1, "Brake Fail Brk 2 Short GND") - - brk2.state = BRK_2_REST_V - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 0, "Brake Fail Check 3 Starts 0") - check.equal(brk_fail_tap.state, 0, "Brake Fail Check 3 Starts 0") - - brk1.state = 5.0 # Short VCC - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 1, "Brake Fail Brk 1 Short VCC") - check.equal(brk_fail_tap.state, 1, "Brake Fail Brk 1 Short VCC") - - brk1.state = BRK_1_REST_V - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 0, "Brake Fail Check 4 Starts 0") - check.equal(brk_fail_tap.state, 0, "Brake Fail Check 4 Starts 0") - - brk2.state = 5.0 # Short VCC - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 1, "Brake Fail Brk 2 Short VCC") - check.equal(brk_fail_tap.state, 1, "Brake Fail Brk 2 Short VCC") - - brk2.state = BRK_2_REST_V - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 0, "Brake Fail Check 5 Starts 0") - check.equal(brk_fail_tap.state, 0, "Brake Fail Check 5 Starts 0") - - brk1.hiZ() - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 1, "Brake Fail Brk 1 Hi-Z") - check.equal(brk_fail_tap.state, 1, "Brake Fail Brk 1 Hi-Z") - - brk1.state = BRK_1_REST_V - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 0, "Brake Fail Check 6 Starts 0") - check.equal(brk_fail_tap.state, 0, "Brake Fail Check 6 Starts 0") - - brk2.hiZ() - time.sleep(0.1) - # hil.check(brk_fail_tap.state == 1, "Brake Fail Brk 2 Hi-Z") - check.equal(brk_fail_tap.state, 1, "Brake Fail Brk 2 Hi-Z") - - brk2.state = BRK_2_REST_V - - # Brake Fail - reset_bspd(brk1, brk2, c_sense) - cycle_power() - time.sleep(BSPD_DASH_ON_TIME) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - - brk1.state = 0.0 - t = utils.measure_trip_time(bspd_ctrl, 5.0, is_falling=True) - # hil.check(t < R_BSPD_MAX_TRIP_TIME_S, "Brake Fail") - # hil.check(brk_fail_tap.state == 1, "Brake Fail went high") - check.less(t, R_BSPD_MAX_TRIP_TIME_S, "Brake Fail") - check.equal(brk_fail_tap.state, 1, "Brake Fail went high") - - brk1.state = BRK_1_REST_V - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(brk_fail_tap.state == 0, "Brake Fail returned low") - # hil.check(bspd_ctrl.state == 0, "Brake Fail Stays Latched") - check.equal(brk_fail_tap.state, 0, "Brake Fail returned low") - check.equal(bspd_ctrl.state, 0, "Brake Fail Stays Latched") - - # Brake Fail on Power On - reset_bspd(brk1, brk2, c_sense) - # Manual power cycle, setting brk_fail on before - # Will cause power to feed back into system - power.state = 1 - time.sleep(2) - brk1.state = 0.0 - power.state = 0 - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - time.sleep(BSPD_DASH_ON_TIME) # NOTE: test can't check for the trip time - # hil.check(bspd_ctrl.state == 0, "Power On Brake Fail") - check.equal(bspd_ctrl.state, 0, "Power On Brake Fail") - - # Current no brake - reset_bspd(brk1, brk2, c_sense) - power.state = 1 - time.sleep(3) - power.state = 0 - time.sleep(1) - #hil.check(bspd_ctrl.state == 1, "Power On") - time.sleep(2) # TODO: I am not sure why this fails, but oh well - set_bspd_current(c_sense, 75) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # time.sleep(100) - time.sleep(3) - # hil.check(bspd_ctrl.state == 1, "Current no brake") - check.equal(bspd_ctrl.state, 1, "Current no brake") - - # Current Sense Short to Ground - reset_bspd(brk1, brk2, c_sense) - cycle_power() - time.sleep(BSPD_DASH_ON_TIME) - # hil.check(bspd_ctrl.state == 1, "Power On" - check.equal(bspd_ctrl.state, 1, "Power On") - - c_sense.state = 0.0 - t = utils.measure_trip_time(bspd_ctrl, 5.0, is_falling=True) - # hil.check(t < R_BSPD_MAX_TRIP_TIME_S, "Current short to ground") - check.less(t, R_BSPD_MAX_TRIP_TIME_S, "Current short to ground") - set_bspd_current(c_sense, 0.0) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Current short to ground stays latched") - check.equal(bspd_ctrl.state, 0, "Current short to ground stays latched") - - # Current Sense Short to 5V - reset_bspd(brk1, brk2, c_sense) - cycle_power() - time.sleep(BSPD_DASH_ON_TIME*1.2) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - c_sense.state = ABOX_DHAB_CH1_DIV.div(5.0) - t = utils.measure_trip_time(bspd_ctrl, 5.0, is_falling=True) - # hil.check(t < R_BSPD_MAX_TRIP_TIME_S, "Current short to 5V") - check.less(t, R_BSPD_MAX_TRIP_TIME_S, "Current short to 5V") - set_bspd_current(c_sense, 0.0) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Current short to 5V stays latched") - check.equal(bspd_ctrl.state, 0, "Current short to 5V stays latched") - - # Braking - reset_bspd(brk1, brk2, c_sense) - cycle_power() - time.sleep(BSPD_DASH_ON_TIME) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - brk1.state = BRK_1_THRESH_V - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 1, "Brake no current") - # hil.check(brk_stat_tap.state == 1, "Brake stat went high") - check.equal(bspd_ctrl.state, 1, "Brake no current") - check.equal(brk_stat_tap.state, 1, "Brake stat went high") - - # Lowest current required to trip at - min_trip_current = R_BSPD_POWER_THRESH_W / ACCUM_MAX_V - set_bspd_current(c_sense, min_trip_current) - t = utils.measure_trip_time(bspd_ctrl, 10.0, is_falling=True) - # hil.check(t < R_BSPD_MAX_TRIP_TIME_S, "Braking with current") - check.less(t, R_BSPD_MAX_TRIP_TIME_S, "Braking with current") - - # Measure braking with current threshold - reset_bspd(brk1, brk2, c_sense) - cycle_power() - time.sleep(BSPD_DASH_ON_TIME) - brk1.state = BRK_1_THRESH_V - - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - start = ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(0.0)) - stop = ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(DHAB_S124_CH1_MAX_A)) - step = 0.1 - thresh = utils.measure_trip_thresh(c_sense, start, stop, step, - R_BSPD_MAX_TRIP_TIME_S, - bspd_ctrl, is_falling=True) - thresh_amps = dhab_ch1_v_to_a(ABOX_DHAB_CH1_DIV.reverse(thresh)) - print(f"Current while braking threshold: {thresh}V = {thresh_amps}A") - # hil.check_within(thresh, ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(min_trip_current)), 0.1, "Current while braking threshold") - check.almost_equal( - thresh, ABOX_DHAB_CH1_DIV.div(dhab_ch1_a_to_v(min_trip_current)), - abs=0.1, rel=0.0, - msg="Current while braking threshold" - ) - - # Determine the current sense short to gnd threshold - reset_bspd(brk1, brk2, c_sense) - cycle_power() - time.sleep(BSPD_DASH_ON_TIME) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - set_bspd_current(c_sense, DHAB_S124_CH1_MIN_A) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 1, "Min output current okay") - check.equal(bspd_ctrl.state, 1, "Min output current okay") - start = ABOX_DHAB_CH1_DIV.div(DHAB_S124_MIN_OUT_V) - stop = 0.0 - step = -0.1 - thresh = utils.measure_trip_thresh(c_sense, start, stop, step, - R_BSPD_MAX_TRIP_TIME_S, - bspd_ctrl, is_falling=True) - print(f"Short to ground threshold: {thresh}V") - # hil.check(stop < (thresh) < start, "Current short to ground threshold") - check.between(thresh, stop, start, "Current short to ground threshold") - - # Determine the current sense short to 5V threshold - reset_bspd(brk1, brk2, c_sense) - cycle_power() - time.sleep(BSPD_DASH_ON_TIME) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - time.sleep(2) - - set_bspd_current(c_sense, DHAB_S124_CH1_MAX_A) - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 1, "Max output current okay") - check.equal(bspd_ctrl.state, 1, "Max output current okay") - - start = ABOX_DHAB_CH1_DIV.div(DHAB_S124_MAX_OUT_V) - stop = ABOX_DHAB_CH1_DIV.div(5.0) - step = 0.01 - - c_sense.state = start - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 1, "Max output voltage okay") - check.equal(bspd_ctrl.state, 1, "Max output voltage okay") - input("enter to continue") - - thresh = utils.measure_trip_thresh(c_sense, start, stop, step, - R_BSPD_MAX_TRIP_TIME_S, - bspd_ctrl, is_falling=True) - print(f"Short to 5V threshold: {thresh}V") - # hil.check(bspd_ctrl.state == 0, "Short to 5V trips") - check.equal(bspd_ctrl.state, 0, "Short to 5V trips") - print(stop) - print(start) - # hil.check(start < (thresh) <= stop, "Current short to 5V threshold") - check.between(thresh, start, stop, "Current short to 5V threshold", le=True) - - # Floating current - reset_bspd(brk1, brk2, c_sense) - cycle_power() - time.sleep(BSPD_DASH_ON_TIME) - # hil.check(bspd_ctrl.state == 1, "Power On") - check.equal(bspd_ctrl.state, 1, "Power On") - c_sense.hiZ() - time.sleep(R_BSPD_MAX_TRIP_TIME_S) - # hil.check(bspd_ctrl.state == 0, "Floating current") - check.equal(bspd_ctrl.state, 0, "Floating current") - - # Floating brake_fail - # Can't test this at system level! - - # Floating brake_stat - # Can't test this at system level! - - # End the test - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -IMD_RC_MIN_TRIP_TIME_S = IMD_STARTUP_TIME_S -IMD_RC_MAX_TRIP_TIME_S = R_IMD_MAX_TRIP_TIME_S - IMD_MEASURE_TIME_S -IMD_CTRL_OKAY = 1 -IMD_CTRL_TRIP = 0 - -def test_imd(hil): - # Begin the test - # hil.start_test(test_imd.__name__) - - # Outputs - imd_stat = hil.dout("Main_Module", "IMD_Status") - - # Outputs to set SDC status to okay - ams_stat = hil.dout("Main_Module", "BMS-Status-Main") - brk1 = hil.aout("Dashboard", "BRK1_RAW") - brk2 = hil.aout("Dashboard", "BRK2_RAW") - c_sense = hil.aout("Main_Module", "Current Sense C1") - - # Inputs - imd_ctrl = hil.din("Main_Module", "SDC15") # assuming AMS and BSPD closed - - # Set other SDC nodes to okay - reset_ams(ams_stat) - reset_bspd(brk1, brk2, c_sense) - - # IMD Fault - reset_imd(imd_stat) - cycle_power() - # hil.check(imd_ctrl.state == IMD_CTRL_OKAY, "Power On") - check.equal(imd_ctrl.state, IMD_CTRL_OKAY, "Power On") - time.sleep(1) - imd_stat.state = IMD_STAT_TRIP - t = utils.measure_trip_time(imd_ctrl, R_IMD_MAX_TRIP_TIME_S, is_falling=True) - print(f"Target trip time: [{IMD_RC_MIN_TRIP_TIME_S}, {IMD_RC_MAX_TRIP_TIME_S}]") - # hil.check(IMD_RC_MIN_TRIP_TIME_S < t < IMD_RC_MAX_TRIP_TIME_S, "IMD Trip Time") - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Trip") - check.between(t, IMD_RC_MIN_TRIP_TIME_S, IMD_RC_MAX_TRIP_TIME_S, "IMD Trip Time") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Trip") - imd_stat.state = IMD_STAT_OKAY - time.sleep(IMD_RC_MAX_TRIP_TIME_S * 1.1) - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Fault Stays Latched") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Fault Stays Latched") - - # IMD Fault on Power On - reset_imd(imd_stat) - imd_stat.state = IMD_STAT_TRIP - cycle_power() - time.sleep(IMD_RC_MAX_TRIP_TIME_S) - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Fault Power On") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Fault Power On") - - # IMD Floating - reset_imd(imd_stat) - imd_stat.hiZ() - cycle_power() - t = utils.measure_trip_time(imd_ctrl, R_IMD_MAX_TRIP_TIME_S, is_falling=True) - # hil.check(t < R_IMD_MAX_TRIP_TIME_S, "IMD Floating Trip Time") - # hil.check(imd_ctrl.state == IMD_CTRL_TRIP, "IMD Floating Trip") - check.less(t, R_IMD_MAX_TRIP_TIME_S, "IMD Floating Trip Time") - check.equal(imd_ctrl.state, IMD_CTRL_TRIP, "IMD Floating Trip") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_ams(hil): - # Begin the test - # hil.start_test(test_ams.__name__) - - # Outputs - ams_stat = hil.dout("Main_Module", "BMS-Status-Main") - - # Outputs to set SDC status to okay - imd_stat = hil.dout("Main_Module", "IMD_Status") - brk1 = hil.aout("Dashboard", "BRK1_RAW") - brk2 = hil.aout("Dashboard", "BRK2_RAW") - c_sense = hil.aout("Main_Module", "Current Sense C1") - - # Inputs - ams_ctrl = hil.din("Main_Module", "SDC15") # assumes IMD and BSPD closed - - # Set other SDC nodes to okay - reset_imd(imd_stat) - reset_bspd(brk1, brk2, c_sense) - - # AMS Fault - reset_ams(ams_stat) - cycle_power() - # hil.check(ams_ctrl.state == AMS_CTRL_OKAY, "Power On") - check.equal(ams_ctrl.state, AMS_CTRL_OKAY, "Power On") - time.sleep(1) - ams_stat.state = AMS_STAT_TRIP - t = utils.measure_trip_time(ams_ctrl, AMS_MAX_TRIP_DELAY_S * 2, is_falling=True) - # hil.check(0 < t < AMS_MAX_TRIP_DELAY_S, "AMS Trip Time") - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Trip") - check.between(t, 0, AMS_MAX_TRIP_DELAY_S, "AMS Trip Time") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Trip") - ams_stat.state = AMS_STAT_OKAY - time.sleep(AMS_MAX_TRIP_DELAY_S * 1.1) - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Fault Stays Latched") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Fault Stays Latched") - - # AMS Fault on Power On - reset_ams(ams_stat) - ams_stat.state = AMS_STAT_TRIP - cycle_power() - time.sleep(AMS_MAX_TRIP_DELAY_S) - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Fault Power On") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Fault Power On") - - # AMS Floating - reset_ams(ams_stat) - ams_stat.hiZ() - cycle_power() - t = utils.measure_trip_time(ams_ctrl, AMS_MAX_TRIP_DELAY_S * 2, is_falling=True) - # hil.check(0 <= t < AMS_MAX_TRIP_DELAY_S, "AMS Floating Trip Time") - # hil.check(ams_ctrl.state == AMS_CTRL_TRIP, "AMS Floating Trip") - check.between(t, 0, AMS_MAX_TRIP_DELAY_S, "AMS Floating Trip Time") - check.equal(ams_ctrl.state, AMS_CTRL_TRIP, "AMS Floating Trip") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def tsal_is_red(): - while 1: - i = input("Is TSAL Green (g) or Red (r): ") - utils.clear_term_line() - i = i.upper() - if (i == 'G' or i == 'R'): - return i == 'R' - print("You may only enter G or R!") - -def test_tsal(hil): - - # Begin the test - hil.start_test(test_tsal.__name__) - - # Outputs - v_mc = hil.aout("Main_Module", "Voltage MC Transducer") - - # Inputs - # User :D - print(f"{utils.bcolors.OKCYAN}Press 'G' for green and 'R' for red.{utils.bcolors.ENDC}") - - # Initial State - reset_tsal(v_mc) - time.sleep(0.2) - # No need to power cycle - - # hil.check(tsal_is_red() == False, "LVAL on at v_mc = 0") - check.equal(tsal_is_red(), False, "LVAL on at v_mc = 0") - #hil.check(tsal.state == 0, "TSAL off at v_mc = 0") - - time.sleep(5) - # hil.check(tsal_is_red() == False, "LVAL stays on") - check.equal(tsal_is_red(), False, "LVAL stays on") - - v_mc.state = tiff_hv_to_lv(ACCUM_MIN_V) - time.sleep(0.1) - - #hil.check(lval.state == 0, f"LVAL off at {ACCUM_MIN_V:.4} V") - # hil.check(tsal_is_red() == True, f"TSAL on at {ACCUM_MIN_V:.4} V") - check.equal(tsal_is_red(), True, f"TSAL on at {ACCUM_MIN_V:.4} V") - - reset_tsal(v_mc) - time.sleep(0.2) - # hil.check(tsal_is_red() == False, f"LVAL turns back on") - check.equal(tsal_is_red(), False, f"LVAL turns back on") - - start = tiff_hv_to_lv(50) - stop = tiff_hv_to_lv(R_TSAL_HV_V * 1.5) - step = tiff_hv_to_lv(1) - - gain = 1000 - thresh = start - _start = int(start * gain) - _stop = int(stop * gain) - _step = int(step * gain) - v_mc.state = start - tripped = False - print(f"Start: {_start} Stop: {_stop} Step: {_step} Gain: {gain}") - for v in range(_start, _stop+_step, _step): - v_mc.state = v / gain - time.sleep(0.01) - if (tsal_is_red()): - thresh = v / gain - tripped = True - break - if (not tripped): - utils.log_warning(f"TSAL did not trip at stop of {stop}.") - thresh = stop - # hil.check(tripped, "TSAL tripped") - check.is_true(tripped, "TSAL tripped") - - thresh = tiff_lv_to_hv(thresh) - print(f"TSAL on at {thresh:.4} V") - hil.check_within(thresh, R_TSAL_HV_V, 4, f"TSAL trips at {R_TSAL_HV_V:.4} +-4") - check.almost_equal(thresh, R_TSAL_HV_V, abs=4, rel=0.0, msg=f"TSAL trips at {R_TSAL_HV_V:.4} +-4") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_sdc(hil): - ''' Check that every node in the sdc trips ''' - # Begin the test - # hil.start_test(test_sdc.__name__) - - # Outputs - - # Inputs - - # hil.check(False, "TODO") - check.is_true(False, "TODO") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def is_buzzer_on(): - while 1: - i = input("Is Buzzer On (y) or No (n): ") - utils.clear_term_line() - i = i.upper() - if (i == 'Y' or i == 'N'): - return i == 'Y' - print("You may only enter Y or N!") - -def test_buzzer(hil): - # Begin the test - # hil.start_test(test_buzzer.__name__) - - # Outputs - buzzer_ctrl = hil.daq_var("Main_Module", "daq_buzzer") - - # Inputs - buzzer_stat = hil.mcu_pin("Main_Module", "Buzzer_Prot") - - buzzer_ctrl.state = 0 - time.sleep(0.02) - # hil.check(buzzer_stat.state == 0, "Buzzer Off") - check.equal(buzzer_stat.state, 0, "Buzzer Off") - - buzzer_ctrl.state = 1 - print(buzzer_ctrl.state) - time.sleep(0.02) - # hil.check(buzzer_stat.state == 1, "Buzzer On") - # hil.check(is_buzzer_on() == True, "Buzzer Making Noise") - check.equal(buzzer_stat.state, 1, "Buzzer On") - check.is_true(is_buzzer_on(), "Buzzer Making Noise") - - buzzer_ctrl.state = 0 - time.sleep(0.02) - # hil.check(buzzer_stat.state == 0, "Buzzer back Off") - # hil.check(is_buzzer_on() == False, "Buzzer Not Making Noise") - check.equal(buzzer_stat.state, 0, "Buzzer back Off") - check.is_false(is_buzzer_on(), "Buzzer Not Making Noise") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def is_brake_light_on(): - while 1: - i = input("Is Brake Light On (y) or No (n): ") - utils.clear_term_line() - i = i.upper() - if (i == 'Y' or i == 'N'): - return i == 'Y' - print("You may only enter Y or N!") - -def test_brake_light(hil): - # Begin the test - # hil.start_test(test_brake_light.__name__) - - # Outputs - brk_ctrl = hil.daq_var("Main_Module", "daq_brake") - - # Inputs - brk_stat = hil.mcu_pin("Main_Module", "Brake_Light_CTRL_Prot") - - brk_ctrl.state = 0 - time.sleep(0.02) - # hil.check(brk_ctrl.state == 0, "Brake Off") - check.equal(brk_ctrl.state, 0, "Brake Off") - - brk_ctrl.state = 1 - print(brk_ctrl.state) - time.sleep(0.02) - # hil.check(brk_ctrl.state == 1, "Brake Light On") - # hil.check(is_brake_light_on() == True, "Brake Light is On") - check.equal(brk_ctrl.state, 1, "Brake Light On") - check.is_true(is_brake_light_on(), "Brake Light is On") - - brk_ctrl.state = 0 - time.sleep(0.02) - # hil.check(brk_ctrl.state == 0, "Brake Light back Off") - # hil.check(is_brake_light_on() == False, "Brake Light is Off") - check.equal(brk_ctrl.state, 0, "Brake Light back Off") - check.is_false(is_brake_light_on(), "Brake Light is Off") - - - # Can copy lot from bspd - # Read the brake control mcu pin - # Finally have user verify light actually turned on - - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_light_tsal_buz(hil): - # hil.start_test(test_light_tsal_buz.__name__) - - # Outputs - brk_ctrl = hil.daq_var("Main_Module", "daq_brake") - buzzer_ctrl = hil.daq_var("Main_Module", "daq_buzzer") - - brk_ctrl.state = 1 - buzzer_ctrl.state = 1 - input("Press enter to end the test") - brk_ctrl.state = 0 - buzzer_ctrl.state = 0 - - check.is_true(True, "TODO") - - # hil.end_test() -# ---------------------------------------------------------------------------- # \ No newline at end of file diff --git a/scripts/test_test.py b/scripts/test_test.py deleted file mode 100644 index b2536bb..0000000 --- a/scripts/test_test.py +++ /dev/null @@ -1,201 +0,0 @@ -from os import sys, path -# adds "./HIL-Testing" to the path, basically making it so these scripts were run one folder level higher -sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) - -from hil.hil import HIL -# import hil.utils as utils -import time -import can -from rules_constants import * -from vehicle_constants import * - -import pytest_check as check -import pytest - - -# ---------------------------------------------------------------------------- # -@pytest.fixture(scope="session") -def hil(): - hil_instance = HIL() - - # hil.load_config("config_testing.json") - hil_instance.load_pin_map("per_24_net_map.csv", "stm32f407_pin_map.csv") - hil_instance.init_can() - - yield hil_instance - - hil_instance.shutdown() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_bspd(hil): - # Begin the test - # hil.start_test(test_bspd.__name__) - - # Inputs - d2 = hil.din("Test_HIL", "AI2") - d3 = hil.din("Test_HIL", "AI3") - d4 = hil.din("Test_HIL", "AI4") - # d5 = hil.din("Test_HIL", "DI5") - # d6 = hil.din("Test_HIL", "DI6") - # d7 = hil.din("Test_HIL", "DI7") - r1 = hil.dout("Test_HIL", "RLY1") - r2 = hil.dout("Test_HIL", "RLY2") - r3 = hil.dout("Test_HIL", "RLY3") - r4 = hil.dout("Test_HIL", "RLY4") - - a1 = hil.dout("Test_HIL", "AI1") - - a1.state = 1 - - - r1.state = 1 - r2.state = 1 - r3.state = 1 - r4.state = 1 - - r1.state = 0 - input("") - r2.state = 0 - input("") - r3.state = 0 - input("") - r4.state = 0 - input("") - - for _ in range(100): - - #print(f"{d2.state}, {d3.state}, {d4.state}") - for i in range(4): - r1.state = not ((i % 4) == 0) - r2.state = not ((i % 4) == 1) - r3.state = not ((i % 4) == 2) - r4.state = not ((i % 4) == 3) - time.sleep(1) - time.sleep(2) - - check.is_true(True, "TODO") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_dac(hil): - # hil.start_test(test_dac.__name__) - - dac1 = hil.aout("Test_HIL", "DAC1") - dac2 = hil.aout("Test_HIL", "DAC2") - - dac1.state = 2.5 - dac2.state = 5.0 - input("5, 2") - dac1.hiZ() - dac2.hiZ() - input("hi-z") - dac1.state = 0.25 - dac2.state = 0.25 - input(".25") - - check.is_true(True, "TODO") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_pot(hil): - - pot1 = hil.pot("Test_HIL", "POT1") - pot2 = hil.pot("Test_HIL", "POT2") - - print("initial") - input(" - ") - print("0.5, 1") - pot1.state = 0.5 - pot2.state = 0.5 - input(" - ") - - print("1, 0.5") - pot1.state = 1.0 - pot2.state = 1.0 - input(" - ") - - print("0, 0") - pot1.state = 0.0 - pot2.state = 0.0 - input(" - ") - - for i in range(1000): - pot1.state = 0.25 - pot2.state = 0.25 - time.sleep(0.01) - pot1.state = 0.75 - pot2.state = 0.75 - time.sleep(0.01) - - pot1.state = 0.5 - pot2.state = 0.5 - input("-------") - - check.is_true(True, "TODO") -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_mcu_pin(hil): - # hil.start_test(test_mcu_pin.__name__) - - brk_stat_tap = hil.mcu_pin("Dashboard", "BRK_STAT_TAP") - - delta_avg = 0 - delta_cnt = 0 - for i in range(100): - t_start = time.time() - #time.sleep(0.01) - print(brk_stat_tap.state) - t_start = time.time() - t_start - delta_avg += t_start - delta_cnt = delta_cnt + 1 - - print(f"Average: {delta_avg/delta_cnt}") - - check.is_true(True, "TODO") - - # hil.end_test() -# ---------------------------------------------------------------------------- # - - -# ---------------------------------------------------------------------------- # -def test_daq(hil): - # hil.start_test(test_daq.__name__) - - counter = 0 - start_time = time.time() - - LOOP_TIME_S = 0.001 - - # while(1): - # time.sleep(3) - - print("Sending") - - #while (time.time() - start_time < 15*60): - while (counter < 4000): - last_tx = time.perf_counter() - #msg = can.Message(arbitration_id=0x14000072, data=counter.to_bytes(4, 'little')) - #msg = can.Message(arbitration_id=0x80080c4, data=counter.to_bytes(4, 'little')) - msg = can.Message(arbitration_id=0x400193e, data=counter.to_bytes(8, 'little')) - #print(msg) - hil.can_bus.sendMsg(msg) - counter = counter + 1 - delta = LOOP_TIME_S - (time.perf_counter() - last_tx) - if (delta < 0): delta = 0 - time.sleep(delta) - - check.is_true(True, "TODO") - - print("Done") - print(f"Last count sent: {counter - 1}") -# ---------------------------------------------------------------------------- # \ No newline at end of file diff --git a/scripts/vehicle_constants.py b/scripts/vehicle_constants.py deleted file mode 100644 index 4db3f2a..0000000 --- a/scripts/vehicle_constants.py +++ /dev/null @@ -1,77 +0,0 @@ -from os import sys, path -# adds "./HIL-Testing" to the path, basically making it so these scripts were run one folder level higher -sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) - -import hil.utils as utils - -# NOTE: each value in this file should be a physical or electrical property of the vehicle - -# Accumulator Constants -ACCUM_MAX_V = 317.3 -ACCUM_MIN_V = 190.0 -ACCUM_NOM_V = 273.6 -ACCUM_FUSE_A = 140.0 - -ABOX_DHAB_CH1_DIV = utils.VoltageDivider(1000, 2000) - -# IMD Constants -IMD_MEASURE_TIME_S = 20.0 -IMD_STARTUP_TIME_S = 2.0 - -# AMS Constants -AMS_MAX_TRIP_DELAY_S = 3.0 - -# Precharge Constants -PCHG_COMPLETE_DELAY_S = 0.5 - -# Tiffomy Constants -TIFF_LV_MAX = 5.0 -TIFF_LV_MIN = -5.0 -TIFF_SCALE = 100.0 -def tiff_hv_to_lv(hv_voltage): - return min(max(hv_voltage / TIFF_SCALE, TIFF_LV_MIN), TIFF_LV_MAX) -def tiff_lv_to_hv(lv_voltage): - return lv_voltage * TIFF_SCALE - -# DHAB S124 Current Sensor -DHAB_S124_MAX_OUT_V = 4.8 -DHAB_S124_MIN_OUT_V = 0.2 -DHAB_S124_OFFSET_V = 2.5 -DHAB_S124_CH1_SENSITIVITY = 26.7 / 1000.0 # V / A -DHAB_S124_CH2_SENSITIVITY = 4.9 / 1000.0 # V / A -DHAB_S124_CH1_MAX_A = 75.0 -DHAB_S124_CH1_MIN_A = -75.0 -DHAB_S124_CH2_MAX_A = 500.0 -DHAB_S124_CH2_MIN_A = -500.0 - -def dhab_ch1_v_to_a(signal_v): - return (signal_v - DHAB_S124_OFFSET_V) / DHAB_S124_CH1_SENSITIVITY - -def dhab_ch2_v_to_a(signal_v): - return (signal_v - DHAB_S124_OFFSET_V) / DHAB_S124_CH2_SENSITIVITY - -def dhab_ch1_a_to_v(amps): - amps = min(max(amps, DHAB_S124_CH1_MIN_A), DHAB_S124_CH1_MAX_A) - return (amps * DHAB_S124_CH1_SENSITIVITY) + DHAB_S124_OFFSET_V - -def dhab_ch2_a_to_v(amps): - amps = min(max(amps, DHAB_S124_CH2_MIN_A), DHAB_S124_CH2_MAX_A) - return (amps * DHAB_S124_CH2_SENSITIVITY) + DHAB_S124_OFFSET_V - -def dhab_v_valid(signal_v): - return (DHAB_S124_MIN_OUT_V <= signal_v <= DHAB_S124_MAX_OUT_V) - -# Brake Pressure Transducer -BRK_MAX_OUT_V = 4.8 -BRK_MIN_OUT_V = 0.2 -BRK_1_REST_V = 0.5 # Resting line voltage of brake 1 -BRK_2_REST_V = 0.5 # Resting line voltage of brake 2 -BRK_1_DIV = utils.VoltageDivider(5600, 10000) -BRK_2_DIV = utils.VoltageDivider(5600, 10000) -BRK_1_THRESH_V = 0.68 # Threshold that is considered braking for brake 1 -BRK_2_THRESH_V = 0.68 # Threshold that is considered braking for brake 2 - -# Throttle -THTL_MAX_P = 0.9 # Maximum pedal press percent -THTL_MIN_P = 0.1 # Minimum pedal press percent -THTL_THRESH = 0.2 # Throttle pressed percent diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/dashboard/__init__.py b/tests/dashboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/dashboard/config.json b/tests/dashboard/config.json new file mode 100644 index 0000000..3fd75d9 --- /dev/null +++ b/tests/dashboard/config.json @@ -0,0 +1,19 @@ +{ + "dut_connections":[ + { "board":"Dashboard", "harness_connections":[ + {"_": "BRK1_RAW", "dut":{"connector":"J1","pin":9}, "hil":{"device":"HIL2", "port":"DAC1" }}, + {"_": "BRK2_RAW", "dut":{"connector":"J1","pin":10}, "hil":{"device":"HIL2", "port":"DAC2" }}, + {"_": "THRTL1_RAW", "dut":{"connector":"J1","pin":11}, "hil":{"device":"HIL2", "port":"DAC3" }}, + {"_": "THRTL2_RAW", "dut":{"connector":"J1","pin":12}, "hil":{"device":"HIL2", "port":"DAC4" }}, + {"_": "SDC", "dut":{"connector":"J?","pin":-1}, "hil":{"device":"HIL2", "port":"DMUX_0" }}, + {"_": "UP", "dut":{"connector":"P3","pin":3}, "hil":{"device":"HIL2", "port":"D01" }}, + {"_": "DOWN", "dut":{"connector":"P3","pin":4}, "hil":{"device":"HIL2", "port":"D02" }}, + {"_": "SELECT", "dut":{"connector":"P3","pin":5}, "hil":{"device":"HIL2", "port":"D03" }}, + {"_": "START", "dut":{"connector":"P3","pin":6}, "hil":{"device":"HIL2", "port":"D04" }}, + {"_": "USART_LCD_TX", "dut":{"connector":"P3","pin":1}, "hil":{"device":"HIL2", "port":"DMUX_0" }} + ] } + ], + "hil_devices": [ + { "name":"HIL2", "config":"teensy_pcb.json", "id":1 } + ] +} diff --git a/tests/dashboard/main.py b/tests/dashboard/main.py new file mode 100644 index 0000000..a2c0121 --- /dev/null +++ b/tests/dashboard/main.py @@ -0,0 +1,296 @@ +from typing import Optional + +import hil2.hil2 as hil2 +import hil2.component as hil2_comp +import hil2.can_helper as can_helper +import mk_assert.mk_assert as mka + +import time +import logging + +# Consts ------------------------------------------------------------------------------# +PEDAL_LOW_V = 0.5 # volts read when pedal is not pressed +PEDAL_HIGH_V = 4.5 # volts read when pedal is fully pressed +PEDAL_PERCENT_V = (PEDAL_HIGH_V - PEDAL_LOW_V) / 100.0 + +SLEEP_TIME = 0.03 # seconds + +MSG_NAME = "raw_throttle_brake" + +# Helpers -----------------------------------------------------------------------------# +def pedal_percent_to_volts_1(percent: float) -> float: + """ + Normal linear mapping from 0-100% to volts + + :param percent: Percent value from 0 to 100 + :return: Corresponding voltage value + """ + return PEDAL_LOW_V + percent * PEDAL_PERCENT_V + +def pedal_percent_to_volts_2(percent: float) -> float: + """ + Inverted linear mapping from 0-100% to volts + + :param percent: Percent value from 0 to 100 + :return: Corresponding voltage value + """ + return PEDAL_HIGH_V - percent * PEDAL_PERCENT_V + +def power_cycle(pow: hil2_comp.DO, delay_s: float = 0.5): + pow.set(False) + time.sleep(delay_s) + pow.set(True) + time.sleep(delay_s) + + +def set_both(pedal1: hil2_comp.AO, pedal2: hil2_comp.AO, percent: float) -> None: + """ + Set a set of two pedals to the same percent value. + + :param pedal1: First pedal AO component + :param pedal2: Second pedal AO component + :param percent: Percent value from 0 to 100 + """ + pedal1.set(pe + +def check_msg(msg: Optional[can_helper.CanMessage], test_prefix: str): + mka.assert_true(msg is not None, f"{test_prefix}: VCAN message received") + +def check_brakes(msg: Optional[can_helper.CanMessage], exp_percent: float, tol_v: float, test_prefix: str): + check_msg(msg, test_prefix) + mka.assert_eqf(msg is not None and msg.data["brake"], pedal_percent_to_volts(exp_percent), tol_v, f"{test_prefix}: brake left {exp_percent}%") + mka.assert_eqf(msg is not None and msg.data["brake_right"], pedal_percent_to_volts(exp_percent), tol_v, f"{test_prefix}: brake right {exp_percent}%") + +def check_throttle_left(msg: Optional[can_helper.CanMessage], exp_percent: float, tol_v: float, test_prefix: str): + mka.assert_eqf(msg is not None and msg.data["throttle"], pedal_percent_to_volts(exp_percent), tol_v, f"{test_prefix}: throttle left {exp_percent}%") + +def check_throttle_right(msg: Optional[can_helper.CanMessage], exp_percent: float, tol_v: float, test_prefix: str): + mka.assert_eqf(msg is not None and msg.data["throttle_right"], pedal_percent_to_volts(exp_percent), tol_v, f"{test_prefix}: throttle right {exp_percent}%") + +def check_throttles(msg: Optional[can_helper.CanMessage], exp_percent: float, tol_v: float, test_prefix: str): + check_msg(msg, test_prefix) + check_throttle_left(msg, exp_percent, tol_v, test_prefix) + check_throttle_right(msg, exp_percent, tol_v, test_prefix) + +# EV.4.7.2 ----------------------------------------------------------------------------# +def ev_4_7_2_test(h: hil2.Hil2): + """ + If brake is activated (5% pressed) and throttle is activated more than 25%, motor + must shutdown and stay shutdown until throttle is under 5% + + - brake low, throttle low, check motor on + - brake high, throttle low, check motor on + - brake high, throttle high, check motor off + - brake low, throttle mid, check motor off (sweep down to 5% on throttle) + - brake low, throttle low, check motor back on + Note: Check for motor off: throttle can message is 0 + """ + + brk1 = h.ao("Dashboard", "BRK1_RAW") + brk2 = h.ao("Dashboard", "BRK2_RAW") + thrtl1 = h.ao("Dashboard", "THRTL1_RAW") + thrtl2 = h.ao("Dashboard", "THRTL2_RAW") + mcan = h.can("HIL2", "VCAN") + + # Setup: set brake and throttle to 0% + set_both(brk1, brk2, pedal_percent_to_volts(0)) + set_both(thrtl1, thrtl2, pedal_percent_to_volts(0)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_brakes(msg, 0, 0.1, "Setup") + check_throttles(msg, 0, 0.1, "Setup") + + # Test 1: brake low, throttle low, check motor on + set_both(brk1, brk2, pedal_percent_to_volts(5)) + set_both(thrtl1, thrtl2, pedal_percent_to_volts(5)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_brakes(msg, 5, 0.1, "Brakes low, throttle low") + check_throttles(msg, 5, 0.1, "Brakes low, throttle low") + + # Test 2: brake high, throttle low, check motor on + set_both(brk1, brk2, pedal_percent_to_volts(50)) + set_both(thrtl1, thrtl2, pedal_percent_to_volts(5)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_brakes(msg, 50, 0.1, "Brakes high, throttle low") + check_throttles(msg, 5, 0.1, "Brakes high, throttle low") + + # Test 3: brake high, throttle high, check motor off + set_both(brk1, brk2, pedal_percent_to_volts(50)) + set_both(thrtl1, thrtl2, pedal_percent_to_volts(50)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_brakes(msg, 50, 0.1, "Brakes high, throttle high") + check_throttles(msg, 0, 0.1, "Brakes high, throttle high") + + # Test 4: brake low, throttle mid, check motor off (sweep down to 5% on throttle) + set_both(brk1, brk2, pedal_percent_to_volts(5)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_brakes(msg, 5, 0.1, "Brakes low, throttle mid") + + for p in range(50, 4, -1): + set_both(thrtl1, thrtl2, pedal_percent_to_volts(p)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + expected_throttle = 0 if p > 5 else pedal_percent_to_volts(p) + check_throttles(msg, expected_throttle, 0.1, f"Brakes low, throttle {p} (expected {expected_throttle}%)") + + # Test 5: brake low, throttle mid, check motor back on + set_both(brk1, brk2, pedal_percent_to_volts(5)) + set_both(thrtl1, thrtl2, pedal_percent_to_volts(25)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_brakes(msg, 5, 0.1, "Brakes low, throttle mid") + check_throttles(msg, 25, 0.1, "Brakes low, throttle mid") + + +# T.4.2.5 -----------------------------------------------------------------------------# +def t_4_2_5_impl(left_is_1: bool, sens1: hil2_comp.AO, sens2: hil2_comp.AO, sdc: hil2_comp.DI, mcan: hil2_comp.CAN): + """ + - sens1 and sens2 similar, check motor on, sdc not triggered + - sens1 and sens2 slightly different, check motor on, sdc not triggered + - sens1 and sens2 10% different, check motor on, sdc not triggered + - sens1 and sens2 slightly different, check motor on, sdc not triggered + - sens1 and sens2 10% different, check motor on, sdc not triggered + - sens1 and sens2 still 10% different (~100 msec later), check motor off, sdc not triggered + - sens1 and sens2 similar, check motor on, sdc not triggered + Note: Check for motor off: throttle can message is 0 + """ + + # Sensors similar, check motor on, sdc not triggered + set_both(sens1, sens2, pedal_percent_to_volts(20)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_throttles(msg, 20, 0.1, "Sensors similar") + mka.assert_false(sdc.get(), "SDC not triggered") + + # Sensors slightly different, check motor on, sdc not triggered + sens1.set(pedal_percent_to_volts(20)) + sens2.set(pedal_percent_to_volts(25)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_msg(msg, "Sensors slightly different") + if left_is_1: + check_throttle_left(msg, 20, 0.1, "Sensors slightly different") + check_throttle_right(msg, 25, 0.1, "Sensors slightly different") + else: + check_throttle_left(msg, 25, 0.1, "Sensors slightly different") + check_throttle_right(msg, 20, 0.1, "Sensors slightly different") + mka.assert_false(sdc.get(), "SDC not triggered") + + # Sensors 10% different, check motor on, sdc not triggered + sens1.set(pedal_percent_to_volts(20)) + sens2.set(pedal_percent_to_volts(30)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_msg(msg, "Sensors 10% different") + if left_is_1: + check_throttle_left(msg, 20, 0.1, "Sensors 10% different") + check_throttle_right(msg, 30, 0.1, "Sensors 10% different") + else: + check_throttle_left(msg, 30, 0.1, "Sensors 10% different") + check_throttle_right(msg, 20, 0.1, "Sensors 10% different") + mka.assert_false(sdc.get(), "SDC not triggered") + + # Sensors slightly different, check motor on, sdc not triggered + sens1.set(pedal_percent_to_volts(25)) + sens2.set(pedal_percent_to_volts(30)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_msg(msg, "Sensors slightly different") + if left_is_1: + check_throttle_left(msg, 25, 0.1, "Sensors slightly different") + check_throttle_right(msg, 30, 0.1, "Sensors slightly different") + else: + check_throttle_left(msg, 30, 0.1, "Sensors slightly different") + check_throttle_right(msg, 25, 0.1, "Sensors slightly different") + mka.assert_false(sdc.get(), "SDC not triggered") + + # Sensors 10% different, check motor on, sdc not triggered + sens1.set(pedal_percent_to_volts(20)) + sens2.set(pedal_percent_to_volts(30)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_msg(msg, "Sensors 10% different") + if left_is_1: + check_throttle_left(msg, 20, 0.1, "Sensors 10% different") + check_throttle_right(msg, 30, 0.1, "Sensors 10% different") + else: + check_throttle_left(msg, 30, 0.1, "Sensors 10% different") + check_throttle_right(msg, 20, 0.1, "Sensors 10% different") + mka.assert_false(sdc.get(), "SDC not triggered") + + # Sensors still 10% different (~100 msec later), check motor off, sdc not triggered + time.sleep(0.1) + msg = mcan.get_last(MSG_NAME) + check_msg(msg, "Sensors still 10% different (~100 msec later)") + check_throttles(msg, 0, 0.1, "Sensors still 10% different (~100 msec later)") + mka.assert_false(sdc.get(), "SDC not triggered") + + # Sensors similar, check motor on, sdc not triggered + set_both(sens1, sens2, pedal_percent_to_volts(20)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_throttles(msg, 20, 0.1, "Sensors similar") + mka.assert_false(sdc.get(), "SDC not triggered") + + +def t_4_2_5_test(h: hil2.Hil2): + """ + If the throttle sensors differ by more than 10% of the pedal travel or disconnects + and this exists for more than 100 msec, motors must be stopped, sdc isn't tripped + """ + thrtl1 = h.ao("Dashboard", "THRTL1_RAW") + thrtl2 = h.ao("Dashboard", "THRTL2_RAW") + sdc = h.di("Dashboard", "SDC") + mcan = h.can("HIL2", "VCAN") + + # Setup: set brake and throttle to 0% + set_both(thrtl1, thrtl2, pedal_percent_to_volts(0)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_throttles(msg, 0, 0.1, "Setup") + + # Test with left as sens1 + t_4_2_5_impl(True, thrtl1, thrtl2, sdc, mcan) + + # Setup: set brake and throttle to 0% + set_both(thrtl1, thrtl2, pedal_percent_to_volts(0)) + time.sleep(SLEEP_TIME) + msg = mcan.get_last(MSG_NAME) + check_throttles(msg, 0, 0.1, "Setup") + + # Test with right as sens1 + t_4_2_5_impl(False, thrtl2, thrtl1, sdc, mcan) + +# T.4.2.10 ----------------------------------------------------------------------------# +def t_4_2_10_test_out_of_range(left_is_1: bool, sens1: hil2_comp.AO, sens2: hil2_comp.AO, sdc: hil2_comp.DI, mcan: hil2_comp.CAN): + """ + - sens1 and sens2 ok, check motor on, sdc not triggered + - both are out of range high, check motor off, sdc triggered + """ + + +# Main --------------------------------------------------------------------------------# +def main(): + logging.basicConfig(level=logging.DEBUG) + + with hil2.Hil2( + "./tests/dash/config.json", + "device_configs", + "TODO", + "TODO" + ) as h: + + pow = h.do("HIL2", "RLY1") + + mka.set_setup_fn(lambda: power_cycle(pow, 0.5)) + mka.add_test(ev_4_7_2_test, h) + mka.add_test(t_4_2_5_test, h) + mka.run_tests() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/dashboard/main2.py b/tests/dashboard/main2.py new file mode 100644 index 0000000..f03edfd --- /dev/null +++ b/tests/dashboard/main2.py @@ -0,0 +1,546 @@ +from typing import Optional + +import hil2.hil2 as hil2 +import hil2.component as hil2_comp +import hil2.can_helper as can_helper +import mk_assert.mk_assert as mka + +import time +import logging + +# Consts ------------------------------------------------------------------------------# +PEDAL_LOW_V = 0.5 # volts read when pedal is not pressed (in normal orientation) +PEDAL_HIGH_V = 4.5 # volts read when pedal is fully pressed (in normal orientation) +PEDAL_PERCENT_V = (PEDAL_HIGH_V - PEDAL_LOW_V) / 100.0 + +SLEEP_TIME = 0.01 # seconds, how long to wait before checking a CAN message + +PEDAL_MSG = "raw_throttle_brake" # note: motor "off" => throttle = 0 +SHOCK_MSG = "shock_front" + + +# Helpers -----------------------------------------------------------------------------# +def power_cycle(h: hil2.Hil2, delay_s: float = 0.5): + """ + Power cycle the system by turning the power off for delay_s seconds, then back on. + + :param pow: Power DO component (e.g. relay) + :param delay_s: Time in seconds to wait with power off + """ + pow = h.do("HIL2", "RLY1") + pow.set(False) + time.sleep(delay_s) + pow.set(True) + time.sleep(delay_s) + +def pedal_percent_to_volts_1(percent: float) -> float: + """ + Normal linear mapping from 0-100% to volts + + :param percent: Percent value from 0 to 100 + :return: Corresponding voltage value + """ + return PEDAL_LOW_V + percent * PEDAL_PERCENT_V + +def pedal_percent_to_volts_2(percent: float) -> float: + """ + Inverted linear mapping from 0-100% to volts + + :param percent: Percent value from 0 to 100 + :return: Corresponding voltage value + """ + return PEDAL_HIGH_V - percent * PEDAL_PERCENT_V + +def set_both(pedal1: hil2_comp.AO, pedal2: hil2_comp.AO, percent: float) -> None: + """ + Set a set of two pedals to the same percent value. + + :param pedal1: First pedal AO component (in normal orientation) + :param pedal2: Second pedal AO component (in inverted orientation) + :param percent: Percent value from 0 to 100 + """ + pedal1.set(pedal_percent_to_volts_1(percent)) + pedal2.set(pedal_percent_to_volts_2(percent)) + +def check_msg(can_bus: hil2_comp.CAN, msg_name: str | int, test_prefix: str) -> Optional[can_helper.CanMessage]: + msg = can_bus.get_last(msg_name) + mka.assert_true(msg is not None, f"{test_prefix}: VCAN message received") + return msg + +def check_brakes(msg: Optional[can_helper.CanMessage], exp_percent: float, tol_v: float, test_prefix: str): + mka.assert_eqf(msg is not None and msg.data["brake"], exp_percent, tol_v, f"{test_prefix}: brake left {exp_percent}%") + mka.assert_eqf(msg is not None and msg.data["brake_right"], exp_percent, tol_v, f"{test_prefix}: brake right {exp_percent}%") + +def check_throttles_diff(msg: Optional[can_helper.CanMessage], exp_percent1: float, exp_percent2: float, tol_v: float, test_prefix: str): + mka.assert_eqf(msg is not None and msg.data["throttle"], exp_percent1, tol_v, f"{test_prefix}: throttle left {exp_percent1}%") + mka.assert_eqf(msg is not None and msg.data["throttle_right"], exp_percent2, tol_v, f"{test_prefix}: throttle right {exp_percent2}%") + +def check_throttles(msg: Optional[can_helper.CanMessage], exp_percent: float, tol_v: float, test_prefix: str): + check_throttles_diff(msg, exp_percent, exp_percent, tol_v, test_prefix) + +def check_uart(uart: hil2_comp.DI, test_prefix: str): + for _ in range(10): + if uart.get(): + mka.assert_true(True, f"{test_prefix}: UART activity detected") + return + time.sleep(0.01) + mka.assert_true(False, f"{test_prefix}: UART activity detected") + +def float_range(start, stop, step): + while start <= stop: + yield start + start += step + +def shockpots_from_voltage(v_left: float, v_right: float) -> tuple[int, int]: + POT_VOLT_MAX = 3.0 + POT_VOLT_MIN_L = 4082.0 + POT_VOLT_MIN_R = 4090.0 + POT_MAX_DIST = 75 + POT_DIST_DROOP_L = 56 + POT_DIST_DROOP_R = 56 + + adc_left = (v_left / POT_VOLT_MAX) * POT_VOLT_MIN_L + adc_right = (v_right / POT_VOLT_MAX) * POT_VOLT_MIN_R + + shock_l = -1 * ((POT_MAX_DIST - int((adc_left / (POT_VOLT_MIN_L - POT_VOLT_MAX)) * POT_MAX_DIST)) - POT_DIST_DROOP_L) + shock_r = -1 * ((POT_MAX_DIST - int((adc_right / (POT_VOLT_MIN_R - POT_VOLT_MAX)) * POT_MAX_DIST)) - POT_DIST_DROOP_R) + + return shock_l, shock_r + + +# EV.4.7.2 ----------------------------------------------------------------------------# +def ev_4_7_2_test(h: hil2.Hil2): + """ + If brake is activated (5% pressed) and throttle is activated more than 25%, motor + must shutdown and stay shutdown until throttle is under 5% + + - brake low, throttle low, check motor on + - brake high, throttle low, check motor on + - brake high, throttle high, check motor off + - brake low, throttle mid, check motor off (sweep down to 5% on throttle) + - brake low, throttle low, check motor back on + """ + + brk1 = h.ao("Dashboard", "BRK1_RAW") + brk2 = h.ao("Dashboard", "BRK2_RAW") + thrtl1 = h.ao("Dashboard", "THRTL1_RAW") + thrtl2 = h.ao("Dashboard", "THRTL2_RAW") + vcan = h.can("HIL2", "VCAN") + + # Setup: set brake and throttle to 0% + vcan.clear() + set_both(brk1, brk2, 0) + set_both(thrtl1, thrtl2, 0) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Setup") + check_brakes(msg, 0, 0.1, "Setup") + check_throttles(msg, 0, 0.1, "Setup") + time.sleep(0.1) + + # Test 1: brake low, throttle low, check motor on + vcan.clear() + set_both(brk1, brk2, 5) + set_both(thrtl1, thrtl2, 5) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Brakes low, throttle low") + check_brakes(vcan, 5, 0.1, "Brakes low, throttle low") + check_throttles(vcan, 5, 0.1, "Brakes low, throttle low") + time.sleep(0.1) + + # Test 2: brake high, throttle low, check motor on + vcan.clear() + set_both(brk1, brk2, 50) + set_both(thrtl1, thrtl2, 5) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Brakes high, throttle low") + check_brakes(msg, 50, 0.1, "Brakes high, throttle low") + check_throttles(msg, 5, 0.1, "Brakes high, throttle low") + time.sleep(0.1) + + # Test 3: brake high, throttle high, check motor off + vcan.clear() + set_both(brk1, brk2, 50) + set_both(thrtl1, thrtl2, 50) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Brakes high, throttle high") + check_brakes(msg, 50, 0.1, "Brakes high, throttle high") + check_throttles(msg, 0, 0.1, "Brakes high, throttle high") + time.sleep(0.1) + + # Test 4: brake low, throttle mid, check motor off (sweep down to 5% on throttle) + vcan.clear() + set_both(brk1, brk2, 4) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Brakes low, throttle mid") + check_brakes(msg, 4, 0.1, "Brakes low, throttle mid") + + for p in range(50, 4, -1): + vcan.clear() + set_both(thrtl1, thrtl2, p) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, f"Brakes low, throttle {p}") + expected_throttle = 0 if p > 5 else p + check_throttles(msg, expected_throttle, 0.1, f"Brakes low, throttle {p} (expected {expected_throttle}%)") + + time.sleep(0.1) + + # Test 5: brake low, throttle mid, check motor back on + vcan.clear() + set_both(brk1, brk2, 5) + set_both(thrtl1, thrtl2, 25) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Brakes low, throttle mid") + check_brakes(msg, 5, 0.1, "Brakes low, throttle mid") + check_throttles(msg, 25, 0.1, "Brakes low, throttle mid") + + +# T.4.2.5 -----------------------------------------------------------------------------# +def t_4_2_5_test(h: hil2.Hil2): + """ + - sens1 and sens2 similar, check motor on, sdc not triggered + - sens1 and sens2 slightly different, check motor on, sdc not triggered + - sens1 and sens2 10% different, check motor on, sdc not triggered + - sens1 and sens2 slightly different, check motor on, sdc not triggered + - sens1 and sens2 10% different, check motor on, sdc not triggered + - sens1 and sens2 still 10% different (~100 msec later), check motor off, sdc not triggered + - power cycle, confirm everything resets + - sens1 and sens2 similar, check motor on, sdc not triggered + """ + thrtl1 = h.ao("Dashboard", "THRTL1_RAW") + thrtl2 = h.ao("Dashboard", "THRTL2_RAW") + vcan = h.can("HIL2", "VCAN") + sdc = h.di("Dashboard", "SDC") + + # Set 1: sens1 = left, sens2 = right ----------------------------------------------# + + # Similar, check motor on, sdc not triggered + vcan.clear() + set_both(thrtl1, thrtl2, 25) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Set 1 - Similar") + check_throttles(msg, 25, 0.1, "Set 1 - Similar") + mka.assert_false(sdc.get(), "Set 1 - Similar: SDC not triggered") + time.sleep(0.1) + + # Slightly different, check motor on, sdc not triggered + vcan.clear() + thrtl1.set(pedal_percent_to_volts_1(20)) + thrtl2.set(pedal_percent_to_volts_2(25)) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Set 1 - Slightly different") + check_throttles_diff(msg, 20, 25, 0.1, "Set 1 - Slightly different") + mka.assert_false(sdc.get(), "Set 1 - Slightly different: SDC not triggered") + time.sleep(0.1) + + # 10% different, check motor on, sdc not triggered + vcan.clear() + thrtl1.set(pedal_percent_to_volts_1(20)) + thrtl2.set(pedal_percent_to_volts_2(30)) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Set 1 - 10% different") + check_throttles_diff(msg, 20, 30, 0.1, "Set 1 - 10% different") + mka.assert_false(sdc.get(), "Set 1 - 10% different: SDC not triggered") + time.sleep(0.03) + + # Slightly different, check motor on, sdc not triggered + vcan.clear() + thrtl1.set(pedal_percent_to_volts_1(25)) + thrtl2.set(pedal_percent_to_volts_2(30)) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Set 1 - Slightly different") + check_throttles_diff(msg, 25, 30, 0.1, "Set 1 - Slightly different") + mka.assert_false(sdc.get(), "Set 1 - Slightly different: SDC not triggered") + time.sleep(0.1) + + # 10% different, check motor on, sdc not triggered + vcan.clear() + thrtl1.set(pedal_percent_to_volts_1(20)) + thrtl2.set(pedal_percent_to_volts_2(30)) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Set 1 - 10% different") + check_throttles_diff(msg, 20, 30, 0.1, "Set 1 - 10% different") + mka.assert_false(sdc.get(), "Set 1 - 10% different: SDC not triggered") + time.sleep(0.03) + + # Still 10% different (~100 msec later), check motor off, sdc not triggered + vcan.clear() + time.sleep(0.07) + msg = check_msg(vcan, PEDAL_MSG, "Set 1 - Still 10% different (~100 msec later)") + check_throttles(msg, 0, 0.1, "Set 1 - Still 10% different (~100 msec later)") + mka.assert_false(sdc.get(), "Set 1 - Still 10% different (~100 msec later): SDC not triggered") + time.sleep(0.1) + + # Power cycle and confirm everything resets + power_cycle(h) + + # Similar, check motor on, sdc not triggered + vcan.clear() + set_both(thrtl1, thrtl2, 20) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Set 1 - Similar") + check_throttles(msg, 20, 0.1, "Set 1 - Similar") + mka.assert_false(sdc.get(), "Set 1 - Similar: SDC not triggered") + time.sleep(0.1) + + # Set 2: sens1 = right, sens2 = left ----------------------------------------------# + + # Similar, check motor on, sdc not triggered + vcan.clear() + set_both(thrtl1, thrtl2, 25) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Set 2 - Similar") + check_throttles(msg, 25, 0.1, "Set 2 - Similar") + mka.assert_false(sdc.get(), "Set 2 - Similar: SDC not triggered") + time.sleep(0.1) + + # Slightly different, check motor on, sdc not triggered + vcan.clear() + thrtl1.set(pedal_percent_to_volts_1(25)) + thrtl2.set(pedal_percent_to_volts_2(20)) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Set 2 - Slightly different") + check_throttles_diff(msg, 25, 20, 0.1, "Set 2 - Slightly different") + mka.assert_false(sdc.get(), "Set 2 - Slightly different: SDC not triggered") + time.sleep(0.1) + + # 10% different, check motor on, sdc not triggered + vcan.clear() + thrtl1.set(pedal_percent_to_volts_1(30)) + thrtl2.set(pedal_percent_to_volts_2(20)) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Set 2 - 10% different") + check_throttles_diff(msg, 30, 20, 0.1, "Set 2 - 10% different") + mka.assert_false(sdc.get(), "Set 2 - 10% different: SDC not triggered") + time.sleep(0.03) + + # Slightly different, check motor on, sdc not triggered + vcan.clear() + thrtl1.set(pedal_percent_to_volts_1(30)) + thrtl2.set(pedal_percent_to_volts_2(25)) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Set 2 - Slightly different") + check_throttles_diff(msg, 30, 25, 0.1, "Set 2 - Slightly different") + mka.assert_false(sdc.get(), "Set 2 - Slightly different: SDC not triggered") + time.sleep(0.1) + + # 10% different, check motor on, sdc not triggered + vcan.clear() + thrtl1.set(pedal_percent_to_volts_1(30)) + thrtl2.set(pedal_percent_to_volts_2(20)) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Set 2 - 10% different") + check_throttles_diff(msg, 30, 20, 0.1, "Set 2 - 10% different") + mka.assert_false(sdc.get(), "Set 2 - 10% different: SDC not triggered") + time.sleep(0.03) + + # Still 10% different (~100 msec later), check motor off, sdc not triggered + vcan.clear() + time.sleep(0.07) + msg = check_msg(vcan, PEDAL_MSG, "Set 2 - Still 10% different (~100 msec later)") + check_throttles(msg, 0, 0.1, "Set 2 - Still 10% different (~100 msec later)") + mka.assert_false(sdc.get(), "Set 2 - Still 10% different (~100 msec later): SDC not triggered") + time.sleep(0.1) + + # Power cycle and confirm everything resets + power_cycle(h) + + # Similar, check motor on, sdc not triggered + vcan.clear() + set_both(thrtl1, thrtl2, 20) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Set 2 - Similar") + check_throttles(msg, 20, 0.1, "Set 2 - Similar") + mka.assert_false(sdc.get(), "Set 2 - Similar: SDC not triggered") + time.sleep(0.1) + +# T.4.2.10 ----------------------------------------------------------------------------# +def t_4_2_10_test(h: hil2.Hil2): + """ + - sens1 and sens2 ok, check motor on, sdc not triggered + - both are out of range high, check motor off, sdc triggered + """ + + thrtl1 = h.ao("Dashboard", "THRTL1_RAW") + thrtl2 = h.ao("Dashboard", "THRTL2_RAW") + vcan = h.can("HIL2", "VCAN") + sdc = h.di("Dashboard", "SDC") + + # Set 1: out of range high --------------------------------------------------------# + + # Both ok, check motor on, sdc not triggered + vcan.clear() + set_both(thrtl1, thrtl2, 25) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Both ok") + check_throttles(msg, 25, 0.1, "Both ok") + mka.assert_false(sdc.get(), "Both ok: SDC not triggered") + time.sleep(0.1) + + # Both out of range high, check motor off, sdc triggered + vcan.clear() + thrtl1.set(5.5) # volts + thrtl2.set(5.5) # volts + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Both out of range high") + check_throttles(msg, 0, 0.1, "Both out of range high") + mka.assert_true(sdc.get(), "Both out of range high: SDC triggered") + time.sleep(0.1) + + # Power cycle and confirm everything resets + power_cycle(h) + + # Both ok, check motor on, sdc not triggered + vcan.clear() + set_both(thrtl1, thrtl2, 20) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Both ok") + check_throttles(msg, 20, 0.1, "Both ok") + mka.assert_false(sdc.get(), "Both ok: SDC not triggered") + time.sleep(0.1) + + + # Set 2: throttle 1 disconnects ---------------------------------------------------# + + # Sens1 disconnected, check motor off, sdc triggered + vcan.clear() + thrtl2.set(pedal_percent_to_volts_2(25)) + thrtl1.hiZ() + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Sens1 disconnected") + check_throttles(msg, 0, 0.1, "Sens1 disconnected") + mka.assert_true(sdc.get(), "Sens1 disconnected: SDC triggered") + time.sleep(0.1) + + # Power cycle and confirm everything resets + power_cycle(h) + + # Sens1 and sens2 ok, check motor on, sdc not triggered + vcan.clear() + set_both(thrtl1, thrtl2, 20) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Sens1 and sens2 ok") + check_throttles(msg, 20, 0.1, "Sens1 and sens2 ok") + mka.assert_false(sdc.get(), "Sens1 and sens2 ok: SDC not triggered") + time.sleep(0.1) + + # Set 3: throttle 2 disconnects ---------------------------------------------------# + + # Sens2 disconnected, check motor off, sdc triggered + vcan.clear() + thrtl1.set(pedal_percent_to_volts_1(25)) + thrtl2.hiZ() + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Sens2 disconnected") + check_throttles(msg, 0, 0.1, "Sens2 disconnected") + mka.assert_true(sdc.get(), "Sens2 disconnected: SDC triggered") + time.sleep(0.1) + + # Power cycle and confirm everything resets + power_cycle(h) + + # Sens1 and sens2 ok, check motor on, sdc not triggered + vcan.clear() + set_both(thrtl1, thrtl2, 20) + time.sleep(SLEEP_TIME) + msg = check_msg(vcan, PEDAL_MSG, "Sens1 and sens2 ok") + check_throttles(msg, 20, 0.1, "Sens1 and sens2 ok") + mka.assert_false(sdc.get(), "Sens1 and sens2 ok: SDC not triggered") + time.sleep(0.1) + + +# Buttons test ------------------------------------------------------------------------# +def buttons_test(h: hil2.Hil2): + """ + 4 buttons, gpio on the UART line + - Try different combinations of the buttons and check that there is activity on the UART + """ + + up = h.do("Dashboard", "UP") + down = h.do("Dashboard", "DOWN") + select = h.do("Dashboard", "SELECT") + start = h.do("Dashboard", "START") + uart = h.di("Dashboard", "USART_LCD_TX") + + # Setup: set all buttons to not pressed + up.set(False) + down.set(False) + select.set(False) + start.set(False) + + # Test 1: press UP, check UART activity + up.set(True) + check_uart(uart, "Press UP") + up.set(False) + time.sleep(0.1) + + # Test 2: press DOWN, check UART activity + down.set(True) + check_uart(uart, "Press DOWN") + down.set(False) + time.sleep(0.1) + + # Test 3: press SELECT, check UART activity + select.set(True) + check_uart(uart, "Press SELECT") + select.set(False) + time.sleep(0.1) + + # Test 4: press START, check UART activity + start.set(True) + check_uart(uart, "Press START") + start.set(False) + time.sleep(0.1) + + +# Shockpot test -----------------------------------------------------------------------# +def shockpot_test(h: hil2.Hil2): + """ + DAC -> sweep 0 to 3v and check CAN values + Read can messages to check the values + """ + + left = h.ao("Dashboard", "LeftPot") + right = h.ao("Dashboard", "RightPot") + vcan = h.can("HIL2", "VCAN") + + for lv in float_range(0, 3, 0.2): + left.set(lv) + for rv in float_range(0, 3, 0.2): + vcan.clear() + right.set(rv) + time.sleep(SLEEP_TIME) + + msg = check_msg(vcan, SHOCK_MSG, f"Left {lv:.1f}V, Right {rv:.1f}V") + exp_l, exp_r = shockpots_from_voltage(lv, rv) + mka.assert_true(msg is not None, f"Left {lv:.1f}V, Right {rv:.1f}V: CAN message received") + mka.assert_true(msg is not None and msg["left_shock"] == exp_l, f"Left {lv:.1f}V, Right {rv:.1f}V: left shock {exp_l}") + mka.assert_true(msg is not None and msg["right_shock"] == exp_r, f"Left {lv:.1f}V, Right {rv:.1f}V: right shock {exp_r}") + + + + + +# Main --------------------------------------------------------------------------------# +def main(): + logging.basicConfig(level=logging.DEBUG) + + with hil2.Hil2( + "./tests/dashboard/config.json", + "./device_configs", + "./netmap/per24.csv", + "TODO" + ) as h: + mka.set_setup_fn(lambda: power_cycle(h)) + mka.set_teardown_fn(h.close) + + mka.add_test(ev_4_7_2_test, h) + mka.add_test(t_4_2_5_test, h) + mka.add_test(t_4_2_10_test, h) + mka.add_test(buttons_test, h) + mka.add_test(shockpot_test, h) + + mka.run_tests() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/example/__init__.py b/tests/example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/example/config.json b/tests/example/config.json new file mode 100644 index 0000000..34f531a --- /dev/null +++ b/tests/example/config.json @@ -0,0 +1,5 @@ +{ + "hil_devices": [ + { "name":"HIL2", "config":"teensy_pcb.json", "id":1 } + ] +} diff --git a/tests/example/test.py b/tests/example/test.py new file mode 100644 index 0000000..7046238 --- /dev/null +++ b/tests/example/test.py @@ -0,0 +1,159 @@ +import hil2.hil2 as hil2 +import mk_assert.mk_assert as mka +import time + +import logging + +def float_range(start, stop, step): + while start <= stop: + yield start + start += step + +# def do_test(h: hil2.Hil2): +# do_2 = h.do("HIL2Bread", "DO@2") +# di_28 = h.di("HIL2Bread", "DI@28") +# ai_14 = h.ai("HIL2Bread", "AI@14") + +# do_2.set(True) +# val = di_28.get() +# a = ai_14.get() +# print(f"DI@28: {val}, AI@14: {a}") +# mka.assert_true(val, "DI@28 should be True after setting DO@2 to True") +# mka.assert_eqf(a, 3.3, 0.5, "AI@14 should be approximately 3.3V") + +# do_2.set(False) +# val = di_28.get() +# a = ai_14.get() +# print(f"DI@28: {val}, AI@14: {a}") +# mka.assert_false(val, "DI@28 should be False after setting DO@2 to False") +# mka.assert_eqf(a, 0.0, 0.5, "AI@14 should be approximately 0.0V") + +# vcan = h.can("HIL2", "VCAN") +# vcan.send("BrakeLeft", { "raw_reading": 12 }) + +def do_di_test(h: hil2.Hil2): + # do = h.do("HIL2", "DO1") + + for i in range(0, 8): + do = h.do("HIL2", f"DO{i+1}") + print(f"Setting DO{i+1} LOW") + do.set(False) + + input("Press Enter to continue...") + + do = h.do("HIL2", f"DO{4}") + print(f"Setting DO4 HIGH") + do.set(True) + time.sleep(0.1) + + input("Press Enter to continue...") + + + # print("Setting DO4 LOW") + # do.set(False) + # time.sleep(0.1) + # input("Press Enter to continue...") + # state = True + # while True: + # print("") + # print(f"Setting DO1 to {state}") + # do.set(state) + # time.sleep(0.05) + + # for i in range(0, 16): + # di = h.di("HIL2", f"DMUX_{i}") + # val = di.get() + # add = "" if not val else " (HIGH)" + # print(f"DI_DMUX_{i}: {val} {add}") + # time.sleep(0.03) + + # state = not state + # input("Press Enter to toggle DO1...") + +def ao_ai_test(h: hil2.Hil2): + # ao1 = h.ao("HIL2", "DAC1") + # ao2 = h.ao("HIL2", "DAC2") + + # while True: + # for voltage in float_range(0.0, 5.0, 0.2): + # print("") + + # print(f"Setting DAC1 to {voltage}V") + # ao1.set(voltage) + # time.sleep(0.2) + + + for i in range(0, 8): + ao = h.ao("HIL2", f"DAC{i+1}") + print(f"Setting DAC{i+1} to 0.0V") + ao.set(0.0) + + for i in range(0, 8): + ao = h.ao("HIL2", f"DAC{i+1}") + print(f"Setting DAC{i+1} to 2.5V") + ao.set(2.5) + input("Press Enter to continue...") + + + + + # dai = h.ai("HIL2", "DAI2") + # ai = h.ai("HIL2", "5vMUX_0") + + # val = dai.get() + # print(f"Initial DAI2 reading: {val}V") + + # for voltage in float_range(0.0, 5.0, 0.2): + # print("") + + # print(f"Setting DACS to {voltage}V") + # ao1.set(voltage) + # ao2.set(voltage) + # time.sleep(0.02) + + # val = dai.get() + # mka.assert_eqf(val, voltage, 0.02, f"DAI2 should read approximately {voltage}V (got {val}V)") + + # val = ai.get() + # mka.assert_eqf(val, voltage, 0.02, f"5vMUX_0 should read approximately {voltage}V (got {val}V)") + + # input("Press Enter to continue...") + +def main(): + logging.basicConfig(level=logging.DEBUG) + + with hil2.Hil2( + "./tests/example/config.json", + "device_configs", + None, + None + ) as h: + # mka.add_test(do_di_test, h) + mka.add_test(ao_ai_test, h) + + mka.run_tests() + + # v_bat = h.ao("Main_Module", "VBatt") + # v_bat.set(3.2) + # val = v_bat.get() + + # h.set_ao("Main_Module", "VBatt", 3.2) + # val = h.get_ao("Main_Module", "VBatt") + + # h.get_last_can("HIL2", "MCAN", "Signal") + + # mcan = h.can("HIL2", "MCAN") + # mcan.send("Signal", {}) + # mcan.send(23, {}) + # sig_dict = mcan.get_last("Signal") + # sig_dict = mcan.get_last(23) + # sig_dicts = mcan.get_all("Signal") + # sig_dicts = mcan.get_all(23) + # sig_dicts = mcan.get_all() + # mcan.clear("Signal") + # mcan.clear(23) + # mcan.clear() + + +if __name__ == "__main__": + main()