From 2524d4a3d885f89cf8c10593ff87a7365c12ce2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moir=C3=A9?= Date: Sat, 5 Sep 2026 19:24:47 -0700 Subject: [PATCH 1/4] Allow UART dropping --- src/floatware.rs | 91 +++++++++++++++++----------- src/main.rs | 7 ++- src/prelude.rs | 1 + src/signals.rs | 72 +++++++--------------- src/tasks/button.rs | 85 ++++++++++++++++++++++++-- src/tasks/http.rs | 27 ++++++--- src/tasks/led.rs | 103 +++++++++++++++++++------------- src/tasks/status.rs | 49 ++++----------- src/tasks/stepper_controller.rs | 21 +++++-- 9 files changed, 273 insertions(+), 183 deletions(-) diff --git a/src/floatware.rs b/src/floatware.rs index ac6839b..ef77290 100644 --- a/src/floatware.rs +++ b/src/floatware.rs @@ -1,37 +1,41 @@ use crate::{ prelude::*, tasks::{ - *, + button::boot_button_pressed_task, charter::{ - depth_target_update_task, - depth_control_task + depth_control_task, + depth_target_update_task }, i2c::initialize_i2c_thread, leak_detection::leak_detection_task, + led::{led_cycle_task, led_selection_task}, power_measurement::power_measurement_task, - shutdown::Shutdown, - status::status_publishing_task, - stepper_controller::stepper_control_task, sd_card::{ - read_config_from_sd, - setup_sd_card, mount_sd_card, - sd_logging_task + read_config_from_sd, + sd_logging_task, + setup_sd_card }, - button::boot_button_pressed_task, + shutdown::{ + shutdown_task, + Shutdown, + }, + status::status_publishing_task, + stepper_controller::stepper_control_task, + *, }, }; use esp_idf_svc::{ eventloop::EspSystemEventLoop, hal::{ - peripherals::Peripherals, - gpio::{Gpio9, PinDriver, Pull} + gpio::{Gpio9, PinDriver, Pull}, + peripherals::Peripherals }, http::server::EspHttpServer, nvs::EspDefaultNvsPartition, - timer::EspTaskTimerService, sys::{uxTaskGetStackHighWaterMark2, xTaskGetCurrentTaskHandle}, + timer::EspTaskTimerService, }; use ws2812_esp32_rmt_driver::{driver::color::LedPixelColorGrbw32, LedPixelEsp32Rmt, RGB8}; @@ -74,6 +78,8 @@ static I2C_CHANNEL: I2cUnsplitChannel = I2cUnsplitChannel::new(); static CHARTER_STATE_CHANNEL: CharterStateUnsplitWatch = CharterStateUnsplitWatch::new(/* no Charter */); +static RELEASE_UART_CHANNEL: UartReleaseUnsplitWatch = UartReleaseUnsplitWatch::new(); + /// Where we actually start setting up everything. This function is responsible for /// spawning the remaining threads, initializing peripherals, and spawning tasks. pub async fn float_thread() -> Void { @@ -94,6 +100,7 @@ pub async fn float_thread() -> Void { let status_channel = StatusUnsplitWatch::new(); let led_state_channel = LedStateSignal::new(); let stepper_state_channel = StepperStateSignal::new(); + let button_led_signal = LedColorSignal::new(); //////////////////////////////////////// // Initialize external free-running systems @@ -149,6 +156,7 @@ pub async fn float_thread() -> Void { &SHUTDOWN_CHANNEL, I2C_CHANNEL.sender(), CHARTER_STATE_CHANNEL.sender(), + RELEASE_UART_CHANNEL.sender(), ).map_err(damn!("Failed to initialize HTTP server"))?; let led_driver = @@ -163,14 +171,7 @@ pub async fn float_thread() -> Void { // Any task returning an error leads to an immediate shutdown let Err(error) = futures::try_join!( - led::led_task( - led_driver, - &led_state_channel, // reader - ), - shutdown::shutdown_task( - &SHUTDOWN_CHANNEL, // reader - CHARTER_STATE_CHANNEL.sender() - ), + // ========== Peripheral Management ========== depth_target_update_task( wrecv!(CHARTER_STATE_CHANNEL), CHARTER_STATE_CHANNEL.sender(), @@ -181,6 +182,15 @@ pub async fn float_thread() -> Void { wrecv!(status_channel), &stepper_state_channel, // writer ), + stepper_control_task( + peripherals.uart0, + peripherals.pins.gpio20, // RX + peripherals.pins.gpio21, // TX + peripherals.pins.gpio18, // dir + peripherals.pins.gpio19, // step + &stepper_state_channel, // reader + wrecv!(RELEASE_UART_CHANNEL), + ), leak_detection_task( peripherals.pins.gpio3, &SHUTDOWN_CHANNEL, // writer @@ -191,28 +201,37 @@ pub async fn float_thread() -> Void { peripherals.pins.gpio1, // current ADC power_measurement_request_channel.receiver(), ), - stepper_control_task( - peripherals.uart0, - peripherals.pins.gpio20, // RX - peripherals.pins.gpio21, // TX - peripherals.pins.gpio18, // dir - peripherals.pins.gpio19, // step - &stepper_state_channel, // reader + // ========== I/O ========== + boot_button_pressed_task( + peripherals.pins.gpio9, + &button_led_signal, // writer + RELEASE_UART_CHANNEL.sender(), // writer + ), + led_selection_task( + &led_state_channel, // writer + wrecv!(status_channel), + &button_led_signal, // reader + wrecv!(RELEASE_UART_CHANNEL), + ), + led_cycle_task( + led_driver, + &led_state_channel, // reader ), + sd_logging_task( + wrecv!(status_channel), + &sd_fs_handle, + ), + // ========== Internal Housekeeping ========== status_publishing_task( power_measurement_request_channel.sender(), status_channel.sender(), I2C_CHANNEL.sender(), wrecv!(CHARTER_STATE_CHANNEL), - &led_state_channel, // writer ), - boot_button_pressed_task( - peripherals.pins.gpio9 + shutdown_task( + &SHUTDOWN_CHANNEL, // reader + CHARTER_STATE_CHANNEL.sender() ), - sd_logging_task( - wrecv!(status_channel), - &sd_fs_handle, - ) ); // The shutdown task has a unique error that it returns to indicate a normal shutdown @@ -240,7 +259,7 @@ fn boot_button_pressed(input: &mut Gpio9) -> bool { unsafe { input.reborrow() }, Pull::Up ) { - Ok(driver) => driver.is_low(), + Ok(driver) => driver.is_low(), // Button is active-low Err(error) => { warn!("Unable to create input driver to check boot button: {error:#?}"); false diff --git a/src/main.rs b/src/main.rs index d615e6e..6ece06f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,8 @@ -#![feature(io_error_too_many_open_files)] -#![feature(io_error_input_output_error)] +#![feature( + io_error_too_many_open_files, + io_error_input_output_error, + const_trait_impl, +)] mod floatware; mod tasks; diff --git a/src/prelude.rs b/src/prelude.rs index d5cb63b..4302d11 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -10,6 +10,7 @@ use esp_idf_svc::timer::EspAsyncTimer; pub(crate) use { crate::{ signals::*, + tasks::*, damn, sleep, sleep_ms, ret_err, SD_CARD_NAME, sd }, anyhow::Error as AnyhowError, diff --git a/src/signals.rs b/src/signals.rs index 0ad1846..76618b0 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -34,75 +34,49 @@ use embassy_sync::{ Receiver as WatchReceiver, } }; - +use embassy_sync::blocking_mutex::raw::NoopRawMutex; use futures::channel::oneshot::Sender; - +use smart_leds_trait::RGB8; //////////////////////////////////////////////////////////////////////////////// // TODO: When finished, convert all the Watches that only have one reader into Signals +// TODO: Make a macro to generate watches -/// Signal that tells the float-thread to exit. Uses [EspRawMutex] because can be -/// written from the HTTP thread. -/// -/// Unit because the intent is marked by simple presence of the message. Once a -/// message is sent on the channel, the system will shut down. +/// Signal that tells the float-thread to exit. Written from the HTTP `/shutdown`. pub(crate) type ShutdownSignal = Signal; - -// const SHUTDOWN_SIGNAL_RECEIVERS: usize = 2; -// pub(crate) type ShutdownSignalUnsplitWatch = Watch; -// pub(crate) type ShutdownSignalReceiver<'a> = WatchReceiver<'a, EspRawMutex, (), SHUTDOWN_SIGNAL_RECEIVERS>; -// pub(crate) type ShutdownSignalSender<'a> = WatchSender<'a, EspRawMutex, (), SHUTDOWN_SIGNAL_RECEIVERS>; - - +/// Used to send commands to the stepper motor. Only used on float thread. +pub(crate) type StepperStateSignal = Signal; +/// Used to send LED color information between tasks. Only used on float thread. +pub(crate) type LedColorSignal = Signal; +/// Used to set the current LED cycle. Only used on float thread. +pub(crate) type LedStateSignal = Signal; + +// Written by HTTP `/start_dive` const CHARTER_STATE_RECEIVERS: usize = 3; pub(crate) type CharterStateUnsplitWatch = Watch; pub(crate) type CharterStateReceiver<'a> = WatchReceiver<'a, EspRawMutex, CharterState, CHARTER_STATE_RECEIVERS>; pub(crate) type CharterStateSender<'a> = WatchSender<'a, EspRawMutex, CharterState, CHARTER_STATE_RECEIVERS>; -pub(crate) type StepperStateSignal = Signal; - -pub(crate) type LedStateSignal = Signal; - -// TODO: perhaps make a macro to generate these - -// ==== System Config Channels ==== - -// Instead of having a single big config struct, we have separate channels -// (Watches, to be precise) that contain the system status, which tasks can -// individually examine. That way, updates to unrelated configuration don't -// unnecessarily wake unrelated tasks. - - -// const CONFIG_CHARTER_RECEIVERS: usize = 2; -// pub(crate) type ConfigCharterUnsplitWatch = Watch; -// pub(crate) type ConfigCharterReceiver<'a> = WatchReceiver<'a, EspRawMutex, Charter, CONFIG_CHARTER_RECEIVERS>; -// pub(crate) type ConfigCharterSender<'a> = WatchSender<'a, EspRawMutex, Charter, CONFIG_CHARTER_RECEIVERS>; - -// ==== System Status Channels ==== - -// In the same way, we don't have a big status struct and channel for it. We split. +// Written from HTTP thread +const UART_RELEASE_RECEIVERS: usize = 2; +pub(crate) type UartReleaseUnsplitWatch = Watch; +pub(crate) type UartReleaseReceiver<'a> = WatchReceiver<'a, EspRawMutex, (), UART_RELEASE_RECEIVERS>; +pub(crate) type UartReleaseSender<'a> = WatchSender<'a, EspRawMutex, (), UART_RELEASE_RECEIVERS>; +// Not used outside float thread const POWER_MEASUREMENT_REQUEST_BUFFER_SIZE: usize = 8; -pub(crate) type PowerMeasurementRequestUnsplitChannel = Channel, POWER_MEASUREMENT_REQUEST_BUFFER_SIZE>; -pub(crate) type PowerMeasurementRequestReceiver<'a> = ChannelReceiver<'a, EspRawMutex, Sender, POWER_MEASUREMENT_REQUEST_BUFFER_SIZE>; -pub(crate) type PowerMeasurementRequestSender<'a> = ChannelSender<'a, EspRawMutex, Sender, POWER_MEASUREMENT_REQUEST_BUFFER_SIZE>; +pub(crate) type PowerMeasurementRequestUnsplitChannel = Channel, POWER_MEASUREMENT_REQUEST_BUFFER_SIZE>; +pub(crate) type PowerMeasurementRequestReceiver<'a> = ChannelReceiver<'a, NoopRawMutex, Sender, POWER_MEASUREMENT_REQUEST_BUFFER_SIZE>; +pub(crate) type PowerMeasurementRequestSender<'a> = ChannelSender<'a, NoopRawMutex, Sender, POWER_MEASUREMENT_REQUEST_BUFFER_SIZE>; +// Will be used in http thread const STATUS_RECEIVERS: usize = 2; pub(crate) type StatusUnsplitWatch = Watch; pub(crate) type StatusReceiver<'a> = WatchReceiver<'a, EspRawMutex, SystemStatus, STATUS_RECEIVERS>; pub(crate) type StatusSender<'a> = WatchSender<'a, EspRawMutex, SystemStatus, STATUS_RECEIVERS>; -// const CURRENT_DEPTH_RECEIVERS: usize = 2; -// pub(crate) type CurrentDepthUnsplitWatch = Watch; -// pub(crate) type CurrentDepthReceiver<'a> = WatchReceiver<'a, EspRawMutex, Depth, CURRENT_DEPTH_RECEIVERS>; -// pub(crate) type CurrentDepthSender<'a> = WatchSender<'a, EspRawMutex, Depth, CURRENT_DEPTH_RECEIVERS>; - -// ==== I2C Command Channels ==== - +// Used on I2C thread, obviously. const I2C_BUFFER_SIZE: usize = 8; -/// If this signature is updated, [i2c::i2c_thread] must also be updated. -/// -/// TODO: check thread safety pub(crate) type I2cUnsplitChannel = Channel; pub(crate) type I2cSender<'a> = ChannelSender<'a, EspRawMutex, I2cCommand, I2C_BUFFER_SIZE>; pub(crate) type I2cReceiver<'a> = ChannelReceiver<'a, EspRawMutex, I2cCommand, I2C_BUFFER_SIZE>; diff --git a/src/tasks/button.rs b/src/tasks/button.rs index 7b02b26..83a00d1 100644 --- a/src/tasks/button.rs +++ b/src/tasks/button.rs @@ -1,23 +1,96 @@ +//! This task reads the state of the "BOOT" button on the ESP rust board. When +//! pressed, it measures the amount of time that the button is held for, and when +//! the button is released, different actions are taken based on held duration. +//! Additionally, the LED changes while the button is held in order to indicate what +//! will happen when it is released. + use crate::prelude::*; use esp_idf_svc::hal::gpio::{Gpio9, PinDriver, Pull}; +use embassy_time::{Duration, WithTimeout}; +use smart_leds_trait::RGB8; + //////////////////////////////////////////////////////////////////////////////// -/// Sends out a signal containing the state of the BOOT button on the board when -/// it's pressed. +#[derive(Copy, Clone)] +struct Con(Duration, RGB8); + +const impl Con { + fn from_list(lst: [(u64, RGB8); N]) -> [Self; N] { + let mut prev = 0; + + // Since this function is `const`, these elements will never actually exist (I think) + let mut ret = [Self(Duration::MIN, led::NONE); N]; + + let mut i = 0; + while i < N { + let (ms, led) = lst[i]; + ret[i] = Self(Duration::from_millis(ms - prev), led); + prev = ms; + + i += 1; + } + + ret + } +} + +/// When holding the button, the LED will show the particular color for up to that +/// many milliseconds after the button begins to be held. They are time-delimiters +/// for the actions shown in-between. +const DURATIONS: [Con; 3] = Con::from_list([ + // None, guard against accidental presses + ( 200, led::NONE), + // TBD + (3000, led::NONE), + // Drop UART + (6000, led::PINK), + // None; if you hold the button for too long, you can keep holding it until it resets to noop. +]); + pub async fn boot_button_pressed_task( boot_button: Gpio9<'_>, - // TODO: communication channel + led_color_signal: &LedColorSignal, + release_uart_signal: UartReleaseSender<'_>, ) -> Never { let mut driver = PinDriver::input(boot_button, Pull::Up) .map_err(damn!("Failed to initialize boot button input driver"))?; loop { - driver.wait_for_any_edge().await.map_err(damn!("Edge await failed"))?; + // Button is active low + driver.wait_for_falling_edge().await.map_err(damn!("Press await failed"))?; + + // Iterate over all the durations. For each one, set the LED to that color, + for (index, Con(duration, led)) in DURATIONS.iter().enumerate() { + // Set LED to indicate what will happen if you release the button + led_color_signal.signal(*led); - let state = driver.is_high(); + if let Ok(result) = + driver.wait_for_rising_edge() + .with_timeout(*duration) + .await + { + // Button released + result.map_err(damn!("Unpress await failed"))?; + match index { + 0 => {/* [0, 200) -> nothing; guard */}, + 1 => { + // [200, 3000) -> TBD + }, + 2 => { + // [3000, 6000) -> release UART + release_uart_signal.send(()); + }, + _ => unreachable!(), + } + break; + } // else: timed out, go to next + } + // Went past last item (or broke after action); overflow/reset to doing nothing - info!("Boot button {}pressed", if state { "" } else { "un" }); + led_color_signal.reset(); + // Note that signal reset != none. When it is none, the LED + // will (probably) flash, as opposed to not changing at all } } diff --git a/src/tasks/http.rs b/src/tasks/http.rs index 3e9cc10..e56eed7 100644 --- a/src/tasks/http.rs +++ b/src/tasks/http.rs @@ -50,17 +50,19 @@ use embedded_svc::{ pub fn initialize_http_server<'server>( shutdown_signal_sender: &'static ShutdownSignal, i2c_sender: I2cSender<'static>, - charter_state_sender: CharterStateSender<'static> + charter_state_sender: CharterStateSender<'static>, + uart_release_sender: UartReleaseSender<'static>, ) -> Result, EspIOError> { let mut server = EspHttpServer::new(&Default::default())?; server - .handler("/heartbeat", Method::Get, GetHeartbeat)? - .handler("/status", Method::Get, pep(GetStatus { i2c_sender }))? - .handler("/config", Method::Post, pep(PostConfig {}))? - .handler("/shutdown", Method::Post, pep(PostShutdown { shutdown_signal_sender }))? - .handler("/start_dive", Method::Post, pep(PostStartDive { charter_state_sender }))? - .handler("/time_sync", Method::Post, pep(PostTimeSync))? + .handler("/heartbeat", Method::Get, GetHeartbeat)? + .handler("/status", Method::Get, pep(GetStatus { i2c_sender }))? + .handler("/config", Method::Post, pep(PostConfig {}))? + .handler("/shutdown", Method::Post, pep(PostShutdown { shutdown_signal_sender }))? + .handler("/start_dive", Method::Post, pep(PostStartDive { charter_state_sender }))? + .handler("/time_sync", Method::Post, pep(PostTimeSync))? + .handler("/release_uart", Method::Post, pep(PostReleaseUart { uart_release_sender }))? ; Ok(server) @@ -151,6 +153,17 @@ impl<'request> Handler> for PostShutdown { } } +struct PostReleaseUart { uart_release_sender: UartReleaseSender<'static> } +impl<'request> Handler> for PostReleaseUart { + type Error = AnyhowError; + + fn handle(&self, conn: &mut EspHttpConnection) -> Result<(), AnyhowError> { + self.uart_release_sender.send(()); + + reply_204(conn) + } +} + //////////////////////////////////////////////////////////////////////////////// fn read_body(conn: &mut EspHttpConnection) -> Result, AnyhowError> { diff --git a/src/tasks/led.rs b/src/tasks/led.rs index 7b1ddfc..462c937 100644 --- a/src/tasks/led.rs +++ b/src/tasks/led.rs @@ -1,8 +1,12 @@ use crate::{ prelude::*, + charter::CharterState, }; -use std::ops::Index; +use std::{ + ops::Index, + ptr, +}; use serde::Deserialize; @@ -29,12 +33,14 @@ macro_rules! color_const { } color_const!( NONE, 0x000000); -color_const!( DIM, 0x444444); +color_const!( DIM, 0x222222); color_const!( RED, 0xFF0000); color_const!( GREEN, 0x00FF00); color_const!( BLUE, 0x0000FF); color_const!(YELLOW, 0xFFFF00); color_const!(ORANGE, 0xFF7700); +color_const!( PINK, 0xFF36D9); +color_const!( AQUA, 0x00FFAA); const LED_MAXIMUM_SIMULTANEOUS_STATES: usize = 6; @@ -45,16 +51,14 @@ pub struct LEDState { } impl LEDState { - pub fn new(list: &[RGB8]) -> Result { - let size = list.len(); - if size > LED_MAXIMUM_SIMULTANEOUS_STATES { - Err(AnyhowError::msg("LED state size is too large")) - } else { - Ok(Self { - size, - list: list.try_into()?, - }) - } + pub fn new(slice: &[RGB8]) -> Result { + let size = slice.len(); + if size <= LED_MAXIMUM_SIMULTANEOUS_STATES { + let mut list = [NONE; LED_MAXIMUM_SIMULTANEOUS_STATES]; + // This is safe because slice.len() <= list.len() + unsafe { ptr::copy_nonoverlapping(slice.as_ptr(), list.as_mut_ptr(), size); } + Ok(Self { size, list }) + } else { Err(AnyhowError::msg("LED state size is too large")) } } } @@ -66,38 +70,11 @@ impl Index for LEDState { } } -// enum LEDStateOld { -// CharterUpdate(CharterState), -// StatusUpdate(SystemStatus) -// } -// -// impl LEDStateOld { -// fn get_led_color(&self) -> RGB8 { -// match self { -// Self::CharterUpdate(charter_state) => match charter_state { -// CharterState::StartRequested => BLUE, -// CharterState::InProgress { -// charter_index, target_depth: _, charter_size -// } => { -// // Fade from blue to green as the charter is completed -// let progress = (charter_index * 255 / charter_size) as u8; // 0 to 255 -// RGB8::new(0, progress, 255 - progress) -// } -// CharterState::Completed => GREEN, -// CharterState::Aborted { .. } => RED, -// }, -// Self::StatusUpdate(status) => { -// -// } -// } -// } -// } - //////////////////////////////////////////////////////////////////////////////// /// Lights the LED in accordance with the LED status set by the status generation /// task. We iterate through the colors, and cycle the LED through them. -pub(crate) async fn led_task( +pub(crate) async fn led_cycle_task( mut led_driver: LedPixelEsp32Rmt<'_, RGB8, LedPixelColorGrbw32>, led_state_signal: &LedStateSignal, ) -> Never { @@ -139,3 +116,49 @@ pub(crate) async fn led_task( set_led(current_led_state[led_index]); } } + +pub(crate) async fn led_selection_task( + led_state_signal: &LedStateSignal, + mut status_receiver: StatusReceiver<'_>, + button_led_signal: &LedColorSignal, + uart_release_receiver: UartReleaseReceiver<'_> +) -> Never { + loop { + let status = status_receiver.get().await; + + let charter_state = status.charter_state; + + // The LED can be set to blink in a sequence of colors, so different systems can + // convey their statuses simultaneously. + // Currently, though, the only thing that does is the charter. + let charter_led = match &charter_state { + None => AQUA, + Some(state) => match state { + CharterState::StartRequested => BLUE, + CharterState::InProgress { + charter_index, target_depth: _, charter_size + } => { // Fade from blue to green as the charter is completed + let progress = (charter_index * 255 / charter_size) as u8; // 0 to 255 + RGB8::new(0, progress, 255 - progress) + }, + CharterState::Completed => GREEN, + CharterState::Aborted { .. } => ORANGE, + } + }; + + let button_led = button_led_signal.try_take(); + + let mut led_vec = Vec::with_capacity(LED_MAXIMUM_SIMULTANEOUS_STATES); + led_vec.push(charter_led); + + if let Some(button_led) = button_led { + led_vec.push(button_led); + } + + if uart_release_receiver.contains_value() { + led_vec.push(PINK); + } + + led_state_signal.signal(LEDState::new(led_vec.as_slice())?); + } +} diff --git a/src/tasks/status.rs b/src/tasks/status.rs index 3ec09a9..c541406 100644 --- a/src/tasks/status.rs +++ b/src/tasks/status.rs @@ -1,21 +1,19 @@ -use crate::{prelude::*, get_time, tasks::{ +use crate::{get_time, prelude::*, tasks::{ charter::{ - Depth, - CharterState + CharterState, + Depth }, - power_measurement::PowerMeasurement, i2c::I2cCommand, - led::LEDState -}, TimeContainer, tasks}; + power_measurement::PowerMeasurement, +}, TimeContainer}; use std::fmt::{Debug, Display}; -use std::slice; + use futures::{ channel::oneshot::channel, join }; -use smart_leds_trait::RGB8; -use tasks::led; + //////////////////////////////////////////////////////////////// const SNAPSHOT_INTERVAL_MS: u64 = 100; @@ -59,9 +57,8 @@ pub async fn status_publishing_task( status_sender: StatusSender<'_>, i2c_sender: I2cSender<'_>, mut charter_state_receiver: CharterStateReceiver<'_>, - led_signal: &LedStateSignal, ) -> Never { - let mut counter = 0u8; + let mut counter = 0; loop { counter += 1; let create_log_entry = counter == 10; @@ -70,8 +67,8 @@ pub async fn status_publishing_task( } // To make things more efficient, we can concurrently await all four asynchronous - // transactions that need to take place (respectively sending and receiving the request and - // response for each of the power and depth measurements + // transactions that need to take place (respectively sending and receiving the + // request and response for each of the power and depth measurements let (power_measurement, depth) = { let (power_tx, power_rx) = channel(); let (depth_tx, depth_rx) = channel(); @@ -97,32 +94,6 @@ pub async fn status_publishing_task( let charter_state = charter_state_receiver.try_get(); - // The LED can be set to blink in a sequence of colors, so different systems can - // convey their statuses simultaneously. - // Currently, though, the only thing that does is the charter. - let charter_led = match &charter_state { - None => led::NONE, - Some(state) => match state { - CharterState::StartRequested => led::BLUE, - CharterState::InProgress { - charter_index, target_depth: _, charter_size - } => { // Fade from blue to green as the charter is completed - let progress = (charter_index * 255 / charter_size) as u8; // 0 to 255 - RGB8::new(0, progress, 255 - progress) - }, - CharterState::Completed => led::GREEN, - CharterState::Aborted { .. } => led::ORANGE, - } - }; - - let led_state = if charter_led == led::NONE { - LEDState::new(&[led::NONE, led::DIM])? // Heartbeat of sorts if no charter - } else { - LEDState::new(slice::from_ref(&charter_led))? - }; - - led_signal.signal(led_state); - status_sender.send(SystemStatus { depth, power_measurement, diff --git a/src/tasks/stepper_controller.rs b/src/tasks/stepper_controller.rs index 15ff6f5..ce78e33 100644 --- a/src/tasks/stepper_controller.rs +++ b/src/tasks/stepper_controller.rs @@ -16,11 +16,13 @@ use esp_idf_svc::{ } }; +use futures::future::{select, Either}; + //////////////////////////////////////////////////////////////////////////////// pub type StepperState = (/* TODO */); -/// TODO: Tharuka +/// TODO pub async fn stepper_control_task( uart_controller: impl Uart, rx_pin: impl InputPin, @@ -28,7 +30,14 @@ pub async fn stepper_control_task( dir_pin: impl OutputPin, step_pin: impl OutputPin, stepper_state_channel: &StepperStateSignal, -) -> Never { + mut release_uart_signal: UartReleaseReceiver<'_>, +) -> Void { + // When the UART controller assumes control of the UART pins, it + // clobbers USB serial communication, including flashing & console. + // So, in order to make reflashing easier, we need to have a way to drop + // the UART controller and let it release its associated pins. So when + // we receive a release UART signal, we must return. + let uart_driver = AsyncUartDriver::new( uart_controller, tx_pin, rx_pin, None::, None::, // CTS & RTS pins, which we don't use @@ -42,7 +51,11 @@ pub async fn stepper_control_task( .map_err(damn!("Failed to initialize stepper STEP pin"))?; loop { - let state = stepper_state_channel.wait().await; - // TODO + // Concurrently await stepper commands or a UART release signal + let future = select(stepper_state_channel.wait(), release_uart_signal.get()).await; + + // Pull out stepper state if we get it, otherwise we got the UART release signal so we need + let stepper_state = if let Either::Left((s, _)) = future { s } else { return Ok(()) }; + } } From 82b841d7ef120a2e5104636b60550686aa9d5ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moir=C3=A9?= Date: Tue, 8 Sep 2026 22:47:06 -0700 Subject: [PATCH 2/4] Profiling --- .cargo/config.toml | 6 +- Cargo.toml | 2 + pin_allocations.txt | 8 +- sdkconfig.defaults | 3 + src/config.rs | 26 ++++ src/debugging.rs | 178 ++++++++++++++++++++++++ src/floatware.rs | 232 ++++++++++++++++++++++---------- src/main.rs | 3 + src/signals.rs | 130 +++++++++++------- src/tasks/button.rs | 14 +- src/tasks/charter.rs | 46 ++----- src/tasks/http.rs | 4 +- src/tasks/led.rs | 39 +++--- src/tasks/sd_card.rs | 35 ++--- src/tasks/status.rs | 13 +- src/tasks/stepper_controller.rs | 92 ++++++++++--- 16 files changed, 605 insertions(+), 226 deletions(-) create mode 100644 src/config.rs create mode 100644 src/debugging.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 94270e4..70222e4 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,7 +4,10 @@ target = "riscv32imc-esp-espidf" [target.'cfg(target_os = "espidf")'] linker = "ldproxy" runner = "espflash flash --monitor" -rustflags = [ "--cfg", "espidf_time64"] +rustflags = [ + "--cfg", "espidf_time64", + "-C", "force-frame-pointers=yes" +] [unstable] build-std = ["std", "panic_abort"] @@ -13,6 +16,7 @@ build-std = ["std", "panic_abort"] MCU = "esp32c3" ESP_IDF_VERSION = "v5.5.3" ESP_IDF_TOOLS_INSTALL_DIR = "workspace" +RUST_BACKTRACE = "1" # Uncomment this if you have moved the target dir by setting CARGO_TARGET_DIR or similar. # Required for now due to embuild limitations. diff --git a/Cargo.toml b/Cargo.toml index f640a0e..95fd777 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,5 +53,7 @@ time = { version = "0.3.55", features = ["default", "serde"] } embassy-time = { version = "0.5", features = ["generic-queue-8"] } +paste = "1.0.15" + [build-dependencies] embuild = "0.33" diff --git a/pin_allocations.txt b/pin_allocations.txt index 845b82c..2beab1d 100644 --- a/pin_allocations.txt +++ b/pin_allocations.txt @@ -9,7 +9,7 @@ 8: I2C SCL 9: Onboard button 10: I2C SDA -18: Stepper DIR -19: Stepper STEP -20: Stepper UART RX -21: Stepper UART TX +18: Stepper UART RX + TX +19: +20: Stepper DIR +21: Stepper STEP diff --git a/sdkconfig.defaults b/sdkconfig.defaults index ca7a8a8..ac30b32 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -10,3 +10,6 @@ CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=4096 # that allocate large stack variables; or better yet - use # `std::thread::Builder::new().stack_size(XXX)` for spawning CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=4096 + +CONFIG_ESP_SYSTEM_USE_FRAME_POINTER=y +FREERTOS_GENERATE_RUN_TIME_STATS=y diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..5dc762f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,26 @@ +use crate::charter::Charter; + +use serde::Deserialize; + +use heapless::String as HeaplessString; + +#[derive(Deserialize, Debug)] +pub struct SystemConfig { + pub wifi_ssid: HeaplessString<32>, + pub wifi_pass: HeaplessString<64>, + pub charter: Charter, + pub use_uart: bool, + pub profiling: bool, +} + +impl Default for SystemConfig { + fn default() -> Self { + SystemConfig { + wifi_ssid: "ESP".try_into().unwrap(), // Will never fail; less than character limit + wifi_pass: "floatware".try_into().unwrap(), // ditto + charter: Default::default(), + use_uart: false, + profiling: false, + } + } +} diff --git a/src/debugging.rs b/src/debugging.rs new file mode 100644 index 0000000..1cd102b --- /dev/null +++ b/src/debugging.rs @@ -0,0 +1,178 @@ +use crate::prelude::*; + +use std::{ + future::{poll_fn, Future}, + pin::pin, + time::{Duration, Instant}, + ffi::CStr, + ptr::null_mut, + collections::HashMap, + sync::{RwLock} +}; + +// I don't know why my IDE doesn't break this into multiple lines +use esp_idf_svc::sys::{eTaskState, eTaskState_eBlocked, eTaskState_eDeleted, eTaskState_eInvalid, eTaskState_eReady, eTaskState_eRunning, eTaskState_eSuspended, uxTaskGetNumberOfTasks, uxTaskGetStackHighWaterMark, uxTaskGetSystemState, xTASK_STATUS, xTaskGetCurrentTaskHandle, TaskHandle_t}; + +use serde::Serialize; + +//////////////////////////////////////////////////////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct TaskInfo { + #[serde(skip_serializing)] + pub handle: TaskHandle_t, + pub name: String, + pub task_number: u32, + pub state: TaskState, + pub priority: u32, + pub run_time_counter: u32, + pub stack_high_water_mark: u32, +} + +impl From for TaskInfo { + fn from(task: xTASK_STATUS) -> Self { + Self { + handle: task.xHandle, + name: unsafe { CStr::from_ptr(task.pcTaskName) }.to_str().map_or_default(String::from), + task_number: task.xTaskNumber, + state: task.eCurrentState.into(), + priority: task.uxCurrentPriority, + run_time_counter: task.ulRunTimeCounter, + stack_high_water_mark: task.usStackHighWaterMark, + } + } +} + +#[derive(Debug, Serialize)] +pub enum TaskState { + Running, + Ready, + Blocked, + Suspended, + Deleted, + Invalid +} + +impl From for TaskState { + fn from(value: eTaskState) -> Self { + #[allow(non_upper_case_globals)] + match value { + eTaskState_eRunning => Self::Running, + eTaskState_eReady => Self::Ready, + eTaskState_eBlocked => Self::Blocked, + eTaskState_eSuspended => Self::Suspended, + eTaskState_eDeleted => Self::Deleted, + eTaskState_eInvalid | _ => Self::Invalid, + } + } +} + +//////////////////////////////////////////////////////////////////////////////// + +/// Return the minimum recorded amount of stack space (in bytes) remaining for the +/// provided task handle, or the caller thread if null. +#[inline] pub fn high_water_mark(handle: TaskHandle_t) -> u32 { + unsafe { uxTaskGetStackHighWaterMark(handle) } +} + +/// Return the minimum recorded amount of stack space (in bytes) remaining for the +/// caller thread. Don't call this from an interrupt. +#[inline] pub fn high_water_mark_caller() -> u32 { high_water_mark(null_mut()) } + +/// Returns a list of [xTASK_STATUS] objects, one for each existing FreeRTOS task. +/// +/// This function takes a long time to call. +/// +/// Note that in the extremely unlikely circumstance that a task is created between +/// the time the number of tasks is measured and the vector is populated, the +/// underlying C function will error and this function will return an empty vector. +fn get_task_status_list() -> Vec { + let num_tasks = /* not */ unsafe { uxTaskGetNumberOfTasks() }; + + let mut buf = Vec::with_capacity(num_tasks as _); + + unsafe { + let populated = uxTaskGetSystemState(buf.as_mut_ptr(), num_tasks, null_mut()); + buf.set_len(populated as _); + } + buf +} + +pub fn get_tasks() -> impl Iterator { + get_task_status_list().into_iter().map(TaskInfo::from) +} + +fn print_task_info() { + info!("{:#?}", get_tasks().collect::>().as_slice()); +} + +//////////////////////////////////////////////////////////////////////////////// + +#[derive(Default, Debug)] +pub struct FutureStats { + polls: u32, + total_time: Duration, + longest_time: Duration, +} + +pub type ProfilingData = HashMap<&'static str, RwLock>; + +static mut PROFILING_DATA: Option = None; + +/// If profiling is enabled, return a reference to the profiling data map. +pub fn get_profiling_data() -> Option<&'static mut ProfilingData> { + unsafe { (&raw mut PROFILING_DATA).as_mut_unchecked()}.as_mut() +} + +/// Enable profiling by populating the profiling data hashmap. +pub fn enable_profiling() { + unsafe { + if let None = PROFILING_DATA { + PROFILING_DATA = Some(Default::default()); + } + } +} + +/// Wrap polling calls to the passed future and profile how long they take. +/// +/// If the provided stats struct is None, then the provided future is transparently +/// executed and no profiling will be performed. +pub async fn profile(future: F, stats: Option<&mut RwLock>) -> F::Output { + if let Some(stats) = stats { + // Profiling enabled + let mut future = pin!(future); + + poll_fn(|cx| { + let start = Instant::now(); + + let result = future.as_mut().poll(cx); + + let elapsed = start.elapsed(); + + let current = stats.get_mut().expect("Profiling stats poisoned"); + + current.polls = current.polls.saturating_add(1); + current.total_time = current.total_time.saturating_add(elapsed); + current.longest_time = current.longest_time.max(elapsed); + + result + }).await + } else { + // Profiling disabled + future.await + } + +} + +/// Wrap the passed future in a profiler and return it. If profiling is not enabled, +/// then the future will be executed normally. +#[macro_export] +macro_rules! profile { + ($name:ident($($arg:tt)*)) => {{ + let stats = crate::debugging::get_profiling_data().and_then(|map| + map.try_insert(stringify!($name), Default::default()).ok() + ); + + crate::debugging::profile($name($($arg)*), stats) + }}; +} diff --git a/src/floatware.rs b/src/floatware.rs index ef77290..726ca40 100644 --- a/src/floatware.rs +++ b/src/floatware.rs @@ -1,30 +1,29 @@ -use crate::{ - prelude::*, - tasks::{ - button::boot_button_pressed_task, - charter::{ - depth_control_task, - depth_target_update_task - }, - i2c::initialize_i2c_thread, - leak_detection::leak_detection_task, - led::{led_cycle_task, led_selection_task}, - power_measurement::power_measurement_task, - sd_card::{ - mount_sd_card, - read_config_from_sd, - sd_logging_task, - setup_sd_card - }, - shutdown::{ - shutdown_task, - Shutdown, - }, - status::status_publishing_task, - stepper_controller::stepper_control_task, - *, +use crate::{prelude::*, tasks::{ + button::boot_button_pressed_task, + charter::{ + depth_control_task, + depth_target_update_task }, -}; + i2c::initialize_i2c_thread, + leak_detection::leak_detection_task, + led::{led_cycle_task, led_selection_task}, + power_measurement::power_measurement_task, + sd_card::{ + mount_sd_card, + read_config_from_sd, + sd_logging_task, + setup_sd_card + }, + shutdown::{ + shutdown_task, + Shutdown, + }, + status::status_publishing_task, + stepper_controller::stepper_control_task, + *, +}, debugging::{ProfilingData}, profile}; + +use std::ptr::{read_volatile, write_volatile}; use esp_idf_svc::{ eventloop::EspSystemEventLoop, @@ -34,12 +33,11 @@ use esp_idf_svc::{ }, http::server::EspHttpServer, nvs::EspDefaultNvsPartition, - sys::{uxTaskGetStackHighWaterMark2, xTaskGetCurrentTaskHandle}, timer::EspTaskTimerService, }; use ws2812_esp32_rmt_driver::{driver::color::LedPixelColorGrbw32, LedPixelEsp32Rmt, RGB8}; - +use crate::debugging::enable_profiling; //////////////////////////////////////////////////////////////////////////////// /// Macro that gets a receiver from a watch. There is a cap to the number of @@ -48,24 +46,16 @@ use ws2812_esp32_rmt_driver::{driver::color::LedPixelColorGrbw32, LedPixelEsp32R macro_rules! wrecv { ($channel:expr) => { ($channel).receiver() - .expect(concat!("Must increase max ", stringify!($channel), " receiver count")) + .ok_or(AnyhowError::msg(concat!("Must increase max ", stringify!($channel), " receiver count")))? }; } + macro_rules! take { ($obj:ident) => { $obj::take().map_err(damn!(concat!("Unable to acquire ", stringify!($obj))))? }; } -// /// Creates an [EspAsyncTimer][esp_idf_svc::timer::EspAsyncTimer] using a provided -// /// [EspTaskTimerService]. -// macro_rules! create_timer { -// ($service:expr) => { -// ($service).timer_async() -// .map_err(damn!(concat!("Failed to create timer at ", file!(), ":", line!())))? -// }; -// } - // These are message channels that are shared across multiple threads. They need to // be `static` so we can be assured they will always exist for all threads to make // use of. @@ -97,7 +87,7 @@ pub async fn float_thread() -> Void { // Create non-static communication channels let power_measurement_request_channel = PowerMeasurementRequestUnsplitChannel::new(); - let status_channel = StatusUnsplitWatch::new(); + let status_channel = SystemStatusUnsplitWatch::new(); let led_state_channel = LedStateSignal::new(); let stepper_state_channel = StepperStateSignal::new(); let button_led_signal = LedColorSignal::new(); @@ -136,6 +126,8 @@ pub async fn float_thread() -> Void { info!("Using {config:#?}"); + if config.profiling { enable_profiling() } + let _wifi = wifi::initialize_wifi( peripherals.modem, &sys_loop, @@ -172,66 +164,65 @@ pub async fn float_thread() -> Void { // Any task returning an error leads to an immediate shutdown let Err(error) = futures::try_join!( // ========== Peripheral Management ========== - depth_target_update_task( + profile!(depth_target_update_task( wrecv!(CHARTER_STATE_CHANNEL), CHARTER_STATE_CHANNEL.sender(), &config.charter, - ), - depth_control_task( + )), + profile!(depth_control_task( wrecv!(CHARTER_STATE_CHANNEL), wrecv!(status_channel), &stepper_state_channel, // writer - ), - stepper_control_task( - peripherals.uart0, - peripherals.pins.gpio20, // RX - peripherals.pins.gpio21, // TX - peripherals.pins.gpio18, // dir - peripherals.pins.gpio19, // step + )), + profile!(stepper_control_task( + config.use_uart.then_some(peripherals.uart1), + peripherals.pins.gpio18.into(), // RX + TX + peripherals.pins.gpio20, // dir + peripherals.pins.gpio21, // step &stepper_state_channel, // reader wrecv!(RELEASE_UART_CHANNEL), - ), - leak_detection_task( + )), + profile!(leak_detection_task( peripherals.pins.gpio3, &SHUTDOWN_CHANNEL, // writer - ), - power_measurement_task( + )), + profile!(power_measurement_task( peripherals.adc1, peripherals.pins.gpio0, // voltage ADC peripherals.pins.gpio1, // current ADC power_measurement_request_channel.receiver(), - ), + )), // ========== I/O ========== - boot_button_pressed_task( + profile!(boot_button_pressed_task( peripherals.pins.gpio9, &button_led_signal, // writer RELEASE_UART_CHANNEL.sender(), // writer - ), - led_selection_task( + )), + profile!(led_selection_task( &led_state_channel, // writer wrecv!(status_channel), &button_led_signal, // reader wrecv!(RELEASE_UART_CHANNEL), - ), - led_cycle_task( + )), + profile!(led_cycle_task( led_driver, &led_state_channel, // reader - ), - sd_logging_task( + )), + profile!(sd_logging_task( wrecv!(status_channel), &sd_fs_handle, - ), + )), // ========== Internal Housekeeping ========== - status_publishing_task( + profile!(status_publishing_task( power_measurement_request_channel.sender(), status_channel.sender(), I2C_CHANNEL.sender(), wrecv!(CHARTER_STATE_CHANNEL), - ), - shutdown_task( + )), + profile!(shutdown_task( &SHUTDOWN_CHANNEL, // reader CHARTER_STATE_CHANNEL.sender() - ), + )), ); // The shutdown task has a unique error that it returns to indicate a normal shutdown @@ -245,11 +236,110 @@ pub async fn float_thread() -> Void { //////////////////////////////////////////////////////////////////////////////// -/// Return the minimum recorded amount of stack space (in bytes) remaining for the -/// calling thread. It should go without saying, but don't call this from an -/// interrupt (I don't know what will happen in that case, but probably nothing -/// good) -fn high_water_mark() -> u32 { unsafe { uxTaskGetStackHighWaterMark2(xTaskGetCurrentTaskHandle()) } } +/// Reset the USB data pins (GPIO 18 & 19) so that they can be used for USB +/// communication. **Requires that those pins be disabled** (i.e. no active GPIO +/// driver). +/// +/// Based on the provided esp-idf C code: +/// ```c +/// #include "soc/soc_caps.h" +/// #include "soc/usb_serial_jtag_reg.h" +/// #include "hal/usb_serial_jtag_ll.h" +/// +/// SET_PERI_REG_MASK(USB_SERIAL_JTAG_CONF0_REG, USB_SERIAL_JTAG_PAD_PULL_OVERRIDE); +/// CLEAR_PERI_REG_MASK(USB_SERIAL_JTAG_CONF0_REG, USB_SERIAL_JTAG_DP_PULLUP); +/// SET_PERI_REG_MASK(USB_SERIAL_JTAG_CONF0_REG, USB_SERIAL_JTAG_DP_PULLDOWN); +/// +/// vTaskDelay(pdMS_TO_TICKS(10)); +/// +/// #if USB_SERIAL_JTAG_LL_EXT_PHY_SUPPORTED +/// usb_serial_jtag_ll_phy_enable_external(false); // Use internal PHY +/// usb_serial_jtag_ll_phy_enable_pad(true); // Enable USB PHY pads +/// #else // USB_SERIAL_JTAG_LL_EXT_PHY_SUPPORTED +/// usb_serial_jtag_ll_phy_set_defaults(); // External PHY not supported. Set default values. +/// #endif // USB_WRAP_LL_EXT_PHY_SUPPORTED +/// +/// CLEAR_PERI_REG_MASK(USB_SERIAL_JTAG_CONF0_REG, USB_SERIAL_JTAG_DP_PULLDOWN); +/// SET_PERI_REG_MASK(USB_SERIAL_JTAG_CONF0_REG, USB_SERIAL_JTAG_DP_PULLUP); +/// CLEAR_PERI_REG_MASK(USB_SERIAL_JTAG_CONF0_REG, USB_SERIAL_JTAG_PAD_PULL_OVERRIDE); +/// ``` +/// Source: https://docs.espressif.com/projects/esp-iot-solution/en/latest/usb/usb_overview/usb_serial_jtag.html#using-usb-serial-jtag-pins-as-normal-gpio +/// +/// esp32c3 does not have `USB_SERIAL_JTAG_LL_EXT_PHY_SUPPORTED`, so we do: +/// ```c +/// FORCE_INLINE_ATTR void usb_serial_jtag_ll_phy_set_defaults(void) { +/// USB_SERIAL_JTAG.conf0.phy_sel = 0; +/// USB_SERIAL_JTAG.conf0.usb_pad_enable = 1; +/// } +/// ``` +/// Source: https://github.com/espressif/esp-idf/blob/v5.5.3/components/hal/esp32c3/include/hal/usb_serial_jtag_ll.h#L194 +/// +/// The data structure we need to modify is: +/// ```c +/// union { +/// struct { +/// uint32_t phy_sel : 1; // 0 +/// uint32_t exchg_pins_override : 1; // 1 +/// uint32_t exchg_pins : 1; // 2 +/// uint32_t vrefh : 2; // 3 +/// uint32_t vrefl : 2; // 5 +/// uint32_t vref_override : 1; // 7 +/// uint32_t pad_pull_override : 1; // 8 +/// uint32_t dp_pullup : 1; // 9 +/// uint32_t dp_pulldown : 1; // 10 +/// uint32_t dm_pullup : 1; // 11 +/// uint32_t dm_pulldown : 1; // 12 +/// uint32_t pullup_value : 1; // 13 +/// uint32_t usb_pad_enable : 1; // 14 +/// uint32_t reserved15 :17; +/// }; +/// uint32_t val; +/// } /*usb_serial_jtag_dev_s.*/conf0; +/// ``` +/// Source: https://github.com/espressif/esp-idf/blob/v5.5.3/components/soc/esp32c3/register/soc/usb_serial_jtag_struct.h#L104 +/// +/// So we set `pad_pull_override`, clear `dp_pullup`, set `dp_pulldown`, wait 10ms, +/// clear `phy_sel`, set `usb_pad_enable`, clear `dp_pulldown`, set `dp_pullup`, clear +/// `pad_pull_override`. +async fn reset_usb_gpio() { + /// https://github.com/espressif/esp-idf/blob/v5.5.3/components/soc/esp32c3/register/soc/reg_base.h#L46 + const DR_REG_USB_SERIAL_JTAG_BASE: usize = 0x60043000; + /// https://github.com/espressif/esp-idf/blob/v5.5.3/components/soc/esp32c3/register/soc/usb_serial_jtag_reg.h#L39 + const USB_SERIAL_JTAG_CONF0_REG: *mut u32 = (DR_REG_USB_SERIAL_JTAG_BASE + 0x18) as _; + + const PHY_SEL : usize = 0; + const PAD_PULL_OVERRIDE : usize = 8; + const DP_PULLUP : usize = 9; + const DP_PULLDOWN : usize = 10; + const USB_PAD_ENABLE : usize = 14; + + fn set() { + unsafe { + let current = read_volatile(USB_SERIAL_JTAG_CONF0_REG); + write_volatile(USB_SERIAL_JTAG_CONF0_REG, current | (1 << BIT)); + } + } + + fn clear() { + unsafe { + let current = read_volatile(USB_SERIAL_JTAG_CONF0_REG); + write_volatile(USB_SERIAL_JTAG_CONF0_REG, current & !(1 << BIT)); + } + } + + set::(); + clear::(); + set::(); + + sleep_ms!(10); + + clear::(); + set::(); + + clear::(); + set::(); + clear::(); +} /// Creates a short-lived input driver to read the state of GPIO9's button. fn boot_button_pressed(input: &mut Gpio9) -> bool { diff --git a/src/main.rs b/src/main.rs index 6ece06f..b774c3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,15 @@ io_error_too_many_open_files, io_error_input_output_error, const_trait_impl, + map_try_insert, )] mod floatware; mod tasks; mod signals; mod prelude; +mod debugging; +mod config; use crate::prelude::*; diff --git a/src/signals.rs b/src/signals.rs index 76618b0..3ebe074 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -1,19 +1,14 @@ -//! - [NoopRawMutex](embassy_sync::blocking_mutex::raw::NoopRawMutex): good only for -//! signals within a single FreeRTOS task (i.e. one thread) -//! -//! - [EspRawMutex]: good for signals between FreeRTOS tasks -//! -//! - [CriticalSectionRawMutex](embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex): -//! suitable for signals accessed by ISRs - use crate::{ prelude::*, tasks::{ i2c::I2cCommand, charter::CharterState, - led::LEDState, + led::LedState, power_measurement::PowerMeasurement, - stepper_controller::StepperState, + stepper_controller::{ + StepperState, + UartRelease + }, shutdown::ShutdownRequest, status::SystemStatus, } @@ -32,51 +27,92 @@ use embassy_sync::{ Watch, Sender as WatchSender, Receiver as WatchReceiver, + }, + blocking_mutex::raw::{ + NoopRawMutex, + CriticalSectionRawMutex } }; -use embassy_sync::blocking_mutex::raw::NoopRawMutex; use futures::channel::oneshot::Sender; + use smart_leds_trait::RGB8; + //////////////////////////////////////////////////////////////////////////////// -// TODO: When finished, convert all the Watches that only have one reader into Signals -// TODO: Make a macro to generate watches +/// Constructs a set of three [Watch] types with the given (or assumed) name: a +/// normal (or "unsplit") watch, a receiver, and a sender. +macro_rules! build_watch { + ($(#[$doc:meta])* $prefix:ident, $slot:ty, $mutex:ty, $recv_count:expr) => { ::paste::paste! { + $(#[$doc])* pub(crate) type [<$prefix UnsplitWatch>] = Watch < $mutex, $slot, $recv_count>; + $(#[$doc])* pub(crate) type [<$prefix Sender>] <'a> = WatchSender <'a, $mutex, $slot, $recv_count>; + $(#[$doc])* pub(crate) type [<$prefix Receiver>]<'a> = WatchReceiver<'a, $mutex, $slot, $recv_count>; + }}; + ($(#[$doc:meta])* $slot:ty, $mutex:ty, $recv_count:expr) => { + ::paste::paste! { + build_watch!( + $(#[$doc])* + [<$slot>], $slot, $mutex, $recv_count); + } + }; +} + +/// Similar as above, but for a channel. Note that the number is the channel buffer +/// queue depth; channels have no receiver limit. +macro_rules! build_channel { + ($(#[$doc:meta])* $prefix:ident, $slot:ty, $mutex:ty, $buf_size:expr) => { ::paste::paste! { + $(#[$doc])* pub(crate) type [<$prefix UnsplitChannel>] = Channel < $mutex, $slot, $buf_size>; + $(#[$doc])* pub(crate) type [<$prefix Sender>] <'a> = ChannelSender <'a, $mutex, $slot, $buf_size>; + $(#[$doc])* pub(crate) type [<$prefix Receiver>]<'a> = ChannelReceiver<'a, $mutex, $slot, $buf_size>; + }}; +} + +/// Only safe for signals used within a single FreeRTOS task. +type SingleTaskMutex = NoopRawMutex; +/// Acceptable for signals accessed by multiple FreeRTOS tasks, but not interrupts. +type MultiTaskMutex = EspRawMutex; +/// Safe to use in interrupts and all places. +type IsrMutex = CriticalSectionRawMutex; + +//////////////////////////////////////////////////////////////////////////////// /// Signal that tells the float-thread to exit. Written from the HTTP `/shutdown`. -pub(crate) type ShutdownSignal = Signal; +pub(crate) type ShutdownSignal = Signal; + /// Used to send commands to the stepper motor. Only used on float thread. -pub(crate) type StepperStateSignal = Signal; +pub(crate) type StepperStateSignal = Signal; + /// Used to send LED color information between tasks. Only used on float thread. -pub(crate) type LedColorSignal = Signal; +pub(crate) type LedColorSignal = Signal; + /// Used to set the current LED cycle. Only used on float thread. -pub(crate) type LedStateSignal = Signal; - -// Written by HTTP `/start_dive` -const CHARTER_STATE_RECEIVERS: usize = 3; -pub(crate) type CharterStateUnsplitWatch = Watch; -pub(crate) type CharterStateReceiver<'a> = WatchReceiver<'a, EspRawMutex, CharterState, CHARTER_STATE_RECEIVERS>; -pub(crate) type CharterStateSender<'a> = WatchSender<'a, EspRawMutex, CharterState, CHARTER_STATE_RECEIVERS>; - -// Written from HTTP thread -const UART_RELEASE_RECEIVERS: usize = 2; -pub(crate) type UartReleaseUnsplitWatch = Watch; -pub(crate) type UartReleaseReceiver<'a> = WatchReceiver<'a, EspRawMutex, (), UART_RELEASE_RECEIVERS>; -pub(crate) type UartReleaseSender<'a> = WatchSender<'a, EspRawMutex, (), UART_RELEASE_RECEIVERS>; - -// Not used outside float thread -const POWER_MEASUREMENT_REQUEST_BUFFER_SIZE: usize = 8; -pub(crate) type PowerMeasurementRequestUnsplitChannel = Channel, POWER_MEASUREMENT_REQUEST_BUFFER_SIZE>; -pub(crate) type PowerMeasurementRequestReceiver<'a> = ChannelReceiver<'a, NoopRawMutex, Sender, POWER_MEASUREMENT_REQUEST_BUFFER_SIZE>; -pub(crate) type PowerMeasurementRequestSender<'a> = ChannelSender<'a, NoopRawMutex, Sender, POWER_MEASUREMENT_REQUEST_BUFFER_SIZE>; - -// Will be used in http thread -const STATUS_RECEIVERS: usize = 2; -pub(crate) type StatusUnsplitWatch = Watch; -pub(crate) type StatusReceiver<'a> = WatchReceiver<'a, EspRawMutex, SystemStatus, STATUS_RECEIVERS>; -pub(crate) type StatusSender<'a> = WatchSender<'a, EspRawMutex, SystemStatus, STATUS_RECEIVERS>; - -// Used on I2C thread, obviously. -const I2C_BUFFER_SIZE: usize = 8; -pub(crate) type I2cUnsplitChannel = Channel; -pub(crate) type I2cSender<'a> = ChannelSender<'a, EspRawMutex, I2cCommand, I2C_BUFFER_SIZE>; -pub(crate) type I2cReceiver<'a> = ChannelReceiver<'a, EspRawMutex, I2cCommand, I2C_BUFFER_SIZE>; +pub(crate) type LedStateSignal = Signal; + +build_watch!( +/// Keeps track of the current [CharterState]; what we are currently doing on the +/// dive. Empty means no dive has been started yet. +/// +/// Written by HTTP `/start_dive`. + CharterState, MultiTaskMutex, 3 +); + +build_watch!( +/// Used to communicate the signal for the UART controller to drop its UART driver. +/// Not sure if this is actually needed though. +/// Written from HTTP `/release_uart` + UartRelease, MultiTaskMutex, 2 +); + +build_watch!( +/// Written from HTTP thread + SystemStatus, MultiTaskMutex, 3 +); + +build_channel!( +/// Not used outside float thread + PowerMeasurementRequest, Sender, SingleTaskMutex, 8 +); + +build_channel!( +/// Used on I2C thread, obviously. + I2c, I2cCommand, MultiTaskMutex, 8 +); diff --git a/src/tasks/button.rs b/src/tasks/button.rs index 83a00d1..cc4680f 100644 --- a/src/tasks/button.rs +++ b/src/tasks/button.rs @@ -10,7 +10,7 @@ use esp_idf_svc::hal::gpio::{Gpio9, PinDriver, Pull}; use embassy_time::{Duration, WithTimeout}; use smart_leds_trait::RGB8; - +use crate::tasks::stepper_controller::UartRelease; //////////////////////////////////////////////////////////////////////////////// #[derive(Copy, Clone)] @@ -65,6 +65,7 @@ pub async fn boot_button_pressed_task( for (index, Con(duration, led)) in DURATIONS.iter().enumerate() { // Set LED to indicate what will happen if you release the button led_color_signal.signal(*led); + info!("Next action in {} ms", duration.as_millis()); if let Ok(result) = driver.wait_for_rising_edge() @@ -74,13 +75,18 @@ pub async fn boot_button_pressed_task( // Button released result.map_err(damn!("Unpress await failed"))?; match index { - 0 => {/* [0, 200) -> nothing; guard */}, + 0 => { + // [0, 200) -> nothing; guard + info!("Unpress 0"); + }, 1 => { // [200, 3000) -> TBD + info!("Unpress 1"); }, 2 => { // [3000, 6000) -> release UART - release_uart_signal.send(()); + info!("Unpress 2"); + release_uart_signal.send(UartRelease::Requested); }, _ => unreachable!(), } @@ -88,8 +94,8 @@ pub async fn boot_button_pressed_task( } // else: timed out, go to next } // Went past last item (or broke after action); overflow/reset to doing nothing - led_color_signal.reset(); + info!("Button overflow"); // Note that signal reset != none. When it is none, the LED // will (probably) flash, as opposed to not changing at all } diff --git a/src/tasks/charter.rs b/src/tasks/charter.rs index b7bdeb2..c626f7d 100644 --- a/src/tasks/charter.rs +++ b/src/tasks/charter.rs @@ -11,7 +11,7 @@ use std::{ }; use embassy_time::WithTimeout; - +use crate::tasks::stepper_controller::StepperState; //////////////////////////////////////////////////////////////////////////////// /// TODO based on I2C depth sensor driver code @@ -113,13 +113,14 @@ pub async fn depth_target_update_task( charter_index, (target_start_time, target_depth) ) in charter.iter().enumerate() { // Determine how much time we need to wait until `start_time` has passed since `start` - let time_since_start = Instant::now() - start; + let time_since_start = start.elapsed(); // We need to wait another (start_time - time_since_start) if sleep_and_check_aborted( &mut charter_state_receiver, target_start_time.saturating_sub(time_since_start) ).await.is_some() { + info!("Depth target task: aborting charter"); // We are aborting, stop setting new charter states continue 'enclosing; // Depth control has also received this signal so there's no need to do anything @@ -137,21 +138,6 @@ pub async fn depth_target_update_task( // into the sender from a different thread. However, it is not currently possible for // that to happen, given the current architecture of the firmware. - // if { - // let aborted = RefCell::new(false); - // charter_state_sender.send_modify(|charter_state| { - // if let Some(CharterState::Aborted { reason }) = charter_state { - // error!("Charter aborted: {reason}"); - // aborted.replace(true); - // } else { - // *charter_state = Some(CharterState::InProgress { - // charter_index, target_depth: *target_depth, charter_size - // }); - // } - // }); - // aborted.take() - // } { continue 'enclosing; } - charter_state_sender.send(CharterState::InProgress { charter_index, target_depth: *target_depth, charter_size }); @@ -167,7 +153,7 @@ pub async fn depth_target_update_task( /// the current depth closer to the target. pub async fn depth_control_task( mut charter_state_receiver: CharterStateReceiver<'_>, - mut status_receiver: StatusReceiver<'_>, + mut status_receiver: SystemStatusReceiver<'_>, // i2c_sender: I2cSender<'_>, stepper_state_channel: &StepperStateSignal ) -> Void { @@ -194,7 +180,7 @@ pub async fn depth_control_task( charter_state_receiver.changed_and(charter_state_predicate).await }; - info!("Got state: {:?}", state); + info!("Depth control: got state: {:?}", state); match state { CharterState::InProgress { target_depth, .. } => { @@ -202,6 +188,14 @@ pub async fn depth_control_task( let current_depth = status_receiver.get().await.depth; + stepper_state_channel.signal( + if current_depth > target_depth { + StepperState::WeAreTooLow + } else { + StepperState::WeAreTooHigh + } + ); + // TODO: adjust stepper to get closer to target, then sleep or something } CharterState::Aborted { reason: AbortReason::Shutdown } => { @@ -245,17 +239,3 @@ pub async fn depth_control_task( Some(reason) } else { None } } - -// #[inline] async fn sleep_and_check_aborted( -// charter_state_receiver: &mut CharterStateReceiver<'_>, -// duration: Duration, -// ) -> Option { -// charter_state_receiver.changed_and( -// |state| matches!(state, CharterState::Aborted { .. }) -// ).with_timeout( -// duration.try_into().unwrap() // Will fail if duration > 584542 years -// ).await.ok().map(|state| if let CharterState::Aborted { reason } = state { -// error!("Charter aborted: {reason}"); -// reason -// } else { unreachable!() }) -// } diff --git a/src/tasks/http.rs b/src/tasks/http.rs index e56eed7..e9b3cc4 100644 --- a/src/tasks/http.rs +++ b/src/tasks/http.rs @@ -44,7 +44,7 @@ use embedded_svc::{ Read } }; - +use crate::tasks::stepper_controller::UartRelease; //////////////////////////////////////////////////////////////////////////////// pub fn initialize_http_server<'server>( @@ -158,7 +158,7 @@ impl<'request> Handler> for PostReleaseUart { type Error = AnyhowError; fn handle(&self, conn: &mut EspHttpConnection) -> Result<(), AnyhowError> { - self.uart_release_sender.send(()); + self.uart_release_sender.send(UartRelease::Requested); reply_204(conn) } diff --git a/src/tasks/led.rs b/src/tasks/led.rs index 462c937..72be174 100644 --- a/src/tasks/led.rs +++ b/src/tasks/led.rs @@ -44,13 +44,15 @@ color_const!( AQUA, 0x00FFAA); const LED_MAXIMUM_SIMULTANEOUS_STATES: usize = 6; +const LED_DIMMING_FACTOR: u8 = 5; + #[derive(Debug, Clone, PartialEq)] -pub struct LEDState { +pub struct LedState { pub size: usize, pub list: [RGB8; LED_MAXIMUM_SIMULTANEOUS_STATES], } -impl LEDState { +impl LedState { pub fn new(slice: &[RGB8]) -> Result { let size = slice.len(); if size <= LED_MAXIMUM_SIMULTANEOUS_STATES { @@ -62,7 +64,7 @@ impl LEDState { } } -impl Index for LEDState { +impl Index for LedState { type Output = RGB8; fn index(&self, index: usize) -> &Self::Output { @@ -79,23 +81,27 @@ pub(crate) async fn led_cycle_task( led_state_signal: &LedStateSignal, ) -> Never { let mut current_led_value = NONE; - let mut current_led_state = LEDState::new(&[])?; + let mut current_led_state = LedState::new(&[])?; let mut led_index = 0_usize; - let mut set_led = |led| { + let mut set_led = |mut led| { if current_led_value != led { current_led_value = led; + led.r >>= LED_DIMMING_FACTOR; + led.g >>= LED_DIMMING_FACTOR; + led.b >>= LED_DIMMING_FACTOR; led_driver.write(std::iter::once(led)) .unwrap_or_else(|error| error!("Error writing LED: {error:#}")); } }; loop { - // Status updates come every ~100ms. + // Status updates come every ~500ms. let new_led_state = led_state_signal.wait().await; // If we get a new state, then reset cycle. if new_led_state != current_led_state { + info!("Got new {new_led_state:?}"); current_led_state = new_led_state; led_index = 0; } @@ -107,30 +113,26 @@ pub(crate) async fn led_cycle_task( continue; } - if led_index >= current_led_state.size { - led_index = 0; - } else { - led_index += 1; - } - set_led(current_led_state[led_index]); + + led_index += 1; + if led_index >= current_led_state.size { led_index = 0; } } } pub(crate) async fn led_selection_task( led_state_signal: &LedStateSignal, - mut status_receiver: StatusReceiver<'_>, + mut status_receiver: SystemStatusReceiver<'_>, button_led_signal: &LedColorSignal, uart_release_receiver: UartReleaseReceiver<'_> ) -> Never { loop { - let status = status_receiver.get().await; + let status = status_receiver.changed().await; let charter_state = status.charter_state; // The LED can be set to blink in a sequence of colors, so different systems can // convey their statuses simultaneously. - // Currently, though, the only thing that does is the charter. let charter_led = match &charter_state { None => AQUA, Some(state) => match state { @@ -149,16 +151,17 @@ pub(crate) async fn led_selection_task( let button_led = button_led_signal.try_take(); let mut led_vec = Vec::with_capacity(LED_MAXIMUM_SIMULTANEOUS_STATES); - led_vec.push(charter_led); if let Some(button_led) = button_led { led_vec.push(button_led); } - + if uart_release_receiver.contains_value() { led_vec.push(PINK); } - led_state_signal.signal(LEDState::new(led_vec.as_slice())?); + led_vec.push(charter_led); + + led_state_signal.signal(LedState::new(led_vec.as_slice())?); } } diff --git a/src/tasks/sd_card.rs b/src/tasks/sd_card.rs index 8233e10..5553508 100644 --- a/src/tasks/sd_card.rs +++ b/src/tasks/sd_card.rs @@ -8,22 +8,20 @@ use crate::{ use std::{ fs::{File, OpenOptions}, - io::Write, - io::ErrorKind + io::ErrorKind, + io::Write }; use esp_idf_svc::{ - fs::{ - fatfs::{ - Fatfs, - config::{FatFsType, FormatConfiguration} - } + fs::fatfs::{ + config::{FatFsType, FormatConfiguration}, + Fatfs }, hal::{ gpio::{ AnyIOPin, - OutputPin, InputPin, + OutputPin, }, sd::{ config::Configuration as SdConfiguration, @@ -43,7 +41,7 @@ use esp_idf_svc::{ use heapless::String as HeaplessString; use serde::Deserialize; - +use crate::config::SystemConfig; //////////////////////////////////////////////////////////////////////////////// macro_rules! open_file { @@ -127,23 +125,6 @@ pub fn mount_sd_card<'a>( //////////////////////////////////////////////////////////////////////////////// -#[derive(Deserialize, Debug)] -pub struct SystemConfig { - pub wifi_ssid: HeaplessString<32>, - pub wifi_pass: HeaplessString<64>, - pub charter: Charter -} - -impl Default for SystemConfig { - fn default() -> Self { - SystemConfig { - wifi_ssid: "ESP".try_into().unwrap(), // Will never fail; less than character limit - wifi_pass: "floatware".try_into().unwrap(), // ditto - charter: Default::default() - } - } -} - pub fn read_config_from_sd() -> Result { SystemConfig::deserialize( &mut serde_json::Deserializer::from_reader( @@ -160,7 +141,7 @@ pub fn read_config_from_sd() -> Result { /// **Blocks entire FreeRTOS thread on write!** If the write operations are /// expensive, it may be worthwhile to consider creating an I/O thread like I2C. pub async fn sd_logging_task( - mut status_receiver: StatusReceiver<'_>, + mut status_receiver: SystemStatusReceiver<'_>, fs_handle: &Option>, ) -> Void { // We don't actually need the fs handle in order to write to the filesystem, diff --git a/src/tasks/status.rs b/src/tasks/status.rs index c541406..d95b348 100644 --- a/src/tasks/status.rs +++ b/src/tasks/status.rs @@ -7,7 +7,12 @@ use crate::{get_time, prelude::*, tasks::{ power_measurement::PowerMeasurement, }, TimeContainer}; -use std::fmt::{Debug, Display}; +use std::fmt::{ + Debug, + Display, + Error as FmtError, + Formatter, +}; use futures::{ channel::oneshot::channel, @@ -16,7 +21,7 @@ use futures::{ //////////////////////////////////////////////////////////////// -const SNAPSHOT_INTERVAL_MS: u64 = 100; +const SNAPSHOT_INTERVAL_MS: u64 = 500; #[derive(Debug, Clone)] pub struct SystemStatus { @@ -33,7 +38,7 @@ impl SystemStatus { } impl Display for SystemStatus { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), FmtError> { fmt.write_fmt(format_args!( "[{}] depth: {}; voltage: {}; current: {}; charter state: ", self.timestamp, self.depth, self.power_measurement.voltage, @@ -54,7 +59,7 @@ impl Display for SystemStatus { /// query the hardware themselves. pub async fn status_publishing_task( power_measurement_request_sender: PowerMeasurementRequestSender<'_>, - status_sender: StatusSender<'_>, + status_sender: SystemStatusSender<'_>, i2c_sender: I2cSender<'_>, mut charter_state_receiver: CharterStateReceiver<'_>, ) -> Never { diff --git a/src/tasks/stepper_controller.rs b/src/tasks/stepper_controller.rs index ce78e33..8186bd2 100644 --- a/src/tasks/stepper_controller.rs +++ b/src/tasks/stepper_controller.rs @@ -1,10 +1,10 @@ use crate::prelude::*; use esp_idf_svc::{ + hal::gpio::Pin, hal::{ gpio::{ AnyIOPin, - InputPin, OutputPin, PinDriver }, @@ -13,37 +13,85 @@ use esp_idf_svc::{ AsyncUartDriver, Uart }, - } + }, + sys::{esp, gpio_mode_t_GPIO_MODE_INPUT_OUTPUT_OD, gpio_num_t, gpio_pulldown_dis, gpio_pullup_en, gpio_set_direction, gpio_set_level} }; use futures::future::{select, Either}; //////////////////////////////////////////////////////////////////////////////// -pub type StepperState = (/* TODO */); - /// TODO +#[derive(Debug)] +pub enum StepperState { + WeAreTooHigh, + WeAreTooLow, +} + +macro_rules! esp_unsafe_chk { + ($invocation:expr, $($args:tt)+) => { + if let Err(error) = esp!( + unsafe { $invocation } + ) { + warn!($($args)+, error); + return None; + } + }; +} + pub async fn stepper_control_task( - uart_controller: impl Uart, - rx_pin: impl InputPin, - tx_pin: impl OutputPin, + maybe_uart: Option, + mut uart_pin: AnyIOPin<'_>, dir_pin: impl OutputPin, step_pin: impl OutputPin, stepper_state_channel: &StepperStateSignal, mut release_uart_signal: UartReleaseReceiver<'_>, ) -> Void { + //! TODO + //! This is a single-wire UART setup, so we need to be careful to avoid contention. + //! TODO write down the really important constraints we need to obey to avoid physical damage + // When the UART controller assumes control of the UART pins, it // clobbers USB serial communication, including flashing & console. // So, in order to make reflashing easier, we need to have a way to drop // the UART controller and let it release its associated pins. So when // we receive a release UART signal, we must return. + let uart_driver = maybe_uart.and_then(|uart| { + let pin = uart_pin.pin() as gpio_num_t; + + // OD high mode is hi-Z ("disabled"), which is what we want before initializing driver + esp_unsafe_chk!( + gpio_set_level(pin, 1), + "Unable to set UART pin high: {:?}" + ); - let uart_driver = AsyncUartDriver::new( - uart_controller, tx_pin, rx_pin, - None::, None::, // CTS & RTS pins, which we don't use - &UartConfig { - ..Default::default() // TODO - }).map_err(damn!("Failed to initialize UART driver"))?; + esp_unsafe_chk!( + gpio_set_direction(pin, gpio_mode_t_GPIO_MODE_INPUT_OUTPUT_OD), + "Failed to configure UART pin as OD: {:?}" + ); + + esp_unsafe_chk!( + gpio_pulldown_dis(pin), + "Failed to disable UART pulldown: {:?}" + ); + + esp_unsafe_chk!( + gpio_pullup_en(pin), + "Failed to enable UART pullup: {:?}" + ); + + let rtx_pin_clone = unsafe { + ((&mut uart_pin) as *mut AnyIOPin).as_mut_unchecked().reborrow() + }; + + AsyncUartDriver::new( + uart, uart_pin, rtx_pin_clone, // single pin for RX & TX + None::, None::, // CTS & RTS pins, which we don't use + &UartConfig { + ..Default::default() // TODO + } + ).map_err(|_| warn!("Failed to initialize UART driver")).ok() + }); let dir_driver = PinDriver::output(dir_pin) .map_err(damn!("Failed to initialize stepper DIR pin"))?; @@ -52,10 +100,24 @@ pub async fn stepper_control_task( loop { // Concurrently await stepper commands or a UART release signal - let future = select(stepper_state_channel.wait(), release_uart_signal.get()).await; + let either = select( + stepper_state_channel.wait(), + release_uart_signal.get() // safe to call get() because it terminates the loop + ).await; // Pull out stepper state if we get it, otherwise we got the UART release signal so we need - let stepper_state = if let Either::Left((s, _)) = future { s } else { return Ok(()) }; + let stepper_state = if let Either::Left((s, _)) = either { s } else { + info!("Dropping uart"); + if let Some(uart_controller) = uart_driver { drop(uart_controller) } + return Ok(()); + }; + + info!("Stepper state: {:?}", stepper_state); } } + +#[derive(Clone)] +pub enum UartRelease { + Requested, Done +} From 56a0bfab0e3418aa8b9dd73b855e6ae12948bf10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moir=C3=A9?= Date: Fri, 18 Sep 2026 11:20:20 -0700 Subject: [PATCH 3/4] Temporary commit --- Cargo.toml | 4 +- pin_allocations.txt | 8 +- sdkconfig.defaults | 11 ++- src/config.rs | 6 +- src/debugging.rs | 81 +++++++++++++++----- src/floatware.rs | 115 +++++++++++++++++------------ src/main.rs | 18 +++-- src/prelude.rs | 3 + src/signals.rs | 6 -- src/tasks.rs | 1 - src/tasks/button.rs | 5 +- src/tasks/http.rs | 57 +++++++++++++-- src/tasks/i2c.rs | 130 +++++++++++++++++++-------------- src/tasks/power_measurement.rs | 115 ----------------------------- src/tasks/sd_card.rs | 100 +++++++++++++++++++------ src/tasks/status.rs | 18 +++-- time | 4 + 17 files changed, 386 insertions(+), 296 deletions(-) delete mode 100644 src/tasks/power_measurement.rs create mode 100755 time diff --git a/Cargo.toml b/Cargo.toml index 95fd777..d17ae67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,11 +49,13 @@ cfor = "1.1.0" heapless = "0.9.3" # (::esp-idf-svc) -time = { version = "0.3.55", features = ["default", "serde"] } +time = { version = "0.3.55", features = ["default", "serde", "formatting", "macros"] } embassy-time = { version = "0.5", features = ["generic-queue-8"] } paste = "1.0.15" +itertools = "0.13.0" + [build-dependencies] embuild = "0.33" diff --git a/pin_allocations.txt b/pin_allocations.txt index 2beab1d..ff5bd37 100644 --- a/pin_allocations.txt +++ b/pin_allocations.txt @@ -1,9 +1,9 @@ - 0: Voltage measurement - 1: Current measurement + 0: + 1: 2: Onboard ARGB LED 3: Leak detection - 4: SD SPI CLK - 5: SD SPI MOSI + 4: SD SPI MOSI + 5: SD SPI CLK 6: SD SPI MISO 7: SD SPI CS (has onboard LED) 8: I2C SCL diff --git a/sdkconfig.defaults b/sdkconfig.defaults index ac30b32..dc5f054 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -1,6 +1,6 @@ -# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) -# You might have to increase this further if you allocate large stack variables in the main task -CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 +# Because we immediately spawn a new thread, the main thread memory usage is pretty small. +# I believe that it tops out at 1332 bytes +CONFIG_ESP_MAIN_TASK_STACK_SIZE=1400 # Increase a bit these stack sizes as they are also a bit too small by default CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 @@ -11,5 +11,8 @@ CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=4096 # `std::thread::Builder::new().stack_size(XXX)` for spawning CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=4096 +# Note that you need to prepend `CONFIG_` to the names you see in the Kconfig listings + CONFIG_ESP_SYSTEM_USE_FRAME_POINTER=y -FREERTOS_GENERATE_RUN_TIME_STATS=y +CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y +CONFIG_FREERTOS_USE_TRACE_FACILITY=y diff --git a/src/config.rs b/src/config.rs index 5dc762f..54f2bd5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,10 @@ use crate::charter::Charter; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use heapless::String as HeaplessString; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct SystemConfig { pub wifi_ssid: HeaplessString<32>, pub wifi_pass: HeaplessString<64>, @@ -20,7 +20,7 @@ impl Default for SystemConfig { wifi_pass: "floatware".try_into().unwrap(), // ditto charter: Default::default(), use_uart: false, - profiling: false, + profiling: true, } } } diff --git a/src/debugging.rs b/src/debugging.rs index 1cd102b..6afba6b 100644 --- a/src/debugging.rs +++ b/src/debugging.rs @@ -1,17 +1,34 @@ -use crate::prelude::*; +use crate::{ + floatware::get_float_thread_handle, + prelude::*, + tasks::i2c::get_i2c_thread_handle +}; use std::{ + collections::HashMap, + ffi::CStr, future::{poll_fn, Future}, + num::Saturating, pin::pin, - time::{Duration, Instant}, - ffi::CStr, ptr::null_mut, - collections::HashMap, - sync::{RwLock} + sync::RwLock, + time::{Duration, Instant} }; -// I don't know why my IDE doesn't break this into multiple lines -use esp_idf_svc::sys::{eTaskState, eTaskState_eBlocked, eTaskState_eDeleted, eTaskState_eInvalid, eTaskState_eReady, eTaskState_eRunning, eTaskState_eSuspended, uxTaskGetNumberOfTasks, uxTaskGetStackHighWaterMark, uxTaskGetSystemState, xTASK_STATUS, xTaskGetCurrentTaskHandle, TaskHandle_t}; +use esp_idf_svc::sys::{ + eTaskState, + eTaskState_eBlocked, + eTaskState_eDeleted, + eTaskState_eInvalid, + eTaskState_eReady, + eTaskState_eRunning, + eTaskState_eSuspended, + uxTaskGetNumberOfTasks, + uxTaskGetStackHighWaterMark, + uxTaskGetSystemState, + xTASK_STATUS, + TaskHandle_t +}; use serde::Serialize; @@ -31,9 +48,16 @@ pub struct TaskInfo { impl From for TaskInfo { fn from(task: xTASK_STATUS) -> Self { + let name = String::from(match task.xHandle { + h if h == get_float_thread_handle() => "", + h if h == get_i2c_thread_handle () => "", + _ => unsafe { CStr::from_ptr(task.pcTaskName) }.to_str() + .unwrap_or("") + }); + Self { handle: task.xHandle, - name: unsafe { CStr::from_ptr(task.pcTaskName) }.to_str().map_or_default(String::from), + name, task_number: task.xTaskNumber, state: task.eCurrentState.into(), priority: task.uxCurrentPriority, @@ -98,19 +122,25 @@ fn get_task_status_list() -> Vec { buf } -pub fn get_tasks() -> impl Iterator { +pub fn iter_tasks() -> impl Iterator { get_task_status_list().into_iter().map(TaskInfo::from) } +pub fn get_tasks() -> Vec { + let mut vec = iter_tasks().collect::>(); + vec.sort_by_key(|task_info| task_info.task_number); + vec +} + fn print_task_info() { - info!("{:#?}", get_tasks().collect::>().as_slice()); + info!("{:#?}", get_tasks().as_slice()); } //////////////////////////////////////////////////////////////////////////////// -#[derive(Default, Debug)] +#[derive(Default, Debug, Serialize)] pub struct FutureStats { - polls: u32, + polls: Saturating, total_time: Duration, longest_time: Duration, } @@ -120,8 +150,12 @@ pub type ProfilingData = HashMap<&'static str, RwLock>; static mut PROFILING_DATA: Option = None; /// If profiling is enabled, return a reference to the profiling data map. -pub fn get_profiling_data() -> Option<&'static mut ProfilingData> { - unsafe { (&raw mut PROFILING_DATA).as_mut_unchecked()}.as_mut() +#[inline] pub fn get_profiling_data() -> Option<&'static mut ProfilingData> { + unsafe { PROFILING_DATA.as_mut() } +} + +#[inline] fn profiling_enabled() -> bool { + unsafe { PROFILING_DATA.is_some() } } /// Enable profiling by populating the profiling data hashmap. @@ -141,6 +175,7 @@ pub async fn profile(future: F, stats: Option<&mut RwLock(future: F, stats: Option<&mut RwLock { + current.polls += 1; + current.total_time = current.total_time.saturating_add(elapsed); + current.longest_time = current.longest_time.max(elapsed); + } + Err(poison) => { + error!("Profiling lock poisoned: {poison:?}"); + lock_ok = false; + }, + } + } result }).await diff --git a/src/floatware.rs b/src/floatware.rs index 726ca40..22a25d7 100644 --- a/src/floatware.rs +++ b/src/floatware.rs @@ -1,29 +1,39 @@ -use crate::{prelude::*, tasks::{ - button::boot_button_pressed_task, - charter::{ - depth_control_task, - depth_target_update_task +use crate::{ + prelude::*, + tasks::{ + button::boot_button_pressed_task, + charter::{ + depth_control_task, + depth_target_update_task + }, + i2c::initialize_i2c_thread, + leak_detection::leak_detection_task, + led::{led_cycle_task, led_selection_task}, + sd_card::{ + mount_sd_card, + read_config_from_sd, + sd_logging_task, + setup_sd_card + }, + shutdown::{ + shutdown_task, + Shutdown, + }, + status::status_publishing_task, + stepper_controller::stepper_control_task, + *, }, - i2c::initialize_i2c_thread, - leak_detection::leak_detection_task, - led::{led_cycle_task, led_selection_task}, - power_measurement::power_measurement_task, - sd_card::{ - mount_sd_card, - read_config_from_sd, - sd_logging_task, - setup_sd_card + debugging::{ + ProfilingData, + enable_profiling }, - shutdown::{ - shutdown_task, - Shutdown, - }, - status::status_publishing_task, - stepper_controller::stepper_control_task, - *, -}, debugging::{ProfilingData}, profile}; + profile, +}; -use std::ptr::{read_volatile, write_volatile}; +use std::{ + ptr::{read_volatile, write_volatile}, + sync::OnceLock +}; use esp_idf_svc::{ eventloop::EspSystemEventLoop, @@ -31,13 +41,13 @@ use esp_idf_svc::{ gpio::{Gpio9, PinDriver, Pull}, peripherals::Peripherals }, - http::server::EspHttpServer, nvs::EspDefaultNvsPartition, timer::EspTaskTimerService, + sys::{xTaskGetCurrentTaskHandle, TaskHandle_t} }; use ws2812_esp32_rmt_driver::{driver::color::LedPixelColorGrbw32, LedPixelEsp32Rmt, RGB8}; -use crate::debugging::enable_profiling; + //////////////////////////////////////////////////////////////////////////////// /// Macro that gets a receiver from a watch. There is a cap to the number of @@ -56,6 +66,11 @@ macro_rules! take { }; } +static FLOAT_THREAD_HANDLE: OnceLock = OnceLock::new(); + +/// Using this in functions outside the float thread is probably not safe +#[inline] pub fn get_float_thread_handle() -> TaskHandle_t { *FLOAT_THREAD_HANDLE.get().unwrap() as _ } + // These are message channels that are shared across multiple threads. They need to // be `static` so we can be assured they will always exist for all threads to make // use of. @@ -73,11 +88,13 @@ static RELEASE_UART_CHANNEL: UartReleaseUnsplitWatch = UartReleaseUnsplitWatch:: /// Where we actually start setting up everything. This function is responsible for /// spawning the remaining threads, initializing peripherals, and spawning tasks. pub async fn float_thread() -> Void { + FLOAT_THREAD_HANDLE.set(unsafe { xTaskGetCurrentTaskHandle() as usize}).unwrap(/* unreachable */); + //////////////////////////////////////// // Get handles to hardware resources let mut peripherals = take!(Peripherals); - let should_reset = boot_button_pressed(&mut peripherals.pins.gpio9); + let should_reset = boot_button_pressed(&mut peripherals.pins.gpio9).await; let sys_loop = take!(EspSystemEventLoop); let timer_service = EspTaskTimerService::new()?; @@ -86,7 +103,6 @@ pub async fn float_thread() -> Void { //////////////////////////////////////// // Create non-static communication channels - let power_measurement_request_channel = PowerMeasurementRequestUnsplitChannel::new(); let status_channel = SystemStatusUnsplitWatch::new(); let led_state_channel = LedStateSignal::new(); let stepper_state_channel = StepperStateSignal::new(); @@ -97,8 +113,8 @@ pub async fn float_thread() -> Void { let sd_card_driver = setup_sd_card( peripherals.spi2, // SPI1 is very limited - peripherals.pins.gpio4, // SCLK - peripherals.pins.gpio5, // MOSI + peripherals.pins.gpio5, // SCLK + peripherals.pins.gpio4, // MOSI peripherals.pins.gpio6, // MISO peripherals.pins.gpio7, // CS ).map(Some).unwrap_or_else(|error| { @@ -144,7 +160,7 @@ pub async fn float_thread() -> Void { I2C_CHANNEL.receiver() ).map_err(damn!("Failed to initialize I2C thread"))?; - let _http_server: EspHttpServer = http::initialize_http_server( + let _http_server = http::initialize_http_server( &SHUTDOWN_CHANNEL, I2C_CHANNEL.sender(), CHARTER_STATE_CHANNEL.sender(), @@ -174,24 +190,18 @@ pub async fn float_thread() -> Void { wrecv!(status_channel), &stepper_state_channel, // writer )), - profile!(stepper_control_task( - config.use_uart.then_some(peripherals.uart1), - peripherals.pins.gpio18.into(), // RX + TX - peripherals.pins.gpio20, // dir - peripherals.pins.gpio21, // step - &stepper_state_channel, // reader - wrecv!(RELEASE_UART_CHANNEL), - )), + // profile!(stepper_control_task( + // config.use_uart.then_some(peripherals.uart1), + // peripherals.pins.gpio18.into(), // RX + TX + // peripherals.pins.gpio20, // dir + // peripherals.pins.gpio21, // step + // &stepper_state_channel, // reader + // wrecv!(RELEASE_UART_CHANNEL), + // )), profile!(leak_detection_task( peripherals.pins.gpio3, &SHUTDOWN_CHANNEL, // writer )), - profile!(power_measurement_task( - peripherals.adc1, - peripherals.pins.gpio0, // voltage ADC - peripherals.pins.gpio1, // current ADC - power_measurement_request_channel.receiver(), - )), // ========== I/O ========== profile!(boot_button_pressed_task( peripherals.pins.gpio9, @@ -214,7 +224,6 @@ pub async fn float_thread() -> Void { )), // ========== Internal Housekeeping ========== profile!(status_publishing_task( - power_measurement_request_channel.sender(), status_channel.sender(), I2C_CHANNEL.sender(), wrecv!(CHARTER_STATE_CHANNEL), @@ -342,14 +351,26 @@ async fn reset_usb_gpio() { } /// Creates a short-lived input driver to read the state of GPIO9's button. -fn boot_button_pressed(input: &mut Gpio9) -> bool { +async fn boot_button_pressed(input: &mut Gpio9<'_>) -> bool { match PinDriver::input( // We need (temporary) ownership of the pin, so we reborrow it, knowing that the // driver that owns this reference will be dropped at the end of this block. unsafe { input.reborrow() }, Pull::Up ) { - Ok(driver) => driver.is_low(), // Button is active-low + Ok(mut driver) => { + // Button is active-low + if driver.is_low() { + // wait for release before continuing, so that it does not accidentally + // trigger the button task + if let Err(error) = driver.wait_for_high().await { + warn!("Error waiting for boot button to be unpressed: {error:?}"); + } + true + } else { + false + } + } Err(error) => { warn!("Unable to create input driver to check boot button: {error:#?}"); false diff --git a/src/main.rs b/src/main.rs index b774c3f..5da7d11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,8 @@ use std::{ }; use esp_idf_svc::hal::task::block_on; - +use time::error::Format; +use time::macros::format_description; use time::Timestamp; //////////////////////////////////////////////////////////////////////////////// @@ -58,7 +59,7 @@ fn main() { match thread::Builder::new() .name("float-thread".to_string()) - .stack_size(64 * 1024) + .stack_size(16 * 1024) .spawn(move || { // This closure is the synchronous root of the float-thread. block_on(floatware::float_thread()) @@ -66,7 +67,7 @@ fn main() { Err(spawning_err) => error!("FATAL: Unable to spawn float thread: {spawning_err:#?}"), Ok(handle) => match handle.join() { Err(thread_panic) => error!("FATAL: Float thread panicked: {thread_panic:#?}"), - // The return value of initialize is then returned inside Ok(): + // The return value of `float_thread()` is then returned inside Ok(): Ok(Err(error)) => error!("FATAL: Float thread returned error: {error:#?}"), Ok(Ok(())) => info!("Float thread returned normally. Shutting down."), }, @@ -85,7 +86,7 @@ static mut BOOT_TIMESTAMP: Option = None; /// Return either the time since boot, or the current Unix time. pub fn get_time() -> TimeContainer { unsafe { - BOOT_TIMESTAMP.map(Into::into) + BOOT_TIMESTAMP.map(|ts| TimeContainer::Timestamp(ts)) .unwrap_or_else(|| (Instant::now() - BOOT_TIME.unwrap()).into()) } } @@ -115,8 +116,13 @@ impl Display for TimeContainer { match self { Self::SinceBoot(duration) => f.write_fmt(format_args!("{}.{:06}", duration.as_secs(), duration.subsec_micros())), - Self::Timestamp(timestamp) => - Display::fmt(×tamp, f), + Self::Timestamp(timestamp) => { + let res = timestamp.format(format_description!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]")); + f.write_str(match &res { + Ok(s) => s.as_str(), + Err(_) => "