mirror of
https://github.com/dalathegreat/Battery-Emulator.git
synced 2025-10-03 17:59:27 +02:00
Add CAN log replay based battery tests
This commit is contained in:
parent
7f8f48756d
commit
fd9d1ec714
9 changed files with 281 additions and 1 deletions
|
@ -63,13 +63,18 @@ include_directories("${source_dir}/googletest/include"
|
||||||
include_directories(emul)
|
include_directories(emul)
|
||||||
|
|
||||||
# For eModBus
|
# For eModBus
|
||||||
add_compile_definitions(ESP32 HW_LILYGO NISSAN_LEAF_BATTERY)
|
add_compile_definitions(ESP32 HW_LILYGO COMMON_IMAGE)
|
||||||
|
|
||||||
# add the executable
|
# add the executable
|
||||||
add_executable(tests
|
add_executable(tests
|
||||||
tests.cpp
|
tests.cpp
|
||||||
safety_tests.cpp
|
safety_tests.cpp
|
||||||
battery/NissanLeafTest.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/safety/safety.cpp
|
||||||
../Software/src/devboard/hal/hal.cpp
|
../Software/src/devboard/hal/hal.cpp
|
||||||
../Software/src/devboard/utils/events.cpp
|
../Software/src/devboard/utils/events.cpp
|
||||||
|
|
1
test/can_log_based/can_logs/24_RjxzsBms_overvoltage.txt
Normal file
1
test/can_log_based/can_logs/24_RjxzsBms_overvoltage.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(123.893) RX0 f5 [8] 03 ff ff 00 00 00 00 00
|
6
test/can_log_based/can_logs/37_MgHsPhev_good.txt
Normal file
6
test/can_log_based/can_logs/37_MgHsPhev_good.txt
Normal file
|
@ -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
|
2
test/can_log_based/can_logs/37_MgHsPhev_overvoltage.txt
Normal file
2
test/can_log_based/can_logs/37_MgHsPhev_overvoltage.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# 450V (overvoltage)
|
||||||
|
(13148.893) RX0 3ac [8] 01 d4 01 6f 07 08 4d d0
|
8
test/can_log_based/can_logs/5_BydAtto3_good.txt
Normal file
8
test/can_log_based/can_logs/5_BydAtto3_good.txt
Normal file
|
@ -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)
|
147
test/can_log_based/canlog_safety_tests.cpp
Normal file
147
test/can_log_based/canlog_safety_tests.cpp
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include "../../Software/src/battery/BATTERIES.h"
|
||||||
|
#include "../../Software/src/devboard/utils/events.h"
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
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<CAN_frame> parsedMessages = parse_can_log_file(path_);
|
||||||
|
|
||||||
|
for (const auto& msg : parsedMessages) {
|
||||||
|
dynamic_cast<CanBattery*>(battery)->handle_incoming_can_frame(msg);
|
||||||
|
dynamic_cast<CanBattery*>(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:
|
||||||
|
// <battery_type>_<battery class name>_good.txt (all values present)
|
||||||
|
// <battery_type>_<battery class name>_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()); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
98
test/can_log_based/utils.cpp
Normal file
98
test/can_log_based/utils.cpp
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
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<uint8_t>(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<uint8_t>(byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<CAN_frame> 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<CAN_frame> 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;
|
||||||
|
}
|
10
test/can_log_based/utils.h
Normal file
10
test/can_log_based/utils.h
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#include "../../Software/src/devboard/utils/types.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
bool ends_with(const std::string& str, const std::string& suffix);
|
||||||
|
|
||||||
|
std::vector<CAN_frame> parse_can_log_file(const fs::path& filePath);
|
|
@ -5,8 +5,11 @@
|
||||||
#include "../Software/src/devboard/safety/safety.h"
|
#include "../Software/src/devboard/safety/safety.h"
|
||||||
#include "../Software/src/devboard/utils/events.h"
|
#include "../Software/src/devboard/utils/events.h"
|
||||||
|
|
||||||
|
void RegisterCanLogTests(void);
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
testing::InitGoogleTest(&argc, argv);
|
testing::InitGoogleTest(&argc, argv);
|
||||||
|
RegisterCanLogTests();
|
||||||
return RUN_ALL_TESTS();
|
return RUN_ALL_TESTS();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue