diff --git a/fsae-raspi/Cargo.toml b/fsae-raspi/Cargo.toml index 9d83343..870aed6 100644 --- a/fsae-raspi/Cargo.toml +++ b/fsae-raspi/Cargo.toml @@ -15,3 +15,7 @@ config = "0.15" rumqttc = "0.25" rumqttd = "0.20" tokio-socketcan-isotp = "0.2.0" + +[[bin]] +name = "test" +path = "src/test.rs" \ No newline at end of file diff --git a/fsae-raspi/src/can.rs b/fsae-raspi/src/can.rs index d0a8ed9..370547c 100644 --- a/fsae-raspi/src/can.rs +++ b/fsae-raspi/src/can.rs @@ -1,151 +1,151 @@ -use std::time::Duration; - -use crate::send::{send_message, Reading}; -// use rand::Rng; -use serde::Serialize; -use tokio::time::sleep; -use tokio_socketcan_isotp::{IsoTpSocket, StandardId}; - -const CAN_INTERFACE: &str = "can0"; - -macro_rules! define_enum { - ($name:ident, $($variant:ident = $value:expr),*) => { - #[derive(Serialize)] - #[allow(clippy::enum_variant_names)] - enum $name { - $($variant = $value),* - } - - impl $name { - fn from_byte(byte: u8) -> Self { - match byte { - $($value => Self::$variant),*, - _ => panic!("Invalid value for {}", stringify!($name)), - } - } - } - }; -} - -define_enum!( - MotorState, - MotorStateOff = 0, - MotorStatePrecharging = 1, - MotorStateIdle = 2, - MotorStateDriving = 3, - MotorStateFault = 4 -); - -define_enum!( - MotorRotateDirection, - DirectionStandby = 0, - DirectionForward = 1, - DirectionBackward = 2, - DirectionError = 3 -); - -define_enum!( - MCUMainState, - StateStandby = 0, - StatePrecharge = 1, - StatePowerReady = 2, - StateRun = 3, - StatePowerOff = 4 -); - -define_enum!( - MCUWorkMode, - WorkModeStandby = 0, - WorkModeTorque = 1, - WorkModeSpeed = 2 -); - -define_enum!( - MCUWarningLevel, - ErrorNone = 0, - ErrorLow = 1, - ErrorMedium = 2, - ErrorHigh = 3 -); - -#[derive(Serialize)] -struct TelemetryData { - apps_travel: f32, - motor_speed: f32, - motor_torque: f32, - max_motor_torque: f32, - motor_direction: MotorRotateDirection, - motor_state: MotorState, - mcu_main_state: MCUMainState, - mcu_work_mode: MCUWorkMode, - mcu_voltage: f32, - mcu_current: f32, - motor_temp: i32, - mcu_temp: i32, - dc_main_wire_over_volt_fault: bool, - dc_main_wire_over_curr_fault: bool, - motor_over_spd_fault: bool, - motor_phase_curr_fault: bool, - motor_stall_fault: bool, - mcu_warning_level: MCUWarningLevel, - debug_0: f32, - debug_1: f32, - debug_2: f32, - debug_3: f32, -} - -impl Reading for TelemetryData { - fn topic() -> &'static str { - "telemetry" - } -} - -fn parse_bool(byte: u8) -> bool { - byte != 0 -} - -pub async fn read_can() { - loop { - let Ok(socket) = IsoTpSocket::open( - CAN_INTERFACE, - StandardId::new(0x666).expect("Invalid src id"), - StandardId::new(0x777).expect("Invalid src id"), - ) else { - println!("Failed to open socket"); - sleep(Duration::from_secs(1)).await; - continue; - }; - - while let Ok(packet) = socket.read_packet().await { - if packet.len() != 58 { - println!("Invalid packet length {}: {:?}", packet.len(), packet); - continue; - } - send_message(TelemetryData { - apps_travel: f32::from_le_bytes(packet[0..4].try_into().unwrap()), - motor_speed: f32::from_le_bytes(packet[4..8].try_into().unwrap()), - motor_torque: f32::from_le_bytes(packet[8..12].try_into().unwrap()), - max_motor_torque: f32::from_le_bytes(packet[12..16].try_into().unwrap()), - motor_direction: MotorRotateDirection::from_byte(packet[16]), - motor_state: MotorState::from_byte(packet[17]), - mcu_main_state: MCUMainState::from_byte(packet[18]), - mcu_work_mode: MCUWorkMode::from_byte(packet[19]), - mcu_voltage: f32::from_le_bytes(packet[20..24].try_into().unwrap()), - mcu_current: f32::from_le_bytes(packet[24..28].try_into().unwrap()), - motor_temp: i32::from_le_bytes(packet[28..32].try_into().unwrap()), - mcu_temp: i32::from_le_bytes(packet[32..36].try_into().unwrap()), - dc_main_wire_over_volt_fault: parse_bool(packet[36]), - dc_main_wire_over_curr_fault: parse_bool(packet[37]), - motor_over_spd_fault: parse_bool(packet[38]), - motor_phase_curr_fault: parse_bool(packet[39]), - motor_stall_fault: parse_bool(packet[40]), - mcu_warning_level: MCUWarningLevel::from_byte(packet[41]), - debug_0: f32::from_le_bytes(packet[42..46].try_into().unwrap()), - debug_1: f32::from_le_bytes(packet[46..50].try_into().unwrap()), - debug_2: f32::from_le_bytes(packet[50..54].try_into().unwrap()), - debug_3: f32::from_le_bytes(packet[54..58].try_into().unwrap()), - }) - .await; - } - } -} +use std::time::Duration; + +use crate::send::{send_message, Reading}; +// use rand::Rng; +use serde::Serialize; +use tokio::time::sleep; +use tokio_socketcan_isotp::{IsoTpSocket, StandardId}; + +const CAN_INTERFACE: &str = "can0"; + +macro_rules! define_enum { + (pub $name:ident, $($variant:ident = $value:expr),*) => { + #[derive(Serialize, Clone)] + #[allow(clippy::enum_variant_names)] + pub enum $name { + $($variant = $value),* + } + + impl $name { + fn from_byte(byte: u8) -> Self { + match byte { + $($value => Self::$variant),*, + _ => panic!("Invalid value for {}", stringify!($name)), + } + } + } + }; +} + +define_enum!( + pub MotorState, + MotorStateOff = 0, + MotorStatePrecharging = 1, + MotorStateIdle = 2, + MotorStateDriving = 3, + MotorStateFault = 4 +); + +define_enum!( + pub MotorRotateDirection, + DirectionStandby = 0, + DirectionForward = 1, + DirectionBackward = 2, + DirectionError = 3 +); + +define_enum!( + pub MCUMainState, + StateStandby = 0, + StatePrecharge = 1, + StatePowerReady = 2, + StateRun = 3, + StatePowerOff = 4 +); + +define_enum!( + pub MCUWorkMode, + WorkModeStandby = 0, + WorkModeTorque = 1, + WorkModeSpeed = 2 +); + +define_enum!( + pub MCUWarningLevel, + ErrorNone = 0, + ErrorLow = 1, + ErrorMedium = 2, + ErrorHigh = 3 +); + +#[derive(Serialize, Clone)] +pub struct TelemetryData { + pub apps_travel: f32, + pub motor_speed: f32, + pub motor_torque: f32, + pub max_motor_torque: f32, + pub motor_direction: MotorRotateDirection, + pub motor_state: MotorState, + pub mcu_main_state: MCUMainState, + pub mcu_work_mode: MCUWorkMode, + pub mcu_voltage: f32, + pub mcu_current: f32, + pub motor_temp: i32, + pub mcu_temp: i32, + pub dc_main_wire_over_volt_fault: bool, + pub dc_main_wire_over_curr_fault: bool, + pub motor_over_spd_fault: bool, + pub motor_phase_curr_fault: bool, + pub motor_stall_fault: bool, + pub mcu_warning_level: MCUWarningLevel, + pub debug_0: f32, + pub debug_1: f32, + pub debug_2: f32, + pub debug_3: f32, +} + +impl Reading for TelemetryData { + fn topic() -> &'static str { + "telemetry" + } +} + +fn parse_bool(byte: u8) -> bool { + byte != 0 +} + +pub async fn read_can() { + loop { + let Ok(socket) = IsoTpSocket::open( + CAN_INTERFACE, + StandardId::new(0x666).expect("Invalid src id"), + StandardId::new(0x777).expect("Invalid src id"), + ) else { + println!("Failed to open socket"); + sleep(Duration::from_secs(1)).await; + continue; + }; + + while let Ok(packet) = socket.read_packet().await { + if packet.len() != 58 { + println!("Invalid packet length {}: {:?}", packet.len(), packet); + continue; + } + send_message(TelemetryData { + apps_travel: f32::from_le_bytes(packet[0..4].try_into().unwrap()), + motor_speed: f32::from_le_bytes(packet[4..8].try_into().unwrap()), + motor_torque: f32::from_le_bytes(packet[8..12].try_into().unwrap()), + max_motor_torque: f32::from_le_bytes(packet[12..16].try_into().unwrap()), + motor_direction: MotorRotateDirection::from_byte(packet[16]), + motor_state: MotorState::from_byte(packet[17]), + mcu_main_state: MCUMainState::from_byte(packet[18]), + mcu_work_mode: MCUWorkMode::from_byte(packet[19]), + mcu_voltage: f32::from_le_bytes(packet[20..24].try_into().unwrap()), + mcu_current: f32::from_le_bytes(packet[24..28].try_into().unwrap()), + motor_temp: i32::from_le_bytes(packet[28..32].try_into().unwrap()), + mcu_temp: i32::from_le_bytes(packet[32..36].try_into().unwrap()), + dc_main_wire_over_volt_fault: parse_bool(packet[36]), + dc_main_wire_over_curr_fault: parse_bool(packet[37]), + motor_over_spd_fault: parse_bool(packet[38]), + motor_phase_curr_fault: parse_bool(packet[39]), + motor_stall_fault: parse_bool(packet[40]), + mcu_warning_level: MCUWarningLevel::from_byte(packet[41]), + debug_0: f32::from_le_bytes(packet[42..46].try_into().unwrap()), + debug_1: f32::from_le_bytes(packet[46..50].try_into().unwrap()), + debug_2: f32::from_le_bytes(packet[50..54].try_into().unwrap()), + debug_3: f32::from_le_bytes(packet[54..58].try_into().unwrap()), + }) + .await; + } + } +} diff --git a/fsae-raspi/src/lib.rs b/fsae-raspi/src/lib.rs new file mode 100644 index 0000000..acef2a7 --- /dev/null +++ b/fsae-raspi/src/lib.rs @@ -0,0 +1,5 @@ +pub mod can; +pub mod send; +pub mod mqtt; +pub mod influxdb; +pub mod utils; \ No newline at end of file diff --git a/fsae-raspi/src/send.rs b/fsae-raspi/src/send.rs index d7b0c73..afc34b8 100644 --- a/fsae-raspi/src/send.rs +++ b/fsae-raspi/src/send.rs @@ -1,73 +1,74 @@ -use reqwest::Client; -use rumqttc::{AsyncClient, MqttOptions, QoS}; -use serde::Serialize; -use std::sync::LazyLock; -use tokio::time::Duration; - -use crate::influxdb::to_line_protocol; - -pub const INFLUXDB_URL: &str = "http://0.0.0.0:8181"; -pub const INFLUXDB_DATABASE: &str = "fsae"; - -pub const MQTT_ID: &str = "fsae"; -pub const MQTT_HOST: &str = "127.0.0.1"; -pub const MQTT_PORT: u16 = 1883; - -pub trait Reading: Serialize { - fn topic() -> &'static str; -} - -static INFLUX_CLIENT: LazyLock = LazyLock::new(|| { - reqwest::Client::builder() - .build() - .expect("Failed to build InfluxDB client") -}); - -static MQTT_CLIENT: LazyLock = LazyLock::new(|| { - let mut mqttoptions = MqttOptions::new(MQTT_ID, MQTT_HOST, MQTT_PORT); - mqttoptions.set_keep_alive(Duration::from_secs(5)); - let (mqtt_client, mut eventloop) = AsyncClient::new(mqttoptions, 10); - - tokio::spawn(async move { - loop { - if let Err(e) = eventloop.poll().await { - eprintln!("MQTT eventloop error: {}", e); - tokio::time::sleep(Duration::from_secs(1)).await; - } - } - }); - - mqtt_client -}); - -pub async fn send_message(message: T) { - let json = match serde_json::to_string(&message) { - Ok(j) => j, - Err(e) => { - eprintln!("Failed to serialize: {}", e); - return; - } - }; - - if let Err(e) = MQTT_CLIENT - .publish(T::topic(), QoS::AtLeastOnce, false, json) - .await - { - eprintln!("Failed to publish to MQTT: {}", e); - } - - let line_protocol = to_line_protocol(&message); - let url = format!( - "{}/api/v3/write_lp?db={}&precision=nanosecond&accept_partial=true&no_sync=false", - INFLUXDB_URL, INFLUXDB_DATABASE - ); - if let Err(e) = INFLUX_CLIENT - .post(&url) - .header("Authorization", "Bearer apiv3_TQdSxXbtRc8qbzb4ejQOa-ir9-deb4fSVe5Lc-RgvQZqPKikusEJtZpQmEJakPtxZvst8wW4B20KB8iSGLC-Tg") - .body(line_protocol) - .send() - .await - { - eprintln!("Failed to write to InfluxDB: {}", e); - } -} +use reqwest::Client; +use rumqttc::{AsyncClient, MqttOptions, QoS}; +use serde::Serialize; +use std::sync::LazyLock; +use tokio::time::Duration; + +use crate::influxdb::to_line_protocol; + +pub const INFLUXDB_URL: &str = "http://localhost:8181"; + +pub const INFLUXDB_DATABASE: &str = "fsae"; + +pub const MQTT_ID: &str = "fsae"; +pub const MQTT_HOST: &str = "127.0.0.1"; +pub const MQTT_PORT: u16 = 1883; + +pub trait Reading: Serialize { + fn topic() -> &'static str; +} + +pub static INFLUX_CLIENT: LazyLock = LazyLock::new(|| { + reqwest::Client::builder() + .build() + .expect("Failed to build InfluxDB client") +}); + +static MQTT_CLIENT: LazyLock = LazyLock::new(|| { + let mut mqttoptions = MqttOptions::new(MQTT_ID, MQTT_HOST, MQTT_PORT); + mqttoptions.set_keep_alive(Duration::from_secs(5)); + let (mqtt_client, mut eventloop) = AsyncClient::new(mqttoptions, 10); + + tokio::spawn(async move { + loop { + if let Err(e) = eventloop.poll().await { + eprintln!("MQTT eventloop error: {}", e); + tokio::time::sleep(Duration::from_secs(1)).await; + } + } + }); + + mqtt_client +}); + +pub async fn send_message(message: T) { + let json = match serde_json::to_string(&message) { + Ok(j) => j, + Err(e) => { + eprintln!("Failed to serialize: {}", e); + return; + } + }; + + if let Err(e) = MQTT_CLIENT + .publish(T::topic(), QoS::AtLeastOnce, false, json) + .await + { + eprintln!("Failed to publish to MQTT: {}", e); + } + + let line_protocol = to_line_protocol(&message); + let url = format!( + "{}/api/v3/write_lp?db={}&precision=nanosecond&accept_partial=true&no_sync=false", + INFLUXDB_URL, INFLUXDB_DATABASE + ); + if let Err(e) = INFLUX_CLIENT + .post(&url) + .header("Authorization", "Bearer apiv3_TQdSxXbtRc8qbzb4ejQOa-ir9-deb4fSVe5Lc-RgvQZqPKikusEJtZpQmEJakPtxZvst8wW4B20KB8iSGLC-Tg") + .body(line_protocol) + .send() + .await + { + eprintln!("Failed to write to InfluxDB: {}", e); + } +} diff --git a/fsae-raspi/src/test.rs b/fsae-raspi/src/test.rs new file mode 100644 index 0000000..1653015 --- /dev/null +++ b/fsae-raspi/src/test.rs @@ -0,0 +1,39 @@ + +use fsae_raspi::send::{send_message}; +use fsae_raspi::can::{TelemetryData, MotorState, MotorRotateDirection, MCUMainState, MCUWorkMode, MCUWarningLevel}; +use fsae_raspi::utils::verify_influx_write; + +#[tokio::main] +async fn main() { + let test_packet = TelemetryData { + apps_travel: 1.0, + motor_speed: 1.0, + motor_torque: 1.0, + max_motor_torque: 1.0, + motor_direction: MotorRotateDirection::DirectionForward, + motor_state: MotorState::MotorStateDriving, + mcu_main_state: MCUMainState::StateRun, + mcu_work_mode: MCUWorkMode::WorkModeStandby, + mcu_voltage: 1.0, + mcu_current: 1.0, + motor_temp: 1, + mcu_temp: 1, + dc_main_wire_over_volt_fault: false, + dc_main_wire_over_curr_fault: false, + motor_over_spd_fault: false, + motor_phase_curr_fault: false, + motor_stall_fault: false, + mcu_warning_level: MCUWarningLevel::ErrorNone, + debug_0: 0.0, + debug_1: 0.0, + debug_2: 0.0, + debug_3: 0.0, + }; + + println!("Sending TelemetryData test packet to influxdb3"); + send_message(test_packet.clone()).await; + println!("finished sending, now verifying..."); + verify_influx_write(&test_packet).await; + +} + diff --git a/fsae-raspi/src/utils.rs b/fsae-raspi/src/utils.rs new file mode 100644 index 0000000..0a2e7ae --- /dev/null +++ b/fsae-raspi/src/utils.rs @@ -0,0 +1,70 @@ +use serde_json::Value; +use crate::send::{Reading, INFLUXDB_URL, INFLUXDB_DATABASE, INFLUX_CLIENT}; + +// verify if test packet was sent to influxdb3 +// returns 'true' if a matching packet is found and 'false' otherwise. +pub async fn verify_influx_write( + test_packet: &T, +) -> bool { + // build query string - topic is "telemetry" + let query = format!("SELECT * FROM {} ORDER BY time DESC LIMIT 5", T::topic()); + let url = format!("{}/api/v3/query?db={}", INFLUXDB_URL, INFLUXDB_DATABASE); + + + let resp = INFLUX_CLIENT + .post(&url) + .header("Authorization", "Bearer apiv3_TQdSxXbtRc8qbzb4ejQOa-ir9-deb4fSVe5Lc-RgvQZqPKikusEJtZpQmEJakPtxZvst8wW4B20KB8iSGLC-Tg") + .body(query) + .send() + .await; + + let Ok(resp) = resp else { + eprintln!("Failed to query InfluxDB: {}", resp.err().unwrap()); + return false; + }; + + //reads raw text + let Ok(text) = resp.text().await else { + eprintln!("Failed to parse InfluxDB response text"); + return false; + }; + + //parse json to serde_json + let Ok(json): Result = serde_json::from_str(&text) else { + eprintln!("Failed to deserialize InfluxDB JSON response"); + return false; + }; + + // serialize test packet to json to compare + let Ok(packet_json) = serde_json::to_value(test_packet) else { + eprintln!("Failed to serialize test packet for comparison"); + return false; + }; + + // goes through rows to compare test packet + if let Some(arr) = json.as_array() { + for row in arr { + if let Some(row_obj) = row.as_object() { + let mut matched = true; + + // compare only the overlapping fields that match + for (k, v) in packet_json.as_object().unwrap() { + if let Some(db_val) = row_obj.get(k) { + if db_val.to_string() != v.to_string() { + matched = false; + break; + } + } + } + + if matched { + println!("Matching packet found in InfluxDB"); + return true; + } + } + } + } + + println!("No matching packet found in InfluxDB"); + return false; +} diff --git a/fsae-vehicle-fw/src/main.cpp b/fsae-vehicle-fw/src/main.cpp index bd4137b..556486e 100644 --- a/fsae-vehicle-fw/src/main.cpp +++ b/fsae-vehicle-fw/src/main.cpp @@ -12,6 +12,7 @@ #include "vehicle/motor.h" #include "vehicle/telemetry.h" #include "vehicle/ifl100-36.h" +#include "vehicle/wdt.h" #include #include @@ -33,6 +34,7 @@ void setup() { // runs once on bootup Telemetry_Init(); Motor_Init(); MCU_Init(); + WDT_Init(); xTaskCreate(threadADC, "threadADC", THREAD_ADC_STACK_SIZE, NULL, THREAD_ADC_PRIORITY, NULL); xTaskCreate(threadMotor, "threadMotor", THREAD_MOTOR_STACK_SIZE, NULL, THREAD_MOTOR_PRIORITY, NULL); diff --git a/fsae-vehicle-fw/src/vehicle/apps.cpp b/fsae-vehicle-fw/src/vehicle/apps.cpp index fac83e8..aef3c45 100644 --- a/fsae-vehicle-fw/src/vehicle/apps.cpp +++ b/fsae-vehicle-fw/src/vehicle/apps.cpp @@ -40,6 +40,8 @@ void APPS_Init() { } void APPS_UpdateData(uint32_t rawReading1, uint32_t rawReading2) { + // update clock for WDT + apps_last_run_tick = xTaskGetTickCount(); // Filter incoming values LOWPASS_FILTER(rawReading1, appsData.apps1RawReading, appsAlpha); LOWPASS_FILTER(rawReading2, appsData.apps2RawReading, appsAlpha); diff --git a/fsae-vehicle-fw/src/vehicle/bse.cpp b/fsae-vehicle-fw/src/vehicle/bse.cpp index 637d5ec..fd33914 100644 --- a/fsae-vehicle-fw/src/vehicle/bse.cpp +++ b/fsae-vehicle-fw/src/vehicle/bse.cpp @@ -31,6 +31,8 @@ void BSE_Init() { } void BSE_UpdateData(uint32_t bseReading1, uint32_t bseReading2){ + // update clock for WDT + bse_last_run_tick = xTaskGetTickCount(); // Filter incoming values LOWPASS_FILTER(bseReading1, bseRawData.bseRawFront, bseAlpha); LOWPASS_FILTER(bseReading2, bseRawData.bseRawRear, bseAlpha); diff --git a/fsae-vehicle-fw/src/vehicle/wdt.cpp b/fsae-vehicle-fw/src/vehicle/wdt.cpp new file mode 100644 index 0000000..0649a23 --- /dev/null +++ b/fsae-vehicle-fw/src/vehicle/wdt.cpp @@ -0,0 +1,85 @@ +#include "apps.h" +#include "bse.h" +#include "wdt.h" + +#include +#include +#include + +// Bitmask flag definition +static constexpr uint8_t WDT_BIT_BSE = 0b01; +static constexpr uint8_t WDT_BIT_APPS = 0b10; + +static constexpr uint8_t WDT_REQUIRED_MASK = 0b00; // 0b00 represents no flags + + +static volatile TickType_t bse_last_run_tick; +static volatile TickType_t apps_last_run_tick; + +static WDT_T4 WDT; + +void WDT_Init() { + TickType_t now = xTaskGetTickCount(); + bse_last_run_tick = now; + apps_last_run_tick = now; + + WDT_timings_t config; + + config.timeout = 1.0; // second before reset + config.trigger = 0.0; + config.callback = nullptr; + + WDT.begin(config); + + Serial.println("Watchdog initialized (1 second timeout)"); + +} + + +void WDT_Update_Task(void* arg) +{ + // Change values to actual update periods + static constexpr uint32_t BSE_EXPECTED_PERIOD_MS = 100; + static constexpr uint32_t APPS_EXPECTED_PERIOD_MS = 100; + + // Timeout = expected * 3 + static constexpr uint32_t BSE_WDT_TIMEOUT_MS = BSE_EXPECTED_PERIOD_MS * 3; + static constexpr uint32_t APPS_WDT_TIMEOUT_MS = APPS_EXPECTED_PERIOD_MS * 3; + + + static constexpr uint32_t WDT_CHECK_PERIOD_MS = 100; + + TickType_t now; + + TickType_t bse_ageTicks; + uint32_t bse_ageMs; + + TickType_t apps_ageTicks; + uint32_t apps_ageMs; + + uint8_t mask; + + for (;;) + { + now = xTaskGetTickCount(); + + bse_ageTicks = now - bse_last_run_tick; + bse_ageMs = bse_ageTicks * portTICK_PERIOD_MS; + + apps_ageTicks = now - apps_last_run_tick; + apps_ageMs = apps_ageTicks * portTICK_PERIOD_MS; + + mask = 0b00; + + if (bse_ageMs > BSE_WDT_TIMEOUT_MS) mask |= WDT_BIT_BSE; // x |= y ==> x = x | y + if (apps_ageMs > APPS_WDT_TIMEOUT_MS) mask |= WDT_BIT_APPS; + + // pet if 0b00 + if (mask == WDT_REQUIRED_MASK) + { + WDT.feed(); // pet hardware watchdog + } + + vTaskDelay(pdMS_TO_TICKS(WDT_CHECK_PERIOD_MS)); // 100ms delay + } +} \ No newline at end of file diff --git a/fsae-vehicle-fw/src/vehicle/wdt.h b/fsae-vehicle-fw/src/vehicle/wdt.h new file mode 100644 index 0000000..f63d455 --- /dev/null +++ b/fsae-vehicle-fw/src/vehicle/wdt.h @@ -0,0 +1,4 @@ +// Anteater Electric Racing, 2026 + +void WDT_Init(); +void WDT_Task(); \ No newline at end of file