From 404821c288eda0d94c27464e07c92108bc16fc52 Mon Sep 17 00:00:00 2001 From: kenneth dao Date: Wed, 29 Oct 2025 03:49:10 -0700 Subject: [PATCH 1/2] test influxdb3 --- fsae-raspi/Cargo.toml | 4 + fsae-raspi/src/can.rs | 302 ++++++++++++++++++++-------------------- fsae-raspi/src/lib.rs | 5 + fsae-raspi/src/send.rs | 147 +++++++++---------- fsae-raspi/src/test.rs | 39 ++++++ fsae-raspi/src/utils.rs | 70 ++++++++++ 6 files changed, 343 insertions(+), 224 deletions(-) create mode 100644 fsae-raspi/src/lib.rs create mode 100644 fsae-raspi/src/test.rs create mode 100644 fsae-raspi/src/utils.rs 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; +} From e4e3b2a8833ea036a92eb83aabc3e48062e60197 Mon Sep 17 00:00:00 2001 From: kenneth dao Date: Thu, 19 Feb 2026 00:10:02 -0800 Subject: [PATCH 2/2] Implemented WDT_T4-based hardware watchdog for Teensy 4. Added WDT init and update task Integrated APPS and BSE task heartbeat monitoring Configured automatic MCU reset if either task stalls beyond timeout Added WDT init to main.cpp Unsure of library as compiler can't update so will need check on include header --- fsae-vehicle-fw/src/main.cpp | 2 + fsae-vehicle-fw/src/vehicle/apps.cpp | 2 + fsae-vehicle-fw/src/vehicle/bse.cpp | 2 + fsae-vehicle-fw/src/vehicle/wdt.cpp | 85 ++++++++++++++++++++++++++++ fsae-vehicle-fw/src/vehicle/wdt.h | 4 ++ 5 files changed, 95 insertions(+) create mode 100644 fsae-vehicle-fw/src/vehicle/wdt.cpp create mode 100644 fsae-vehicle-fw/src/vehicle/wdt.h 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