From fd9d1ec714ec9740edb18f179b67abaf6aadc1b6 Mon Sep 17 00:00:00 2001 From: Jonny Date: Tue, 2 Sep 2025 08:59:25 +0100 Subject: [PATCH] Add CAN log replay based battery tests --- test/CMakeLists.txt | 7 +- .../can_logs/24_RjxzsBms_overvoltage.txt | 1 + .../can_logs/37_MgHsPhev_good.txt | 6 + .../can_logs/37_MgHsPhev_overvoltage.txt | 2 + .../can_logs/5_BydAtto3_good.txt | 8 + test/can_log_based/canlog_safety_tests.cpp | 147 ++++++++++++++++++ test/can_log_based/utils.cpp | 98 ++++++++++++ test/can_log_based/utils.h | 10 ++ test/tests.cpp | 3 + 9 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 test/can_log_based/can_logs/24_RjxzsBms_overvoltage.txt create mode 100644 test/can_log_based/can_logs/37_MgHsPhev_good.txt create mode 100644 test/can_log_based/can_logs/37_MgHsPhev_overvoltage.txt create mode 100644 test/can_log_based/can_logs/5_BydAtto3_good.txt create mode 100644 test/can_log_based/canlog_safety_tests.cpp create mode 100644 test/can_log_based/utils.cpp create mode 100644 test/can_log_based/utils.h diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 398cb69a..b3278747 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -63,13 +63,18 @@ include_directories("${source_dir}/googletest/include" include_directories(emul) # For eModBus -add_compile_definitions(ESP32 HW_LILYGO NISSAN_LEAF_BATTERY) +add_compile_definitions(ESP32 HW_LILYGO COMMON_IMAGE) # add the executable add_executable(tests tests.cpp safety_tests.cpp battery/NissanLeafTest.cpp + can_log_based/canlog_safety_tests.cpp + can_log_based/utils.cpp + ../Software/src/communication/can/obd.cpp + ../Software/src/communication/contactorcontrol/comm_contactorcontrol.cpp + ../Software/src/communication/rs485/comm_rs485.cpp ../Software/src/devboard/safety/safety.cpp ../Software/src/devboard/hal/hal.cpp ../Software/src/devboard/utils/events.cpp diff --git a/test/can_log_based/can_logs/24_RjxzsBms_overvoltage.txt b/test/can_log_based/can_logs/24_RjxzsBms_overvoltage.txt new file mode 100644 index 00000000..8d7c133d --- /dev/null +++ b/test/can_log_based/can_logs/24_RjxzsBms_overvoltage.txt @@ -0,0 +1 @@ +(123.893) RX0 f5 [8] 03 ff ff 00 00 00 00 00 diff --git a/test/can_log_based/can_logs/37_MgHsPhev_good.txt b/test/can_log_based/can_logs/37_MgHsPhev_good.txt new file mode 100644 index 00000000..9bb70398 --- /dev/null +++ b/test/can_log_based/can_logs/37_MgHsPhev_good.txt @@ -0,0 +1,6 @@ +(84215.921) RX0 295 [8] d5 1d 00 01 a6 4e 20 01 +(84215.922) RX0 171 [8] 00 34 b5 75 4d 03 78 c2 +(84215.923) RX0 172 [8] 3b e0 d2 3f 20 dd 00 00 +(84216.021) RX0 173 [8] 00 00 c8 c8 0e 2d 0e 29 +(84216.027) RX0 2a2 [8] 78 77 04 c7 40 76 00 6a +(84216.028) RX0 3ac [8] 01 a6 01 2f 05 1d 4e 20 diff --git a/test/can_log_based/can_logs/37_MgHsPhev_overvoltage.txt b/test/can_log_based/can_logs/37_MgHsPhev_overvoltage.txt new file mode 100644 index 00000000..fc1a81be --- /dev/null +++ b/test/can_log_based/can_logs/37_MgHsPhev_overvoltage.txt @@ -0,0 +1,2 @@ +# 450V (overvoltage) +(13148.893) RX0 3ac [8] 01 d4 01 6f 07 08 4d d0 diff --git a/test/can_log_based/can_logs/5_BydAtto3_good.txt b/test/can_log_based/can_logs/5_BydAtto3_good.txt new file mode 100644 index 00000000..8d6ccc1b --- /dev/null +++ b/test/can_log_based/can_logs/5_BydAtto3_good.txt @@ -0,0 +1,8 @@ +# voltage +(0.001) RX0 7ef [8] 00 00 00 08 81 34 ee ee +# lowest cell +(0.002) RX0 7ef [8] 00 00 00 2f 30 ee ee ee +# highest cell +(0.002) RX0 7ef [8] 00 00 00 31 40 ee ee ee + +# this is enough to pass (all the other params have defaults) \ No newline at end of file diff --git a/test/can_log_based/canlog_safety_tests.cpp b/test/can_log_based/canlog_safety_tests.cpp new file mode 100644 index 00000000..489ebbce --- /dev/null +++ b/test/can_log_based/canlog_safety_tests.cpp @@ -0,0 +1,147 @@ +#include + +#include "utils.h" + +#include "../../Software/src/battery/BATTERIES.h" +#include "../../Software/src/devboard/utils/events.h" + +#include + +namespace fs = std::filesystem; + +// These tests replay CAN logs against individual batteries to check that they +// are correctly parsed, and that safety mechanisms work. + +// The base class for our tests +class CanLogTestFixture : public testing::Test { + public: + CanLogTestFixture(fs::path path) : path_(std::move(path)) {} + // Optional: + // static void SetUpTestSuite() { ... } + // static void TearDownTestSuite() { ... } + + void SetUp() override { + // Reset the datalayer and events before each test + datalayer = DataLayer(); + reset_all_events(); + if (battery) { + delete battery; + battery = nullptr; + } + + // Assume a 90s NMC pack for custom-BMS batteries + user_selected_max_pack_voltage_dV = 378 + 10; + user_selected_min_pack_voltage_dV = 261 - 10; + user_selected_max_cell_voltage_mV = 4200 + 20; + user_selected_min_cell_voltage_mV = 2900 - 20; + + // Extract battery type from log filename + std::string filename = path_.filename().string(); + std::string batteryId = filename.substr(0, filename.find('_')); + user_selected_battery_type = (BatteryType)std::stoi(batteryId); + setup_battery(); + + // Initialize datalayer to invalid values + datalayer.battery.status.voltage_dV = 0; + datalayer.battery.status.current_dA = INT16_MIN; + datalayer.battery.status.cell_min_voltage_mV = 0; + datalayer.battery.status.cell_max_voltage_mV = 0; + datalayer.battery.status.real_soc = UINT16_MAX; + datalayer.battery.status.temperature_max_dC = INT16_MIN; + datalayer.battery.status.temperature_min_dC = INT16_MIN; + } + + void TearDown() override { + if (battery) { + delete battery; + battery = nullptr; + } + } + + void ProcessLog() { + std::vector parsedMessages = parse_can_log_file(path_); + + for (const auto& msg : parsedMessages) { + dynamic_cast(battery)->handle_incoming_can_frame(msg); + dynamic_cast(battery)->update_values(); + } + + update_machineryprotection(); + } + + void PrintValues() { + std::cout << "Battery voltage: " << (datalayer.battery.status.voltage_dV / 10.0) << " V" << std::endl; + std::cout << "Battery current: " << (datalayer.battery.status.current_dA / 10.0) << " A" << std::endl; + std::cout << "Battery cell min voltage: " << datalayer.battery.status.cell_min_voltage_mV << " mV" << std::endl; + std::cout << "Battery cell max voltage: " << datalayer.battery.status.cell_max_voltage_mV << " mV" << std::endl; + std::cout << "Battery real SoC: " << (datalayer.battery.status.real_soc / 100.0) << " %" << std::endl; + std::cout << "Battery temperature max: " << (datalayer.battery.status.temperature_max_dC / 10.0) << " C" + << std::endl; + std::cout << "Battery temperature min: " << (datalayer.battery.status.temperature_min_dC / 10.0) << " C" + << std::endl; + } + + private: + fs::path path_; +}; + +// Check that the parsed logs populate the required datalayer values for Battery +// Emulator to function. +class AllValuesPresentTest : public CanLogTestFixture { + public: + explicit AllValuesPresentTest(fs::path path) : CanLogTestFixture(path) {} + void TestBody() override { + ProcessLog(); + // When debugging, uncomment this to see the parsed values + //PrintValues(); + + EXPECT_NE(datalayer.battery.status.voltage_dV, 0); + // TODO: Current isn't actually a requirement? check power instead? + //EXPECT_NE(datalayer.battery.status.current_dA, INT16_MIN); + EXPECT_NE(datalayer.battery.status.cell_min_voltage_mV, 0); + EXPECT_NE(datalayer.battery.status.cell_max_voltage_mV, 0); + EXPECT_NE(datalayer.battery.status.real_soc, UINT16_MAX); + EXPECT_NE(datalayer.battery.status.temperature_max_dC, INT16_MIN); + EXPECT_NE(datalayer.battery.status.temperature_min_dC, INT16_MIN); + + EXPECT_EQ(get_event_pointer(EVENT_BATTERY_OVERVOLTAGE)->occurences, 0); + EXPECT_EQ(get_event_pointer(EVENT_BATTERY_UNDERVOLTAGE)->occurences, 0); + } +}; + +// Check that the parsed logs correctly trigger an overvoltage event. +class OverVoltageTest : public CanLogTestFixture { + public: + explicit OverVoltageTest(fs::path path) : CanLogTestFixture(path) {} + void TestBody() override { + ProcessLog(); + // When debugging, uncomment this to see the parsed values + //PrintValues(); + + EXPECT_EQ(get_event_pointer(EVENT_BATTERY_OVERVOLTAGE)->occurences, 1); + } +}; + +void RegisterCanLogTests() { + // The logs should be named as follows: + // __good.txt (all values present) + // __overvoltage.txt (triggers overvoltage event) + // where battery_type is the integer corresponding to the BatteryType enum + + std::string directoryPath = "../can_log_based/can_logs"; + + for (const auto& entry : fs::directory_iterator(directoryPath)) { + if (entry.is_regular_file() && ends_with(entry.path(), "_good.txt")) { + + testing::RegisterTest("CanLogTestFixture", ("TestAllValuesPresent_" + entry.path().filename().string()).c_str(), + nullptr, entry.path().filename().string().c_str(), __FILE__, __LINE__, + [=]() -> CanLogTestFixture* { return new AllValuesPresentTest(entry.path()); }); + } + if (entry.is_regular_file() && ends_with(entry.path(), "_overvoltage.txt")) { + + testing::RegisterTest("CanLogTestFixture", ("TestOverVoltage_" + entry.path().filename().string()).c_str(), + nullptr, entry.path().filename().string().c_str(), __FILE__, __LINE__, + [=]() -> CanLogTestFixture* { return new OverVoltageTest(entry.path()); }); + } + } +} diff --git a/test/can_log_based/utils.cpp b/test/can_log_based/utils.cpp new file mode 100644 index 00000000..4fb36e9f --- /dev/null +++ b/test/can_log_based/utils.cpp @@ -0,0 +1,98 @@ +#include "utils.h" + +#include + +namespace fs = std::filesystem; + +bool ends_with(const std::string& str, const std::string& suffix) { + return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +void print_frame(const CAN_frame& frame) { + std::cout << "ID: " << std::hex << frame.ID << ", DLC: " << (int)frame.DLC << ", Data: "; + for (int i = 0; i < frame.DLC; ++i) { + std::cout << std::hex << (int)frame.data.u8[i] << " "; + } + std::cout << std::dec << "\n"; +} + +CAN_frame parse_can_log_line(const std::string& logLine) { + std::stringstream ss(logLine); + CAN_frame frame = {}; + char dummy; + + double timestamp; + std::string interfaceName; + + // timestamp and interface name are parsed but not used + ss >> dummy >> timestamp >> dummy; + ss >> interfaceName; + + // parse hexadecimal CAN ID + ss >> std::hex >> frame.ID; + if (ss.fail()) { + throw std::runtime_error("Invalid format: Failed to parse CAN ID."); + } + // check whether the ID is in the extended range + frame.ext_ID = (frame.ID > 0x7FF); + + // parse the data length + int dlc_val; + ss >> dummy; // Consume '[' + if (ss.fail() || dummy != '[') { + throw std::runtime_error("Invalid format: Missing opening bracket for data length."); + } + ss >> dlc_val; + frame.DLC = static_cast(dlc_val); + ss >> dummy; // Consume ']' + if (ss.fail() || dummy != ']') { + throw std::runtime_error("Invalid format: Missing closing bracket for data length."); + } + // crudely assume CAN FD if DLC > 8 + frame.FD = (frame.DLC > 8); + + // parse the actual data bytes + unsigned int byte; + for (int i = 0; i < frame.DLC; ++i) { + ss >> std::hex >> byte; + if (ss.fail()) { + throw std::runtime_error("Fewer data bytes than specified by data length."); + } + frame.data.u8[i] = static_cast(byte); + } + + return frame; +} + +std::vector parse_can_log_file(const fs::path& filePath) { + std::ifstream logFile(filePath); + if (!logFile.is_open()) { + std::cerr << "Error: Could not open file " << filePath << std::endl; + return {}; + } + + std::vector frames; + std::string line; + int lineNumber = 0; + + // read the file line by line + while (std::getline(logFile, line)) { + lineNumber++; + if (line.empty()) { + continue; + } + + if (line[0] == '#' || line[0] == ';') { + continue; // skip comment lines + } + + try { + frames.push_back(parse_can_log_line(line)); + } catch (const std::runtime_error& e) { + std::cerr << "Warning: Skipping malformed line " << lineNumber << " in " << filePath.filename() + << ". Reason: " << e.what() << std::endl; + } + } + + return frames; +} diff --git a/test/can_log_based/utils.h b/test/can_log_based/utils.h new file mode 100644 index 00000000..9de74a98 --- /dev/null +++ b/test/can_log_based/utils.h @@ -0,0 +1,10 @@ +#include "../../Software/src/devboard/utils/types.h" + +#include +#include + +namespace fs = std::filesystem; + +bool ends_with(const std::string& str, const std::string& suffix); + +std::vector parse_can_log_file(const fs::path& filePath); diff --git a/test/tests.cpp b/test/tests.cpp index 29cf7241..4eeff7bf 100644 --- a/test/tests.cpp +++ b/test/tests.cpp @@ -5,8 +5,11 @@ #include "../Software/src/devboard/safety/safety.h" #include "../Software/src/devboard/utils/events.h" +void RegisterCanLogTests(void); + int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); + RegisterCanLogTests(); return RUN_ALL_TESTS(); }