From 9dcb138ffe0e16df1fba5aa7098fff480ca548d2 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:07:38 -0700 Subject: [PATCH 01/12] WIP: Add file to add reverse mapping for file name hashes --- .../test_generate_file_hash_mapping/main.cpp | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 ECU_V2/test/test_generate_file_hash_mapping/main.cpp diff --git a/ECU_V2/test/test_generate_file_hash_mapping/main.cpp b/ECU_V2/test/test_generate_file_hash_mapping/main.cpp new file mode 100644 index 0000000..1ed3910 --- /dev/null +++ b/ECU_V2/test/test_generate_file_hash_mapping/main.cpp @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include + +#include "unity.h" + +#include "assert.hpp" +#include "util.hpp" + +namespace fs = std::filesystem; + +void setUp(void) {} + +void tearDown(void) {} + +int main(int argc, char **argv) { + std::vector target_dirs = {"lib", "src"}; + std::stringstream csv_entries; + + for (const auto& target_dir : target_dirs) { + for (const auto& dir_entry : fs::recursive_directory_iterator("lib")) { + if (!fs::is_regular_file(dir_entry)) continue; + + auto path = dir_entry.path(); + auto extension = path.extension(); + if (extension == ".hpp" || extension == ".cpp" || extension == ".c" || extension == ".h") { + /* This is a code file, so we should generate its hash name. */ + + /* Because some developers are on windows, we create a mapping for both forward and + * backward slashes. */ + std::string forward = path.generic_string(); + uint32_t forward_hash = str_hash(forward.c_str()); + std::string backward = path.generic_string(); + std::replace(backward.begin(), backward.end(), '/', '\\'); + uint32_t backward_hash = str_hash(backward.c_str()); + + std::cerr << forward_hash << ",\"" << forward << "\"\n"; + std::cerr << backward_hash << ",\"" << backward << "\"\n"; + } + } + } +} From 2868a0a9683e5b64325cbb771669799bf4ef72b1 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:11:57 -0700 Subject: [PATCH 02/12] Add assert codes to current asserts --- ECU_V2/lib/common/assert.hpp | 12 +++---- ECU_V2/lib/common/can_serde.cpp | 62 ++++++++++++++++----------------- ECU_V2/lib/common/ecu.cpp | 10 +++--- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/ECU_V2/lib/common/assert.hpp b/ECU_V2/lib/common/assert.hpp index 07aa6d0..1497f6e 100644 --- a/ECU_V2/lib/common/assert.hpp +++ b/ECU_V2/lib/common/assert.hpp @@ -29,7 +29,10 @@ enum class AssertCode: uint8_t { Unknown = 0, - ThrottleOutOfRange = 1, + BadMessage = 1, + ThrottleOutOfRange = 2, + ThrottleDisagreement = 3, + ThrottleAndBrakePressed = 4, }; enum class AssertLevel { @@ -53,8 +56,5 @@ void assert_failed(AssertLevel level, const char* file, int line, AssertCode err } \ } while (0) -#define SOFT_ASSERT(condition) GENERIC_ASSERT(AssertLevel::Soft, (condition), AssertCode::Unknown) -#define SAFETY_ASSERT(condition) GENERIC_ASSERT(AssertLevel::Safety, (condition), AssertCode::Unknown) - -#define SOFT_ASSERT_CODE(condition, code) GENERIC_ASSERT(AssertLevel::Soft, (condition), (code)) -#define SAFETY_ASSERT_CODE(condition, code) GENERIC_ASSERT(AssertLevel::Safety, (condition), (code)) +#define SOFT_ASSERT(condition, code) GENERIC_ASSERT(AssertLevel::Soft, (condition), (code)) +#define SAFETY_ASSERT(condition, code) GENERIC_ASSERT(AssertLevel::Safety, (condition), (code)) diff --git a/ECU_V2/lib/common/can_serde.cpp b/ECU_V2/lib/common/can_serde.cpp index 22d6fdd..da72746 100644 --- a/ECU_V2/lib/common/can_serde.cpp +++ b/ECU_V2/lib/common/can_serde.cpp @@ -70,7 +70,7 @@ void write_f32_le(uint8_t* buf, float value) { bool parse_start_switch(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::StartSwitch) && msg.len >= 1); + SAFETY_ASSERT(msg.id == static_cast(MessageId::StartSwitch) && msg.len >= 1, AssertCode::BadMessage); return msg.buf[0] != 0; } @@ -83,7 +83,7 @@ CAN_message_t create_start_switch(bool value) { uint16_t parse_throttle_one_position(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::ThrottleOnePosition) && msg.len >= 2); + SAFETY_ASSERT(msg.id == static_cast(MessageId::ThrottleOnePosition) && msg.len >= 2, AssertCode::BadMessage); return read_u16_le(msg.buf); } @@ -96,7 +96,7 @@ CAN_message_t create_throttle_one_position(uint16_t value) { uint16_t parse_throttle_two_position(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::ThrottleTwoPosition) && msg.len >= 2); + SAFETY_ASSERT(msg.id == static_cast(MessageId::ThrottleTwoPosition) && msg.len >= 2, AssertCode::BadMessage); return read_u16_le(msg.buf); } @@ -109,7 +109,7 @@ CAN_message_t create_throttle_two_position(uint16_t value) { uint16_t parse_brake_pressure(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::BrakePressure) && msg.len >= 2); + SAFETY_ASSERT(msg.id == static_cast(MessageId::BrakePressure) && msg.len >= 2, AssertCode::BadMessage); return read_u16_le(msg.buf); } @@ -122,10 +122,10 @@ CAN_message_t create_brake_pressure(uint16_t value) { RvcMessage parse_rvc(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::Rvc) && msg.len >= 5); + SAFETY_ASSERT(msg.id == static_cast(MessageId::Rvc) && msg.len >= 5, AssertCode::BadMessage); // Make sure the rvc type is one of the six valid types. uint8_t rvc_type = msg.buf[0]; - SAFETY_ASSERT(rvc_type <= 5); + SAFETY_ASSERT(rvc_type <= 5, AssertCode::BadMessage); float value = read_f32_le(&msg.buf[1]); @@ -146,10 +146,10 @@ CAN_message_t create_rvc(RvcMessage value) { TireRpmMessage parse_tire_rpm(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::TireRpm) && msg.len == 5); + SAFETY_ASSERT(msg.id == static_cast(MessageId::TireRpm) && msg.len == 5, AssertCode::BadMessage); // Make sure the tire position is one of the valid positions. uint8_t tire_position = msg.buf[0]; - SAFETY_ASSERT(tire_position <= 3); + SAFETY_ASSERT(tire_position <= 3, AssertCode::BadMessage); float value = read_f32_le(&msg.buf[1]); @@ -170,10 +170,10 @@ CAN_message_t create_tire_rpm(TireRpmMessage value) { TireTemperatureMessage parse_tire_temperature(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::TireTemperature) && msg.len == 7); + SAFETY_ASSERT(msg.id == static_cast(MessageId::TireTemperature) && msg.len == 7, AssertCode::BadMessage); /* Make sure the tire position is one of the valid positions. */ uint8_t tire_position = msg.buf[0]; - SAFETY_ASSERT(tire_position <= 3); + SAFETY_ASSERT(tire_position <= 3, AssertCode::BadMessage); TireTemperatureMessage temps; temps.position = static_cast(tire_position); @@ -198,10 +198,10 @@ CAN_message_t create_tire_temperature(TireTemperatureMessage value) { LapMessage parse_lap(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::Lap) && msg.len >= 1); + SAFETY_ASSERT(msg.id == static_cast(MessageId::Lap) && msg.len >= 1, AssertCode::BadMessage); /* Make sure the lap message is one of the valid types. */ uint8_t lap_type = msg.buf[0]; - SAFETY_ASSERT(lap_type < 5); + SAFETY_ASSERT(lap_type < 5, AssertCode::BadMessage); return static_cast(lap_type); } @@ -217,7 +217,7 @@ CAN_message_t create_lap(LapMessage value) { MotorTemperaturesOne parse_motor_temperatures_one(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::TemperaturesOne) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::TemperaturesOne) && msg.len == 8, AssertCode::BadMessage); MotorTemperaturesOne temps; temps.phase_a_temp = read_i16_le(&msg.buf[0]); @@ -231,7 +231,7 @@ MotorTemperaturesOne parse_motor_temperatures_one(CAN_message_t msg) { MotorTemperaturesTwo parse_motor_temperatures_two(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::TemperaturesTwo) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::TemperaturesTwo) && msg.len == 8, AssertCode::BadMessage); MotorTemperaturesTwo temps; temps.control_board_temp = read_i16_le(&msg.buf[0]); @@ -245,7 +245,7 @@ MotorTemperaturesTwo parse_motor_temperatures_two(CAN_message_t msg) { MotorTemperaturesThree parse_motor_temperatures_three(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::TemperaturesThree) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::TemperaturesThree) && msg.len == 8, AssertCode::BadMessage); MotorTemperaturesThree temps; temps.coolant_temp = read_i16_le(&msg.buf[0]); @@ -259,7 +259,7 @@ MotorTemperaturesThree parse_motor_temperatures_three(CAN_message_t msg) { MotorAnalogInputVoltages parse_motor_analog_input_voltages(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::AnalogInputVoltages) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::AnalogInputVoltages) && msg.len == 8, AssertCode::BadMessage); MotorAnalogInputVoltages voltages; voltages.analog_input_one = read_i16_le(&msg.buf[0]); @@ -273,7 +273,7 @@ MotorAnalogInputVoltages parse_motor_analog_input_voltages(CAN_message_t msg) { MotorDigitalInputStatus parse_motor_digital_input_status(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::DigitalInputStatus) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::DigitalInputStatus) && msg.len == 8, AssertCode::BadMessage); MotorDigitalInputStatus status; status.forward_switch = msg.buf[0] != 0; @@ -291,7 +291,7 @@ MotorDigitalInputStatus parse_motor_digital_input_status(CAN_message_t msg) { MotorPositionInfo parse_motor_position_info(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::PositionInfo) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::PositionInfo) && msg.len == 8, AssertCode::BadMessage); MotorPositionInfo info; info.motor_angle = read_i16_le(&msg.buf[0]); @@ -305,7 +305,7 @@ MotorPositionInfo parse_motor_position_info(CAN_message_t msg) { MotorCurrentInfo parse_motor_current_info(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::CurrentInfo) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::CurrentInfo) && msg.len == 8, AssertCode::BadMessage); MotorCurrentInfo info; info.phase_a_current = read_i16_le(&msg.buf[0]); @@ -319,7 +319,7 @@ MotorCurrentInfo parse_motor_current_info(CAN_message_t msg) { MotorVoltageInfo parse_motor_voltage_info(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::VoltageInfo) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::VoltageInfo) && msg.len == 8, AssertCode::BadMessage); MotorVoltageInfo info; info.dc_bus_voltage = read_i16_le(&msg.buf[0]); @@ -333,7 +333,7 @@ MotorVoltageInfo parse_motor_voltage_info(CAN_message_t msg) { MotorFluxInfo parse_motor_flux_info(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::FluxInfo) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::FluxInfo) && msg.len == 8, AssertCode::BadMessage); MotorFluxInfo info; info.flux_command = read_i16_le(&msg.buf[0]); @@ -347,7 +347,7 @@ MotorFluxInfo parse_motor_flux_info(CAN_message_t msg) { MotorInternalVoltages parse_motor_internal_voltages(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::InternalVoltages) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::InternalVoltages) && msg.len == 8, AssertCode::BadMessage); MotorInternalVoltages voltages; voltages.reference_1_5_v = read_i16_le(&msg.buf[0]); @@ -361,18 +361,18 @@ MotorInternalVoltages parse_motor_internal_voltages(CAN_message_t msg) { MotorInternalStates parse_motor_internal_states(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::InternalStates)); + SAFETY_ASSERT(msg.id == static_cast(MessageId::InternalStates), AssertCode::BadMessage); MotorInternalStates result; /* Make sure buf[0] is safe to cast to VsmState. */ - SAFETY_ASSERT(msg.buf[0] <= 7 || (msg.buf[0] >= 14 && msg.buf[0] <= 15)); + SAFETY_ASSERT(msg.buf[0] <= 7 || (msg.buf[0] >= 14 && msg.buf[0] <= 15), AssertCode::BadMessage); result.vsm_state = static_cast(msg.buf[0]); result.pwm_frequency = msg.buf[1]; /* Make sure buf[2] is safe to cast to InverterState. */ - SAFETY_ASSERT(msg.buf[2] <= 12); + SAFETY_ASSERT(msg.buf[2] <= 12, AssertCode::BadMessage); result.inverter_state = static_cast(msg.buf[2]); RelayState relay_state; @@ -397,7 +397,7 @@ MotorInternalStates parse_motor_internal_states(CAN_message_t msg) { /* FIXME this assert may be too strong, as the docs state that * "All other states are reserved for future use." May be better * to provide a default if it's out of range. */ - SAFETY_ASSERT(inverter_active_discharge_state <= 4); + SAFETY_ASSERT(inverter_active_discharge_state <= 4, AssertCode::BadMessage); result.inverter_active_discharge_state = static_cast(inverter_active_discharge_state); @@ -447,7 +447,7 @@ MotorInternalStates parse_motor_internal_states(CAN_message_t msg) { MotorFaultCodes parse_motor_fault_codes(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::FaultCodes) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::FaultCodes) && msg.len == 8, AssertCode::BadMessage); MotorFaultCodes result; @@ -529,7 +529,7 @@ MotorFaultCodes parse_motor_fault_codes(CAN_message_t msg) { MotorTorqueAndTimerInfo parse_motor_torque_and_timer_info(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::TorqueAndTimerInfo) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::TorqueAndTimerInfo) && msg.len == 8, AssertCode::BadMessage); MotorTorqueAndTimerInfo result; result.commanded_torque = read_i16_le(&msg.buf[0]); @@ -542,7 +542,7 @@ MotorTorqueAndTimerInfo parse_motor_torque_and_timer_info(CAN_message_t msg) { MotorModIndexAndFlux parse_motor_mod_index_and_flux(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::ModIndexAndFlux) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::ModIndexAndFlux) && msg.len == 8, AssertCode::BadMessage); MotorModIndexAndFlux result; result.modulation_index = read_i16_le(&msg.buf[0]); @@ -556,7 +556,7 @@ MotorModIndexAndFlux parse_motor_mod_index_and_flux(CAN_message_t msg) { int16_t parse_motor_torque_capability(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::TorqueCapability) && msg.len >= 2); + SAFETY_ASSERT(msg.id == static_cast(MessageId::TorqueCapability) && msg.len >= 2, AssertCode::BadMessage); return read_i16_le(&msg.buf[0]); } @@ -592,7 +592,7 @@ CAN_message_t create_critical_fault_command(CriticalFault value) { MotorControlCommand parse_motor_control_command(CAN_message_t msg) { /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. * We use the cast to extract the CAN message id from its name. */ - SAFETY_ASSERT(msg.id == static_cast(MessageId::ControlCommand) && msg.len == 8); + SAFETY_ASSERT(msg.id == static_cast(MessageId::ControlCommand) && msg.len == 8, AssertCode::BadMessage); MotorControlCommand result; result.torque = read_i16_le(&msg.buf[0]); diff --git a/ECU_V2/lib/common/ecu.cpp b/ECU_V2/lib/common/ecu.cpp index 25d5e7d..4475b77 100644 --- a/ECU_V2/lib/common/ecu.cpp +++ b/ECU_V2/lib/common/ecu.cpp @@ -63,7 +63,7 @@ int16_t Ecu::smooth_torque(uint16_t torque) { int16_t throttle_map(uint16_t throttle1, uint16_t throttle2) { /* Make sure the throttle values are in range. */ - SAFETY_ASSERT(throttle1 >= THROTTLE1_MIN && throttle1 <= THROTTLE1_MAX); + SAFETY_ASSERT(throttle1 >= THROTTLE1_MIN && throttle1 <= THROTTLE1_MAX, AssertCode::ThrottleOutOfRange); // SAFETY_ASSERT(throttle2 >= THROTTLE2_MIN && throttle2 <= THROTTLE2_MAX); int64_t throttle1_percent = map(throttle1, THROTTLE1_MIN, THROTTLE1_MAX, 0, 100); @@ -72,15 +72,15 @@ int16_t throttle_map(uint16_t throttle1, uint16_t throttle2) { // int64_t throttle2_percent = map(throttle2, THROTTLE2_MIN, THROTTLE2_MAX, 0, 100); /* Make sure the two throttle values haven't diverged too far. */ - SAFETY_ASSERT(abs(throttle1_percent - throttle2_percent) < THROTTLE_DISAGREE); + SAFETY_ASSERT(abs(throttle1_percent - throttle2_percent) < THROTTLE_DISAGREE, AssertCode::ThrottleDisagreement); int64_t average = (throttle1_percent + throttle2_percent) / 2; /* Make sure the average can fit into the new size (int16_t). */ - SAFETY_ASSERT(average >= MIN_THROTTLE && average <= MAX_THROTTLE); + SAFETY_ASSERT(average >= MIN_THROTTLE && average <= MAX_THROTTLE, AssertCode::ThrottleOutOfRange); int16_t torque_mapped = map(average, MIN_THROTTLE, MAX_THROTTLE, 0, 100); - SAFETY_ASSERT(torque_mapped >= 0); + SAFETY_ASSERT(torque_mapped >= 0, AssertCode::ThrottleOutOfRange); /* call smooth torque function */ torque_mapped = ecu.smooth_torque(torque_mapped); @@ -120,7 +120,7 @@ void Ecu::processMessage(uint32_t current_time_ms, CAN_message_t msg) { } /* Brake and throttle cannot be pressed at the same time. */ - SAFETY_ASSERT(!(mapped_torque > 0 && this->brake_pressure >= BRAKE_CONSIDERED_PRESSED)); + SAFETY_ASSERT(!(mapped_torque > 0 && this->brake_pressure >= BRAKE_CONSIDERED_PRESSED), AssertCode::ThrottleAndBrakePressed); this->calculated_torque = mapped_torque; } From 87da15a375798b3e2e6fc284e72e0337039ae0e4 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:13:46 -0700 Subject: [PATCH 03/12] Add docs about reverse hash mapping --- ECU_V2/README.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 ECU_V2/README.md diff --git a/ECU_V2/README.md b/ECU_V2/README.md new file mode 100644 index 0000000..b832782 --- /dev/null +++ b/ECU_V2/README.md @@ -0,0 +1,6 @@ +## Generate hash number to file name mapping +(Note: untested) You should be able to generate a reverse mapping table to +recover file names from the reported hash by running +```bash +$ pio test -e native --filter="test_generate_file_hash_mapping" -v +``` \ No newline at end of file From 4b0caff338e809f1721266f1efffa404e296dc31 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:26:20 -0700 Subject: [PATCH 04/12] Get a little more pedantic --- ECU_V2/lib/common/can_serde.cpp | 2 +- ECU_V2/lib/common/ecu.cpp | 2 +- ECU_V2/lib/common/util.cpp | 2 +- ECU_V2/lib/common/util.hpp | 2 ++ ECU_V2/platformio.ini | 2 +- ECU_V2/test/test_can_serde/main.cpp | 13 ++++++++----- ECU_V2/test/test_ecu/main.cpp | 4 ++-- .../test/test_generate_file_hash_mapping/main.cpp | 4 ++-- 8 files changed, 18 insertions(+), 13 deletions(-) diff --git a/ECU_V2/lib/common/can_serde.cpp b/ECU_V2/lib/common/can_serde.cpp index da72746..4830c40 100644 --- a/ECU_V2/lib/common/can_serde.cpp +++ b/ECU_V2/lib/common/can_serde.cpp @@ -11,7 +11,7 @@ /* Guaranteed to generate a message filled with zeroes. */ CAN_message_t empty_can_message(MessageId id, uint8_t len) { - CAN_message_t result = {0}; + CAN_message_t result = {}; /* Why use `static_cast`? You can think of it as a normal cast, but with fewer surprises. */ result.id = static_cast(id); result.len = len; diff --git a/ECU_V2/lib/common/ecu.cpp b/ECU_V2/lib/common/ecu.cpp index 4475b77..9dc50ac 100644 --- a/ECU_V2/lib/common/ecu.cpp +++ b/ECU_V2/lib/common/ecu.cpp @@ -44,7 +44,7 @@ int16_t Ecu::smooth_torque(uint16_t torque) { this->torque_memory[3] = 0; return 0; } else { - uint16_t total_torque; + uint16_t total_torque = 0; /* cycle through last 4 torque values*/ this->torque_memory[3] = this->torque_memory[2]; this->torque_memory[2] = this->torque_memory[1]; diff --git a/ECU_V2/lib/common/util.cpp b/ECU_V2/lib/common/util.cpp index be21bc0..6c92a5d 100644 --- a/ECU_V2/lib/common/util.cpp +++ b/ECU_V2/lib/common/util.cpp @@ -38,7 +38,7 @@ uint32_t str_hash(const char *str) { uint32_t hash = 5381; uint32_t c; - while (c = *str++) + while ((c = *str++)) hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ return hash; diff --git a/ECU_V2/lib/common/util.hpp b/ECU_V2/lib/common/util.hpp index c210e3b..f78cf9b 100644 --- a/ECU_V2/lib/common/util.hpp +++ b/ECU_V2/lib/common/util.hpp @@ -24,4 +24,6 @@ class Trigger { #define PRINTF(...) Serial.printf(__VA_ARGS__) #endif +#define UNUSED(x) ((void)x) + uint32_t str_hash(const char *str); diff --git a/ECU_V2/platformio.ini b/ECU_V2/platformio.ini index 92d2303..b0a8343 100644 --- a/ECU_V2/platformio.ini +++ b/ECU_V2/platformio.ini @@ -21,4 +21,4 @@ platform = native test_framework = unity lib_compat_mode = off # If we're building it for a native target (e.g. local computer), it means we're building it to test it. -build_flags = -DBUILD_MODE_TEST -DENABLE_DEBUGGING -g3 -Wall +build_flags = -DBUILD_MODE_TEST -DENABLE_DEBUGGING -fsanitize=address -fsanitize=undefined -g3 -Wall -Wextra -Wpedantic diff --git a/ECU_V2/test/test_can_serde/main.cpp b/ECU_V2/test/test_can_serde/main.cpp index 6f02d9a..fb0e17e 100644 --- a/ECU_V2/test/test_can_serde/main.cpp +++ b/ECU_V2/test/test_can_serde/main.cpp @@ -8,6 +8,7 @@ #include "assert.hpp" #include "can_serde.hpp" +#include "util.hpp" void setUp(void) { // set stuff up here @@ -17,11 +18,13 @@ void tearDown(void) { // clean stuff up here } -void safety_assert_failed_handler(AssertLevel level, const char *file, int line, AssertCode error_code) { +void assert_failed_handler(AssertLevel level, const char *file, int line, AssertCode error_code) { + UNUSED(level); + /* If an SAFETY_ASSERT fails somewhere in the code, this will let the testing environment * know that we failed the test. */ std::stringstream fail_msg; - fail_msg << "Assert failed at " << file << ":" << line; + fail_msg << "Assert failed at " << file << ":" << line << " with code " << static_cast(error_code); TEST_FAIL_MESSAGE(fail_msg.str().c_str()); } @@ -64,7 +67,7 @@ void test_can_dump_parsing() { iss >> msg_num >> time_offset >> type >> id_hex >> direction >> len; /* Construct the message that we'll be using. */ - CAN_message_t msg = {0}; + CAN_message_t msg = {}; msg.id = std::stoul(id_hex, nullptr, 16); /* Hex -> number. */ msg.len = len; @@ -189,8 +192,8 @@ void test_inverter_creation_message() { TEST_ASSERT(created.buf[7] == 0x00); } -int main(int argc, char **argv) { - register_assert_failed_handler(safety_assert_failed_handler); +int main() { + register_assert_failed_handler(assert_failed_handler); UNITY_BEGIN(); RUN_TEST(test_start_switch); diff --git a/ECU_V2/test/test_ecu/main.cpp b/ECU_V2/test/test_ecu/main.cpp index 70c6ae7..d25ac7e 100644 --- a/ECU_V2/test/test_ecu/main.cpp +++ b/ECU_V2/test/test_ecu/main.cpp @@ -23,7 +23,7 @@ void safety_assert_failed_handler(AssertLevel level, const char *file, int line, /* If an SAFETY_ASSERT fails somewhere in the code, this will let the testing environment * know that we failed the test. */ std::stringstream fail_msg; - fail_msg << "Assert failed at " << file << ":" << line; + fail_msg << "Assert failed at " << file << ":" << line << " with code " << static_cast(error_code); TEST_FAIL_MESSAGE(fail_msg.str().c_str()); } } @@ -114,7 +114,7 @@ void test_ecu() { TEST_ASSERT(ecu.emitMessage(current_time_ms) == std::nullopt); } -int main(int argc, char **argv) { +int main(int _argc, char **_argv) { register_assert_failed_handler(safety_assert_failed_handler); UNITY_BEGIN(); diff --git a/ECU_V2/test/test_generate_file_hash_mapping/main.cpp b/ECU_V2/test/test_generate_file_hash_mapping/main.cpp index 1ed3910..81e588e 100644 --- a/ECU_V2/test/test_generate_file_hash_mapping/main.cpp +++ b/ECU_V2/test/test_generate_file_hash_mapping/main.cpp @@ -15,12 +15,12 @@ void setUp(void) {} void tearDown(void) {} -int main(int argc, char **argv) { +int main() { std::vector target_dirs = {"lib", "src"}; std::stringstream csv_entries; for (const auto& target_dir : target_dirs) { - for (const auto& dir_entry : fs::recursive_directory_iterator("lib")) { + for (const auto& dir_entry : fs::recursive_directory_iterator(target_dir)) { if (!fs::is_regular_file(dir_entry)) continue; auto path = dir_entry.path(); From cfdbd1985d6fc88d571ba0c731ba630a5a38e3e8 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:33:42 -0700 Subject: [PATCH 05/12] Fix some pedantic warnings --- ECU_V2/test/test_ecu/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ECU_V2/test/test_ecu/main.cpp b/ECU_V2/test/test_ecu/main.cpp index d25ac7e..c4269da 100644 --- a/ECU_V2/test/test_ecu/main.cpp +++ b/ECU_V2/test/test_ecu/main.cpp @@ -17,6 +17,8 @@ void tearDown(void) {} volatile bool expecting_safety_violation = false; volatile bool hit_safety_violation = false; void safety_assert_failed_handler(AssertLevel level, const char *file, int line, AssertCode error_code) { + UNUSED(level); + if (expecting_safety_violation) { hit_safety_violation = true; } else { @@ -114,7 +116,7 @@ void test_ecu() { TEST_ASSERT(ecu.emitMessage(current_time_ms) == std::nullopt); } -int main(int _argc, char **_argv) { +int main() { register_assert_failed_handler(safety_assert_failed_handler); UNITY_BEGIN(); From 79719443d57624d8fde4ca498a50f1aa8a410f83 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:33:54 -0700 Subject: [PATCH 06/12] Separate debug from production builds --- ECU_V2/platformio.ini | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ECU_V2/platformio.ini b/ECU_V2/platformio.ini index b0a8343..a3cefc8 100644 --- a/ECU_V2/platformio.ini +++ b/ECU_V2/platformio.ini @@ -8,15 +8,25 @@ ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html -[env:teensy41] +[env:teensy41_debug] platform = teensy +build_type = debug +build_flags = -DLOG_FILE_PATH="logs/log_${date}_${time}.log" -DENABLE_DEBUGGING +board = teensy41 +framework = arduino +monitor_filters = time, log2file +lib_deps = https://github.com/tonton81/FlexCAN_T4 + +[env:teensy41_production] +platform = teensy +build_type = release +build_flags = -DLOG_FILE_PATH="logs/log_${date}_${time}.log" board = teensy41 framework = arduino monitor_filters = time, log2file -build_flags = -DLOG_FILE_PATH="logs/log_${date}_${time}.log" -DENABLE_DEBUGGING lib_deps = https://github.com/tonton81/FlexCAN_T4 -[env:native] +[env:native_tests] platform = native test_framework = unity lib_compat_mode = off From 741b23a473a77fe92709a75f7e72c5024672ce70 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Thu, 5 Feb 2026 22:43:20 -0700 Subject: [PATCH 07/12] Now with 65% more pedantic per pedantic --- ECU_V2/platformio.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ECU_V2/platformio.ini b/ECU_V2/platformio.ini index a3cefc8..d1d16bc 100644 --- a/ECU_V2/platformio.ini +++ b/ECU_V2/platformio.ini @@ -11,7 +11,7 @@ [env:teensy41_debug] platform = teensy build_type = debug -build_flags = -DLOG_FILE_PATH="logs/log_${date}_${time}.log" -DENABLE_DEBUGGING +build_flags = -DLOG_FILE_PATH="logs/log_${date}_${time}.log" -DENABLE_DEBUGGING -Wall -Wunused-parameter board = teensy41 framework = arduino monitor_filters = time, log2file @@ -20,7 +20,7 @@ lib_deps = https://github.com/tonton81/FlexCAN_T4 [env:teensy41_production] platform = teensy build_type = release -build_flags = -DLOG_FILE_PATH="logs/log_${date}_${time}.log" +build_flags = -DLOG_FILE_PATH="logs/log_${date}_${time}.log" -Wall -Wunused-parameter board = teensy41 framework = arduino monitor_filters = time, log2file From d9c10d1e839430a6995190f05dbf4b93f45291a0 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Sat, 7 Feb 2026 08:33:18 -0700 Subject: [PATCH 08/12] Workaround for unstable switch --- ECU_V2/lib/common/ecu.cpp | 29 +++++++++++++++++++++++++++-- ECU_V2/lib/common/ecu.hpp | 4 ++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/ECU_V2/lib/common/ecu.cpp b/ECU_V2/lib/common/ecu.cpp index 9dc50ac..9ee4457 100644 --- a/ECU_V2/lib/common/ecu.cpp +++ b/ECU_V2/lib/common/ecu.cpp @@ -125,6 +125,31 @@ void Ecu::processMessage(uint32_t current_time_ms, CAN_message_t msg) { this->calculated_torque = mapped_torque; } + bool debounced_switch; + + /* FIXME hack working around the non-debounced switch. */ + if (this->last_start_switch_value == this->start_switch_on) { + /* Nothing to do, as the last value is the same as the current value. */ + debounced_switch = this->start_switch_on; + this->turn_off_timeout.cancel(); + } else if (this->start_switch_on) { + debounced_switch = true; + this->last_start_switch_value = true; + this->turn_off_timeout.cancel(); + } else { + /* The switch was turned off, but we need to wait a second before + * considering it switched off. */ + + if (!this->turn_off_timeout.started()) { + /* If we haven't started the timer yet, go ahead and start it. */ + this->turn_off_timeout.start(current_time_ms, 1000); + } else if (this->turn_off_timeout.triggerReached(current_time_ms)) { + /* It's been long enough to consider it off. */ + debounced_switch = false; + this->last_start_switch_value = false; + } + } + /* Car startup sequence. */ if (this->car_fully_on) { /* No need to do the motor startup sequence if the car is already fully started. */ @@ -135,7 +160,7 @@ void Ecu::processMessage(uint32_t current_time_ms, CAN_message_t msg) { * Once these preconditions are met, we can do the startup sequence. * We will also wait two seconds before fully starting up, or abort if * one of the preconditions stops holding. */ - if ((this->brake_pressure >= BRAKE_CONSIDERED_PRESSED) && this->start_switch_on) { + if ((this->brake_pressure >= BRAKE_CONSIDERED_PRESSED) && debounced_switch) { if (!this->startup_countdown.started()) { this->startup_countdown.start(current_time_ms, STARTUP_DELAY_MS); } else if (this->startup_countdown.triggerReached(current_time_ms)) { @@ -148,7 +173,7 @@ void Ecu::processMessage(uint32_t current_time_ms, CAN_message_t msg) { } } - if (!this->start_switch_on) { + if (!debounced_switch) { /* Pretty self-explanatory: if the start switch turns off, the car turns off. */ this->car_fully_on = false; } diff --git a/ECU_V2/lib/common/ecu.hpp b/ECU_V2/lib/common/ecu.hpp index 0d442b0..0993b93 100644 --- a/ECU_V2/lib/common/ecu.hpp +++ b/ECU_V2/lib/common/ecu.hpp @@ -44,6 +44,10 @@ class Ecu { /* Whether the car is switched on or not. */ bool start_switch_on = false; + /* FIXME this is a hack to work around the start switch sensor. */ + bool last_start_switch_value = false; + Trigger turn_off_timeout; + /* How far the brake is pressed down. */ uint16_t brake_pressure = BRAKE_PRESSURE_MIN; From 29d20f2e1fec3ecb5c2c9bd9cfd6182b66b355b0 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Sat, 7 Feb 2026 09:11:43 -0700 Subject: [PATCH 09/12] Add debouncing fixes --- ECU_V2/lib/common/ecu.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ECU_V2/lib/common/ecu.cpp b/ECU_V2/lib/common/ecu.cpp index 9ee4457..1bdddef 100644 --- a/ECU_V2/lib/common/ecu.cpp +++ b/ECU_V2/lib/common/ecu.cpp @@ -67,8 +67,8 @@ int16_t throttle_map(uint16_t throttle1, uint16_t throttle2) { // SAFETY_ASSERT(throttle2 >= THROTTLE2_MIN && throttle2 <= THROTTLE2_MAX); int64_t throttle1_percent = map(throttle1, THROTTLE1_MIN, THROTTLE1_MAX, 0, 100); - /* DEBUG ONLY!!!! Throttle 2 set to throttle 1 :) */ - int64_t throttle2_percent = map(throttle1, THROTTLE1_MIN, THROTTLE1_MAX, 0, 100); + /* FIXME Throttle 2 set to throttle 1 :) */ + int64_t throttle2_percent = throttle1_percent; // int64_t throttle2_percent = map(throttle2, THROTTLE2_MIN, THROTTLE2_MAX, 0, 100); /* Make sure the two throttle values haven't diverged too far. */ @@ -143,10 +143,15 @@ void Ecu::processMessage(uint32_t current_time_ms, CAN_message_t msg) { if (!this->turn_off_timeout.started()) { /* If we haven't started the timer yet, go ahead and start it. */ this->turn_off_timeout.start(current_time_ms, 1000); + /* Keep the last value while we wait. */ + debounced_switch = true; } else if (this->turn_off_timeout.triggerReached(current_time_ms)) { /* It's been long enough to consider it off. */ debounced_switch = false; this->last_start_switch_value = false; + } else { + /* Timer is running but hasn't reached yet - keep the old value. */ + debounced_switch = true; } } @@ -225,6 +230,7 @@ void Ecu::printState() { PRINTF("=== ECU State ===\n"); PRINTF("car_fully_on: %s\n", this->car_fully_on ? "true" : "false"); PRINTF("start_switch_on: %s\n", this->start_switch_on ? "true" : "false"); + PRINTF("last_start_switch_value: %s\n", this->last_start_switch_value ? "true" : "false"); PRINTF("brake_pressure: %u\n", this->brake_pressure); PRINTF("calculated_torque: %d\n", this->calculated_torque); From a16a1515321b86c58afe46dcc398a450b4ba664a Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:41:27 -0700 Subject: [PATCH 10/12] Get test working for switch debouncing and fix motor message pacing --- ECU_V2/lib/common/ecu.cpp | 11 +---- ECU_V2/lib/common/ecu.hpp | 4 +- ECU_V2/lib/common/util.cpp | 27 +++++++++++++ ECU_V2/lib/common/util.hpp | 10 +++++ ECU_V2/test/test_ecu/main.cpp | 76 +++++++++++++++++++++++++++++++++-- 5 files changed, 114 insertions(+), 14 deletions(-) diff --git a/ECU_V2/lib/common/ecu.cpp b/ECU_V2/lib/common/ecu.cpp index 1bdddef..133abe4 100644 --- a/ECU_V2/lib/common/ecu.cpp +++ b/ECU_V2/lib/common/ecu.cpp @@ -202,14 +202,8 @@ std::optional Ecu::emitMessage(uint32_t current_time_ms) { torque_to_use = this->calculated_torque; } - /* We need to pace how often motor control messages are passed along the CAN bus. - * We do this by using a trigger. To use the trigger, we first start it. Once - * enough time has passed since we started it, `triggerReached` will return true, - * and mark the trigger as not started. Next time, we'll see that the trigger is - * marked as not started, and we'll start it again. This loops forever. */ - if (!this->motor_control_message_pacing.started()) { - this->motor_control_message_pacing.start(current_time_ms, 15/*ms*/); - } else if (this->motor_control_message_pacing.triggerReached(current_time_ms)) { + /* Pace how often we send a motor command by pacing it with a timer. */ + if (this->motor_control_pacing.shouldFire(current_time_ms)) { MotorControlCommand cmd; cmd.torque = torque_to_use; cmd.speed = 0; @@ -247,5 +241,4 @@ void Ecu::printState() { } PRINTF("startup_countdown: %s\n", this->startup_countdown.started() ? "started" : "not started"); - PRINTF("motor_control_message_pacing: %s\n", this->motor_control_message_pacing.started() ? "started" : "not started"); } diff --git a/ECU_V2/lib/common/ecu.hpp b/ECU_V2/lib/common/ecu.hpp index 0993b93..d22a999 100644 --- a/ECU_V2/lib/common/ecu.hpp +++ b/ECU_V2/lib/common/ecu.hpp @@ -58,8 +58,8 @@ class Ecu { * wait 2 seconds before enabling the motor. This is that startup countdown. */ Trigger startup_countdown; - /* Time since we last sent a motor control command. */ - Trigger motor_control_message_pacing; + /* Timer to pace how often we send motor control commands. */ + Timer motor_control_pacing = Timer(0, 15); /* throttle mapping memory */ uint16_t torque_memory[4] = {0, 0, 0, 0}; diff --git a/ECU_V2/lib/common/util.cpp b/ECU_V2/lib/common/util.cpp index 6c92a5d..07be624 100644 --- a/ECU_V2/lib/common/util.cpp +++ b/ECU_V2/lib/common/util.cpp @@ -31,6 +31,33 @@ void Trigger::cancel() { started_at = std::nullopt; } + +/* Timer */ +Timer::Timer(uint32_t current_time, uint32_t duration_ms) { + this->last_fired_at = current_time; + this->duration_ms = duration_ms; +} + +/* Important: this will reset the timer when this returns true. */ +bool Timer::shouldFire(uint32_t current_time) { + if (current_time - this->last_fired_at >= this->duration_ms) { + this->last_fired_at = current_time; + return true; + } else { + return false; + } +} + +/* Tells us how long until this timer will fire again, or std::nullopt if + * it's due to fire. */ +std::optional Timer::timeUntilNextFiring(uint32_t current_time_ms) { + if (current_time_ms - this->last_fired_at < this->duration_ms) { + return this->duration_ms - (current_time_ms - this->last_fired_at); + } else { + return std::nullopt; + } +} + /* Used to condense the file name into something short enough we can send * along the CAN bus in the case of a fault. */ uint32_t str_hash(const char *str) { diff --git a/ECU_V2/lib/common/util.hpp b/ECU_V2/lib/common/util.hpp index f78cf9b..229a084 100644 --- a/ECU_V2/lib/common/util.hpp +++ b/ECU_V2/lib/common/util.hpp @@ -16,6 +16,16 @@ class Trigger { uint32_t target_duration = 0; }; +class Timer { +public: + Timer(uint32_t current_time, uint32_t frequency); + bool shouldFire(uint32_t current_time); + std::optional timeUntilNextFiring(uint32_t current_time_ms); +private: + uint32_t last_fired_at = 0; + uint32_t duration_ms = 0; +}; + #ifdef BUILD_MODE_TEST #include #define PRINTF(...) printf(__VA_ARGS__) diff --git a/ECU_V2/test/test_ecu/main.cpp b/ECU_V2/test/test_ecu/main.cpp index c4269da..36221fc 100644 --- a/ECU_V2/test/test_ecu/main.cpp +++ b/ECU_V2/test/test_ecu/main.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -53,7 +54,7 @@ std::tuple ecu_after_startup_sequence() { /* 2 seconds later... */ current_time_ms += 2000; /* Now we should be generating a motor command. */ - /* forward vv vv enabled */ + /* forward vv vv enabled */ uint8_t enabled_but_no_torque[] = {0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00}; test_can_msg_eql( ecu.emitMessage(current_time_ms).value(), /* `.value()` will throw if std::nullopt. */ @@ -103,7 +104,7 @@ void test_ecu() { ecu = std::get<0>(new_ecu); current_time_ms = std::get<1>(new_ecu); - /* Lift the brake this time, before puttind down the throttle. */ + /* Lift the brake this time, before putting down the throttle. */ ecu.processMessage(current_time_ms, create_brake_pressure(BRAKE_PRESSURE_MIN)); ecu.processMessage(current_time_ms, create_throttle_one_position(50)); ecu.processMessage(current_time_ms, create_throttle_two_position(22)); @@ -116,10 +117,79 @@ void test_ecu() { TEST_ASSERT(ecu.emitMessage(current_time_ms) == std::nullopt); } +void test_switch_debounce() { + /* This test verifies the switch debouncing logic when turning off. */ + std::tuple new_ecu = ecu_after_startup_sequence(); + Ecu ecu = std::get<0>(new_ecu); + uint32_t current_time_ms = std::get<1>(new_ecu); + + ecu.processMessage(current_time_ms, create_brake_pressure(80)); + + current_time_ms += 10; + /* Lift the brake so the car is fully operational. */ + ecu.processMessage(current_time_ms, create_brake_pressure(BRAKE_PRESSURE_MIN)); + current_time_ms += 20; + + /* At this point, the car should be on and the switch should be on. */ + /* Now turn off the switch. */ + ecu.processMessage(current_time_ms, create_start_switch(false)); + + /* The car should still be on immediately after turning off the switch, + * because of the 1000ms debounce delay. */ + current_time_ms += 20; + std::optional msg = ecu.emitMessage(current_time_ms); + TEST_ASSERT(msg != std::nullopt); + /* Verify the inverter is still enabled. */ + TEST_ASSERT((msg.value().buf[5] & 0x01) == 0x01); /* enable_inverter should be true */ + + /* Wait 500ms (halfway through the debounce period). */ + current_time_ms += 500; + msg = ecu.emitMessage(current_time_ms); + TEST_ASSERT(msg != std::nullopt); + /* Car should still be on. */ + TEST_ASSERT((msg.value().buf[5] & 0x01) == 0x01); + + /* Now turn the switch back on before the debounce completes. */ + ecu.processMessage(current_time_ms, create_start_switch(true)); + current_time_ms += 20; + + /* The car should remain on since we canceled the debounce. */ + msg = ecu.emitMessage(current_time_ms); + TEST_ASSERT(msg != std::nullopt); + TEST_ASSERT((msg.value().buf[5] & 0x01) == 0x01); + + /* Wait another 1000ms to make sure it stays on. */ + current_time_ms += 1000; + msg = ecu.emitMessage(current_time_ms); + TEST_ASSERT(msg != std::nullopt); + TEST_ASSERT((msg.value().buf[5] & 0x01) == 0x01); + + /* Now turn the switch off again and let the full debounce period elapse. */ + ecu.processMessage(current_time_ms, create_start_switch(false)); + current_time_ms += 20; + + /* Car should still be on. */ + msg = ecu.emitMessage(current_time_ms); + TEST_ASSERT(msg != std::nullopt); + TEST_ASSERT((msg.value().buf[5] & 0x01) == 0x01); + + /* Wait for the full 1000ms debounce period to complete. */ + current_time_ms += 1000; + ecu.processMessage(current_time_ms, create_start_switch(false)); + + /* Now the car should be off. */ + current_time_ms += 20; + msg = ecu.emitMessage(current_time_ms); + TEST_ASSERT(msg != std::nullopt); + /* Verify the inverter is now disabled. */ + TEST_ASSERT((msg.value().buf[5] & 0x01) == 0x00); /* enable_inverter should be false */ +} + int main() { register_assert_failed_handler(safety_assert_failed_handler); UNITY_BEGIN(); - RUN_TEST(test_ecu); + // RUN_TEST(test_ecu); + RUN_TEST(test_switch_debounce); UNITY_END(); } From 2e568caa01c07bed91e9ac09c35e0535b30b1a08 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:07:19 -0700 Subject: [PATCH 11/12] Fix code doubling after merge --- ECU_V2/lib/common/ecu.cpp | 40 --------------------------------------- 1 file changed, 40 deletions(-) diff --git a/ECU_V2/lib/common/ecu.cpp b/ECU_V2/lib/common/ecu.cpp index 35f3e93..756e784 100644 --- a/ECU_V2/lib/common/ecu.cpp +++ b/ECU_V2/lib/common/ecu.cpp @@ -155,46 +155,6 @@ void Ecu::processMessage(uint32_t current_time_ms, CAN_message_t msg) { bool debounced_switch; - /* FIXME hack working around the non-debounced switch. */ - if (this->last_start_switch_value == this->start_switch_on) - { - /* Nothing to do, as the last value is the same as the current value. */ - debounced_switch = this->start_switch_on; - this->turn_off_timeout.cancel(); - } - else if (this->start_switch_on) - { - debounced_switch = true; - this->last_start_switch_value = true; - this->turn_off_timeout.cancel(); - } - else - { - /* The switch was turned off, but we need to wait a second before - * considering it switched off. */ - - if (!this->turn_off_timeout.started()) - { - /* If we haven't started the timer yet, go ahead and start it. */ - this->turn_off_timeout.start(current_time_ms, 1000); - /* Keep the last value while we wait. */ - debounced_switch = true; - } - else if (this->turn_off_timeout.triggerReached(current_time_ms)) - { - /* It's been long enough to consider it off. */ - debounced_switch = false; - this->last_start_switch_value = false; - } - else - { - /* Timer is running but hasn't reached yet - keep the old value. */ - debounced_switch = true; - } - } - - bool debounced_switch; - /* FIXME hack working around the non-debounced switch. */ if (this->last_start_switch_value == this->start_switch_on) { /* Nothing to do, as the last value is the same as the current value. */ From fcf1ad5d5645e0919727213daecf2c808c255116 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:21:38 -0700 Subject: [PATCH 12/12] Minor fixes in smooth torque, and format ecu.cpp --- ECU_V2/lib/common/ecu.cpp | 86 +++++++++++++++++++++++++-------------- ECU_V2/lib/common/ecu.hpp | 2 +- 2 files changed, 57 insertions(+), 31 deletions(-) diff --git a/ECU_V2/lib/common/ecu.cpp b/ECU_V2/lib/common/ecu.cpp index 756e784..cf1d4d7 100644 --- a/ECU_V2/lib/common/ecu.cpp +++ b/ECU_V2/lib/common/ecu.cpp @@ -6,9 +6,10 @@ #include "can_serde.hpp" #include "constants.hpp" - -int64_t map(int32_t input_32, int32_t old_min_32, int32_t old_max_32, int32_t new_min_32, int32_t new_max_32) { - if (old_min_32 == old_max_32 || new_min_32 == new_max_32) { +int64_t map(int32_t input_32, int32_t old_min_32, int32_t old_max_32, int32_t new_min_32, int32_t new_max_32) +{ + if (old_min_32 == old_max_32 || new_min_32 == new_max_32) + { /* Avoid division by zero. */ return 0; } @@ -32,18 +33,24 @@ int64_t map(int32_t input_32, int32_t old_min_32, int32_t old_max_32, int32_t ne return output_shifted; } -int16_t Ecu::smoothTorque(uint16_t current_time_ms, uint16_t torque) { - /* smooth out throttle values by averaging out previous four values */ +int16_t Ecu::smoothTorque(uint32_t current_time_ms, int16_t torque) +{ + /* Smooth out throttle values by averaging out previous four values. */ + + static_assert(sizeof(this->torque_memory) / sizeof(this->torque_memory[0]) == 4); /* If recieves a value of 0 return 0 and set all values to 0 ;) */ - if (torque == 0) { - this->torque_memory[0] = 0; - this->torque_memory[1] = 0; - this->torque_memory[2] = 0; - this->torque_memory[3] = 0; + if (torque == 0) + { + for (size_t i = 0; i < 4; i++) + { + this->torque_memory[i] = 0; + } return 0; - } else { - uint16_t total_torque = 0; + } + else + { + int16_t total_torque = 0; /* cycle through last 4 torque values*/ this->torque_memory[3] = this->torque_memory[2]; this->torque_memory[2] = this->torque_memory[1]; @@ -51,7 +58,8 @@ int16_t Ecu::smoothTorque(uint16_t current_time_ms, uint16_t torque) { this->torque_memory[0] = torque; /* sum last 4 torque values */ - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 4; i++) + { total_torque += this->torque_memory[i]; } this->last_output_torque = 0; @@ -59,14 +67,14 @@ int16_t Ecu::smoothTorque(uint16_t current_time_ms, uint16_t torque) { } /* Continues to send same torque message until 20ms has passed */ - if ((uint16_t)(current_time_ms - last_smooth_update_ms) < SMOOTH_PERIOD_MS) + if (current_time_ms - last_smooth_update_ms < SMOOTH_PERIOD_MS) { return this->last_output_torque; } /* Starts timer for next cycle when 20ms has passed */ this->last_smooth_update_ms = current_time_ms; - uint16_t total_torque = 0; + int16_t total_torque = 0; /* cycle through last 4 torque values*/ this->torque_memory[3] = this->torque_memory[2]; this->torque_memory[2] = this->torque_memory[1]; @@ -74,7 +82,8 @@ int16_t Ecu::smoothTorque(uint16_t current_time_ms, uint16_t torque) { this->torque_memory[0] = torque; /* sum last 4 torque values */ - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 4; i++) + { total_torque += this->torque_memory[i]; } @@ -85,8 +94,8 @@ int16_t Ecu::smoothTorque(uint16_t current_time_ms, uint16_t torque) { return total_torque; } - -int16_t throttle_map(uint16_t throttle1, uint16_t throttle2) { +int16_t throttle_map(uint16_t throttle1, uint16_t throttle2) +{ /* Make sure the throttle values are in range. */ SAFETY_ASSERT((throttle1 >= THROTTLE1_MIN && throttle1 <= THROTTLE1_MAX), AssertCode::ThrottleOutOfRange); @@ -112,7 +121,8 @@ int16_t throttle_map(uint16_t throttle1, uint16_t throttle2) { return static_cast(torque_mapped); } -void Ecu::processMessage(uint32_t current_time_ms, CAN_message_t msg) { +void Ecu::processMessage(uint32_t current_time_ms, CAN_message_t msg) +{ /* If we received a CAN message, update the corresponding value. */ switch (static_cast(msg.id)) { @@ -156,28 +166,38 @@ void Ecu::processMessage(uint32_t current_time_ms, CAN_message_t msg) { bool debounced_switch; /* FIXME hack working around the non-debounced switch. */ - if (this->last_start_switch_value == this->start_switch_on) { + if (this->last_start_switch_value == this->start_switch_on) + { /* Nothing to do, as the last value is the same as the current value. */ debounced_switch = this->start_switch_on; this->turn_off_timeout.cancel(); - } else if (this->start_switch_on) { + } + else if (this->start_switch_on) + { debounced_switch = true; this->last_start_switch_value = true; this->turn_off_timeout.cancel(); - } else { + } + else + { /* The switch was turned off, but we need to wait a second before * considering it switched off. */ - if (!this->turn_off_timeout.started()) { + if (!this->turn_off_timeout.started()) + { /* If we haven't started the timer yet, go ahead and start it. */ this->turn_off_timeout.start(current_time_ms, 1000); /* Keep the last value while we wait. */ debounced_switch = true; - } else if (this->turn_off_timeout.triggerReached(current_time_ms)) { + } + else if (this->turn_off_timeout.triggerReached(current_time_ms)) + { /* It's been long enough to consider it off. */ debounced_switch = false; this->last_start_switch_value = false; - } else { + } + else + { /* Timer is running but hasn't reached yet - keep the old value. */ debounced_switch = true; } @@ -196,10 +216,14 @@ void Ecu::processMessage(uint32_t current_time_ms, CAN_message_t msg) { * Once these preconditions are met, we can do the startup sequence. * We will also wait two seconds before fully starting up, or abort if * one of the preconditions stops holding. */ - if ((this->brake_pressure >= BRAKE_CONSIDERED_PRESSED) && debounced_switch) { - if (!this->startup_countdown.started()) { + if ((this->brake_pressure >= BRAKE_CONSIDERED_PRESSED) && debounced_switch) + { + if (!this->startup_countdown.started()) + { this->startup_countdown.start(current_time_ms, STARTUP_DELAY_MS); - } else if (this->startup_countdown.triggerReached(current_time_ms)) { + } + else if (this->startup_countdown.triggerReached(current_time_ms)) + { /* Good to go! */ this->car_fully_on = true; } @@ -211,7 +235,8 @@ void Ecu::processMessage(uint32_t current_time_ms, CAN_message_t msg) { } } - if (!debounced_switch) { + if (!debounced_switch) + { /* Pretty self-explanatory: if the start switch turns off, the car turns off. */ this->car_fully_on = false; } @@ -239,7 +264,8 @@ std::optional Ecu::emitMessage(uint32_t current_time_ms) } /* Pace how often we send a motor command by pacing it with a timer. */ - if (this->motor_control_pacing.shouldFire(current_time_ms)) { + if (this->motor_control_pacing.shouldFire(current_time_ms)) + { MotorControlCommand cmd; cmd.torque = torque_to_use; cmd.speed = 0; diff --git a/ECU_V2/lib/common/ecu.hpp b/ECU_V2/lib/common/ecu.hpp index 725a542..5fa86e8 100644 --- a/ECU_V2/lib/common/ecu.hpp +++ b/ECU_V2/lib/common/ecu.hpp @@ -17,7 +17,7 @@ class Ecu { std::optional emitMessage(uint32_t current_time_ms); /* smooth torque */ - int16_t smoothTorque(uint16_t current_time, uint16_t torque); + int16_t smoothTorque(uint32_t current_time, int16_t torque); /* Uses PRINTF for all printing. */ void printState();