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..d17ae67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,9 +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/ns.py b/ns.py new file mode 100755 index 0000000..2c2c255 --- /dev/null +++ b/ns.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 + +import time +print(time.time_ns() - (time.timezone - time.daylight * 3600) * 1_000_000_000) diff --git a/pin_allocations.txt b/pin_allocations.txt index 845b82c..550aca1 100644 --- a/pin_allocations.txt +++ b/pin_allocations.txt @@ -1,15 +1,15 @@ - 0: Voltage measurement - 1: Current measurement + 0: + 1: Stepper UART RX + TX 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 9: Onboard button 10: I2C SDA -18: Stepper DIR -19: Stepper STEP -20: Stepper UART RX -21: Stepper UART TX +18: [USB] +19: [USB] +20: Stepper DIR +21: Stepper STEP diff --git a/sdkconfig.defaults b/sdkconfig.defaults index ca7a8a8..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 @@ -10,3 +10,9 @@ 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 + +# Note that you need to prepend `CONFIG_` to the names you see in the Kconfig listings + +CONFIG_ESP_SYSTEM_USE_FRAME_POINTER=y +CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y +CONFIG_FREERTOS_USE_TRACE_FACILITY=y diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..f6572a8 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,24 @@ +use crate::charter::Charter; + +use serde::{Deserialize, Serialize}; + +use heapless::String as HeaplessString; + +#[derive(Deserialize, Serialize, Debug)] +pub struct SystemConfig { + pub wifi_ssid: HeaplessString<32>, + pub wifi_pass: HeaplessString<64>, + pub charter: Charter, + 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(), + profiling: false, + } + } +} diff --git a/src/debugging.rs b/src/debugging.rs new file mode 100644 index 0000000..207d890 --- /dev/null +++ b/src/debugging.rs @@ -0,0 +1,227 @@ +//! Some useful functions: +//! esp_get_free_heap_size() +//! esp_get_free_heap_size() +//! esp_get_minimum_free_heap_size() +//! heap_caps_get_total_size(caps) +//! heap_caps_get_free_size(caps) +//! heap_caps_get_minimum_free_size(caps) +//! heap_caps_get_largest_free_block(caps) +//! heap_caps_get_info(&mut multi_heap_info_t, caps) + +use crate::{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, + ptr::null_mut, + sync::RwLock, + time::{Duration, Instant} +}; + +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; + +//////////////////////////////////////////////////////////////////////////////// + +#[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 { + 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, + 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 { + not_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 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().as_slice()); +} + +//////////////////////////////////////////////////////////////////////////////// + +#[derive(Default, Debug, Serialize)] +pub struct FutureStats { + polls: Saturating, + 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. +#[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. +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); + let mut lock_ok = true; + + poll_fn(|cx| { + let start = Instant::now(); + + let result = future.as_mut().poll(cx); + + let elapsed = start.elapsed(); + + if lock_ok { + match stats.get_mut() { + Ok(current) => { + 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 + } 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 ac6839b..40fbf66 100644 --- a/src/floatware.rs +++ b/src/floatware.rs @@ -1,37 +1,39 @@ -use crate::{ - prelude::*, - tasks::{ - *, - charter::{ - depth_target_update_task, - depth_control_task - }, - i2c::initialize_i2c_thread, - leak_detection::leak_detection_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 - }, - button::boot_button_pressed_task, +use crate::{debugging::{ + enable_profiling, + ProfilingData +}, gpio, prelude::*, profile, 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, +}}; + +use std::ptr::{read_volatile, write_volatile}; 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}; @@ -44,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. @@ -81,7 +75,7 @@ pub async fn float_thread() -> Void { // Get handles to hardware resources let mut peripherals = take!(Peripherals); - let should_reset = boot_button_pressed(&mut peripherals.pins.gpio9); + let should_reset = gpio::boot_button_pressed(&mut peripherals.pins.gpio9).await; let sys_loop = take!(EspSystemEventLoop); let timer_service = EspTaskTimerService::new()?; @@ -90,18 +84,18 @@ 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(); //////////////////////////////////////// // Initialize external free-running systems 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| { @@ -129,6 +123,8 @@ pub async fn float_thread() -> Void { info!("Using {config:#?}"); + if config.profiling { enable_profiling() } + let _wifi = wifi::initialize_wifi( peripherals.modem, &sys_loop, @@ -145,7 +141,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(), @@ -163,56 +159,56 @@ 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() - ), - depth_target_update_task( + // ========== Peripheral Management ========== + 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 - ), - leak_detection_task( + )), + // profile!(stepper_control_task( + // peripherals.uart1, + // peripherals.pins.gpio1.into(), // RX + TX + // peripherals.pins.gpio20, // dir + // peripherals.pins.gpio21, // step + // &stepper_state_channel, // reader + // )), + profile!(leak_detection_task( peripherals.pins.gpio3, &SHUTDOWN_CHANNEL, // writer - ), - power_measurement_task( - peripherals.adc1, - peripherals.pins.gpio0, // voltage ADC - 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 - ), - status_publishing_task( - power_measurement_request_channel.sender(), - status_channel.sender(), - I2C_CHANNEL.sender(), - wrecv!(CHARTER_STATE_CHANNEL), + )), + // ========== I/O ========== + profile!(boot_button_pressed_task( + peripherals.pins.gpio9, + &button_led_signal, // writer + )), + profile!(led_selection_task( &led_state_channel, // writer - ), - boot_button_pressed_task( - peripherals.pins.gpio9 - ), - sd_logging_task( + wrecv!(status_channel), + &button_led_signal, // reader + )), + profile!(led_cycle_task( + led_driver, + &led_state_channel, // reader + )), + profile!(sd_logging_task( wrecv!(status_channel), &sd_fs_handle, - ) + )), + // ========== Internal Housekeeping ========== + profile!(status_publishing_task( + status_channel.sender(), + I2C_CHANNEL.sender(), + wrecv!(CHARTER_STATE_CHANNEL), + )), + 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 @@ -223,27 +219,3 @@ pub async fn float_thread() -> Void { Err(error) } } - -//////////////////////////////////////////////////////////////////////////////// - -/// 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()) } } - -/// Creates a short-lived input driver to read the state of GPIO9's button. -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(), - Err(error) => { - warn!("Unable to create input driver to check boot button: {error:#?}"); - false - } - } -} diff --git a/src/gpio.rs b/src/gpio.rs new file mode 100644 index 0000000..ca7c17f --- /dev/null +++ b/src/gpio.rs @@ -0,0 +1,138 @@ +use crate::prelude::*; + +use std::ptr::{read_volatile, write_volatile}; + +use esp_idf_svc::hal::gpio::{Gpio9, PinDriver, Pull}; + +/// 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`. +pub 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. +pub 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(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 + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d615e6e..d6c3c0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,32 @@ -#![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, + map_try_insert, +)] mod floatware; mod tasks; mod signals; mod prelude; +mod debugging; +mod config; +mod timekeeping; +mod gpio; -use crate::prelude::*; +use crate::{ + prelude::*, +}; use std::{ thread, - time::{ - Instant, - Duration - }, - fmt::{ - Debug, - Formatter, - Display - } + sync::OnceLock }; -use esp_idf_svc::hal::task::block_on; - -use time::Timestamp; +use esp_idf_svc::{ + hal::task::block_on, + sys::{xTaskGetCurrentTaskHandle, TaskHandle_t} +}; //////////////////////////////////////////////////////////////////////////////// @@ -31,17 +34,14 @@ use time::Timestamp; /// "float-thread" where everything takes place. We do this for a few reasons: /// - Priority: the main thread has a fairly low priority*, and this gives our code /// a higher priority to prevent being potentially stepped on by library threads -/// - Stack size: I'm not sure how to configure the main thread stack size but I'm -/// pretty sure that it's not as simple as it is here. +/// - Stack size: Makes it easier to configure float thread stack size /// - There is no third reason. /// /// All the "actual" initialization code lives in [floatware::float_thread]. /// /// *See https://github.com/esp-rs/esp-idf-svc/blob/master/examples/tls_async.rs#L49 -/// -/// TODO: figure out how much stack memory is needed for the thread. The current is arbitrary. fn main() { - unsafe { BOOT_TIME = Some(Instant::now()); } + timekeeping::register_boot_instant(); // It is necessary to call this function once. Otherwise, some patches to the runtime // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71 @@ -52,74 +52,23 @@ 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. + FLOAT_THREAD_HANDLE.set(/* not */ not_unsafe! { xTaskGetCurrentTaskHandle() } as usize).unwrap(/* unreachable */); block_on(floatware::float_thread()) }) { 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."), }, }; } -//////////////////////////////////////////////////////////////////////////////// - -/// Set once on boot. Always safe to unwrap. -static mut BOOT_TIME: Option = None; - -/// Requires synchronization data sent from the controlling computer via http, so -/// may not be populated. -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) - .unwrap_or_else(|| (Instant::now() - BOOT_TIME.unwrap()).into()) - } -} - -pub fn set_boot_time(now: Timestamp) { - unsafe { - BOOT_TIMESTAMP = Some(now); - } -} - -#[derive(Clone)] -pub enum TimeContainer { - SinceBoot(Duration), - Timestamp(Timestamp), -} - -impl From for TimeContainer { - fn from(value: Timestamp) -> Self { Self::Timestamp(value) } -} +pub static FLOAT_THREAD_HANDLE: OnceLock = OnceLock::new(); -impl From for TimeContainer { - fn from(value: Duration) -> Self { Self::SinceBoot(value) } -} - -impl Display for TimeContainer { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::SinceBoot(duration) => - f.write_fmt(format_args!("{}.{:06}", duration.as_secs(), duration.subsec_micros())), - Self::Timestamp(timestamp) => - Display::fmt(×tamp, f), - } - } -} - -impl Debug for TimeContainer { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::SinceBoot(duration) => duration.fmt(f), - Self::Timestamp(timestamp) => Debug::fmt(timestamp, f), - } - } -} +/// 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 _ } \ No newline at end of file diff --git a/src/prelude.rs b/src/prelude.rs index d5cb63b..876b023 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -10,7 +10,8 @@ use esp_idf_svc::timer::EspAsyncTimer; pub(crate) use { crate::{ signals::*, - damn, sleep, sleep_ms, ret_err, SD_CARD_NAME, sd + tasks::*, + damn, sleep, sleep_ms, ret_err, SD_CARD_NAME, sd, not_unsafe }, anyhow::Error as AnyhowError, log::{debug, info, warn, error}, @@ -29,6 +30,7 @@ pub(crate) type Never = Result; /// Creates a closure that generates an error message and then returns the argument /// wrapped inside an [AnyhowError]. Intended for use with [Result::map_err]. +/// TODO: [anyhow::context] #[macro_export] macro_rules! damn { ($($args:tt)+) => { |err__| { @@ -80,6 +82,8 @@ macro_rules! SD_CARD_NAME { } /// Create a full path, with the SD card's mountpoint, for the provided file path. +/// +/// **Filenames must be all caps.** #[macro_export] macro_rules! sd { ($path:literal) => { concat!("/", SD_CARD_NAME!(), "/", $path) @@ -88,3 +92,13 @@ macro_rules! SD_CARD_NAME { format!(concat!("/", SD_CARD_NAME!(), "/", "{}"), $path) } } + +/// I like to be able to find all the `unsafe` code in my codebase by searching +/// through it. However, all the esp-idf C functions are `unsafe`, despite many of +/// them being safe to call. For this reason, I have this macro, which I simply use +/// to indicate that an `unsafe` block doesn't actually do anything unsafe. +/// +/// If you are actually using the faculties provided by `unsafe`, do not use this! +#[macro_export] macro_rules! not_unsafe { + {$($code:tt)*} => { unsafe { $($code)* } }; + } diff --git a/src/signals.rs b/src/signals.rs index 0ad1846..4547205 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -1,19 +1,12 @@ -//! - [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, - power_measurement::PowerMeasurement, - stepper_controller::StepperState, + led::LedState, + stepper_controller::{ + StepperState, + }, shutdown::ShutdownRequest, status::SystemStatus, } @@ -32,77 +25,79 @@ use embassy_sync::{ Watch, Sender as WatchSender, Receiver as WatchReceiver, + }, + blocking_mutex::raw::{ + NoopRawMutex, + CriticalSectionRawMutex } }; -use futures::channel::oneshot::Sender; +use smart_leds_trait::RGB8; //////////////////////////////////////////////////////////////////////////////// -// TODO: When finished, convert all the Watches that only have one reader into Signals - -/// 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. -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>; - - -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; +/// 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; -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. +//////////////////////////////////////////////////////////////////////////////// -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>; +/// Signal that tells the float-thread to exit. Written from the HTTP `/shutdown`. +pub(crate) type ShutdownSignal = Signal; -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 to send commands to the stepper motor. Only used on float thread. +pub(crate) type StepperStateSignal = Signal; -// 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>; +/// Used to send LED color information between tasks. Only used on float thread. +pub(crate) type LedColorSignal = Signal; -// ==== I2C Command Channels ==== +/// Used to set the current LED cycle. Only used on float thread. +pub(crate) type LedStateSignal = Signal; -const I2C_BUFFER_SIZE: usize = 8; -/// If this signature is updated, [i2c::i2c_thread] must also be updated. +build_watch!( +/// Keeps track of the current [CharterState]; what we are currently doing on the +/// dive. Empty means no dive has been started yet. /// -/// 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>; +/// Written by HTTP `/start_dive`. + CharterState, MultiTaskMutex, 3 +); + +build_watch!( +/// Written from HTTP thread + SystemStatus, MultiTaskMutex, 3 +); + +build_channel!( +/// Used on I2C thread, obviously. + I2c, I2cCommand, MultiTaskMutex, 8 +); diff --git a/src/tasks/button.rs b/src/tasks/button.rs index 7b02b26..020de2c 100644 --- a/src/tasks/button.rs +++ b/src/tasks/button.rs @@ -1,23 +1,107 @@ -use crate::prelude::*; +//! 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::*, + debugging::{get_tasks} +}; 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::YELLOW), + // Eject SD card? + (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, ) -> 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); + info!("Next action in {} ms", duration.as_millis()); - 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 + info!("Unpress 0"); + }, + 1 => { + // [200, 3000) -> TBD + info!("Unpress 1"); - info!("Boot button {}pressed", if state { "" } else { "un" }); + info!("{:#?}", get_tasks()); + }, + 2 => { + // [3000, 6000) -> eject SD card + info!("Unpress 2"); + // TODO + }, + _ => unreachable!(), + } + break; + } // 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..b2c4fa5 100644 --- a/src/tasks/charter.rs +++ b/src/tasks/charter.rs @@ -1,27 +1,27 @@ use crate::{ - prelude::* + prelude::*, + tasks::{ + stepper_controller::StepperState, + i2c::pressure_sensor::Depth + } }; use std::{ + fmt::{Display, Formatter, Result as FmtResult}, time::{ Duration, Instant - }, - fmt::{Display, Formatter, Result as FmtResult} + } }; use embassy_time::WithTimeout; //////////////////////////////////////////////////////////////////////////////// -/// TODO based on I2C depth sensor driver code -pub type Depth = f64; -/// First argument is the amount of time after starting the dive that we should be -/// at this [Depth]. The duration that we are at that depth is the following entry's -/// [Duration] minus this one. -/// -/// WARNING: durations of more than 584542 years may lead to an error in the sleep code. -pub type DepthEntry = (Duration, Depth); +/// First argument is the amount of time, in milliseconds, after starting the dive +/// that we should be at this [Depth]. The duration that we are at that depth is the +/// following entry's [Duration] minus this one. +pub type DepthEntry = (u32, Depth); /// Maps the time after starting the descent, to a target depth. /// Note that the last entry will, for implementation reasons, always be effectively @@ -49,10 +49,16 @@ pub enum CharterState { impl Display for CharterState { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { - Self::StartRequested => f.write_str("StartRequested"), - Self::InProgress { charter_index, target_depth, charter_size } =>f.write_fmt(format_args!("InProgress {{ {charter_index}/{charter_size}: {target_depth} }}")), - Self::Completed => f.write_str("Completed"), - Self::Aborted { reason } => f.write_fmt(format_args!("Aborted {{ {reason} }}")) + Self::StartRequested => + f.write_str("StartRequested"), + Self::InProgress { charter_index, target_depth, charter_size } => + f.write_fmt(format_args!( + "InProgress {{ {charter_index}/{}: {target_depth} }}", charter_size - 1 + )), + Self::Completed => + f.write_str("Completed"), + Self::Aborted { reason } => + f.write_fmt(format_args!("Aborted {{ {reason} }}")) } } } @@ -67,7 +73,8 @@ impl Display for AbortReason { fn fmt<'a>(&self, f: &mut Formatter<'_>) -> FmtResult { match self { AbortReason::CharterStateError { offending_charter_index } => { - f.write_fmt(format_args!("Invalid charter state: InProgress({offending_charter_index}) at beginning of depth_target_update_task")) + f.write_fmt(format_args!( + "Invalid charter state: InProgress({offending_charter_index}) at beginning of depth_target_update_task")) } AbortReason::Shutdown => f.write_str("Shutdown") } @@ -113,20 +120,24 @@ 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) + let wait_time = Duration::from_millis + (*target_start_time as _).saturating_sub(time_since_start); + if sleep_and_check_aborted( &mut charter_state_receiver, - target_start_time.saturating_sub(time_since_start) + wait_time ).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 } info!("New target: {target_depth:?}, {} seconds after start", - target_start_time.as_secs()); + target_start_time / 1000); // Because the esp32c3 is single-core, we avoid a race condition, wherein an Aborted // value is inserted into the receiver between where we check for one above and where @@ -137,21 +148,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 +163,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 +190,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, .. } => { @@ -203,6 +199,16 @@ pub async fn depth_control_task( let current_depth = status_receiver.get().await.depth; // TODO: adjust stepper to get closer to target, then sleep or something + + stepper_state_channel.signal( + if current_depth > target_depth { + StepperState::WeAreTooLow + } else { + StepperState::WeAreTooHigh + } + ); + + sleep_and_check_aborted(&mut charter_state_receiver, Duration::from_millis(100)).await; } CharterState::Aborted { reason: AbortReason::Shutdown } => { go_to_surface().await; @@ -245,17 +251,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 3e9cc10..efcee82 100644 --- a/src/tasks/http.rs +++ b/src/tasks/http.rs @@ -15,62 +15,70 @@ //! TODO: once I am done testing things with JSON typed into curl, port to MessagePack use crate::{ + debugging::{ + get_profiling_data, + get_tasks, + }, prelude::*, - set_boot_time, tasks::{ - i2c::I2cCommand, charter::CharterState, - shutdown::ShutdownRequest - } + i2c::I2cCommand, + shutdown::ShutdownRequest, + }, + timekeeping::{get_on_duration, set_boot_timestamp} }; +use std::str::from_utf8; + use esp_idf_svc::{ + hal::task::block_on, http::server::{ EspHttpConnection, EspHttpServer }, - io::EspIOError, - hal::task::block_on + io::EspIOError }; use embedded_svc::{ http::server::{ - CompositeHandler, Handler, Middleware, Connection + CompositeHandler, Connection, Handler, Middleware }, http::{ Headers, Method }, - io::{ - Read - } + io::Read }; +use time::Timestamp; + //////////////////////////////////////////////////////////////////////////////// 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>, ) -> 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("/profiling", Method:: Get, pep(GetProfiling))? + .handler("/thread_info", Method:: Get, pep(GetThreadInfo))? + .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))? ; + info!("Server object successfully created"); + Ok(server) } //////////////////////////////////////////////////////////////////////////////// -// TODO: Maybe make a macro that generates these structs? - -/// Noop req. We don't use PlainErrorPage to make handling the connection faster. +/// Noop request. We don't use PlainErrorPage to make handling the connection faster. struct GetHeartbeat; impl<'request> Handler> for GetHeartbeat { type Error = AnyhowError; @@ -104,13 +112,57 @@ impl<'request> Handler> for GetStatus { } } +struct GetProfiling; +impl<'request> Handler> for GetProfiling { + type Error = AnyhowError; + + fn handle(&self, conn: &mut EspHttpConnection) -> Result<(), AnyhowError> { + let profiling = match get_profiling_data() { + Some(data) => data, + None => return reply(conn, 400, "Profiling not enabled".into()), + }; + + info!("{profiling:#?}"); + + match serde_json::to_string_pretty(profiling) { + Ok(json) => reply(conn, 200, json), + Err(error) => reply(conn, 500, error.to_string()), + } + } +} + +struct GetThreadInfo; +impl<'request> Handler> for GetThreadInfo { + type Error = AnyhowError; + + fn handle(&self, conn: &mut EspHttpConnection) -> Result<(), AnyhowError> { + let thread_info= get_tasks(); + + info!("{thread_info:?}"); + + match serde_json::to_string_pretty(&thread_info) { + Ok(json) => reply(conn, 200, json), + Err(error) => reply(conn, 500, error.to_string()), + } + } +} + struct PostTimeSync; impl<'request> Handler> for PostTimeSync { type Error = AnyhowError; fn handle(&self, conn: &mut EspHttpConnection) -> Result<(), AnyhowError> { // The control station will have to account for connection latency - set_boot_time(serde_json::from_slice(read_body(conn)?.as_slice())?); + + let now: Timestamp = Timestamp::from_nanoseconds( + from_utf8( + &read_body(conn)? + )?.parse()? + )?; + + if !set_boot_timestamp(now - get_on_duration()) { + return reply(conn, 409, "Time already set".into()); + } reply_204(conn) } diff --git a/src/tasks/i2c.rs b/src/tasks/i2c.rs index 8c77141..8d2e9b2 100644 --- a/src/tasks/i2c.rs +++ b/src/tasks/i2c.rs @@ -11,17 +11,26 @@ //! transaction completes. The thread is blocked but other threads are able to run. //! Hence, why we make all the magic sauce happen in this thread. -use crate::{ - prelude::*, - tasks::charter::Depth +pub mod pressure_sensor; +pub mod temp_sensor; +pub mod power_sensor; + +use { + power_sensor::*, + pressure_sensor::*, + temp_sensor::* }; +use crate::prelude::*; + use std::{ + sync::OnceLock, thread::{Builder as ThreadBuilder, JoinHandle} }; use esp_idf_svc::{ hal::{ + delay::TickType, gpio::{InputPin, OutputPin}, i2c::{ I2c, @@ -29,67 +38,108 @@ use esp_idf_svc::{ I2cDriver }, task::block_on, - units::Hertz, - delay::TickType + units::Hertz }, - sys::TickType_t, + sys::{ + xTaskGetCurrentTaskHandle, + TaskHandle_t, + TickType_t + } }; -use cfor::cfor; - use futures::channel::oneshot::Sender as OneshotSender; //////////////////////////////////////////////////////////////////////////////// -#[derive(Debug)] -pub struct TempHumidityResponse { - pub celsius: u8, - /// A percentage. - pub relative_humidity: u8 -} - -impl Default for TempHumidityResponse { - fn default() -> Self { - TempHumidityResponse { - celsius: 0, - relative_humidity: 0, +/// TODO: document +/// +macro_rules! make_i2c_commands { + [$( + $name:ident($getter:ident): $response:ident { + $($response_fields:tt)* + } @ $present_field:ident $(+ { + $($status_fields:tt)* + })? + ),+ $(,)?] => { + pub enum I2cCommand { + $( + $name { + response: OneshotSender<$response> + }, + )+ } - } -} - -#[derive(Debug)] -pub struct DepthResponse { - pub depth: Depth, -} - -impl Default for DepthResponse { - fn default() -> Self { - DepthResponse { - depth: 0. + impl I2cCommand { + pub fn name(&self) -> &'static str { + match self { + $( + Self::$name { .. } => stringify!($name), + )+ + } + } + #[inline] async fn handle( + self, + i2c: &mut I2cDriver<'_>, + device_status: &I2cDevicesStatus, + ) -> Result<(), AnyhowError> { + Ok(match self { + $( + Self::$name { response } => if response.send( + if device_status.$present_field { + $getter(i2c).await? + } else { + Default::default() + } + ).is_err() { + // The code that holds the receiver has somehow stopped waiting properly. + warn!(concat!("I2C ", stringify!($name), ": receiver dropped")); + } + )+ + }) + } } - } + $( + #[derive(Debug, Default, Copy, Clone)] + pub struct $response { + $($response_fields)* + } + )+ + #[derive(Default)] + struct I2cDevicesStatus { + $( + $present_field: bool, + $($($status_fields)*)* + )* + } + }; } -pub enum I2cCommand { - GetTH { - response: OneshotSender, +make_i2c_commands![ + GetTH(get_temp_humidity): TempHumidityResponse { + pub celsius: u8, + /// A percentage. + pub relative_humidity: u8 + } @ th_present, + + GetDepth(get_pressure): DepthResponse { + pub depth: Depth, + } @ pressure_present + { + /// 14 bytes read from the MS5837 on startup + pressure_prom: [u8; 14], }, - GetDepth { - response: OneshotSender - } -} -impl I2cCommand { - fn name(&self) -> &'static str { - match self { - I2cCommand::GetTH { .. } => "T&H", - I2cCommand::GetDepth { .. } => "Depth" - } - } -} + GetPower(get_power): PowerResponse { + pub ma: f32, + pub mv: f32, + } @ power_sensor_present, +]; //////////////////////////////////////////////////////////////////////////////// +static I2C_THREAD_HANDLE: OnceLock = OnceLock::new(); + +/// Using this in functions outside the float thread is probably not safe +#[inline] pub fn get_i2c_thread_handle() -> TaskHandle_t { *I2C_THREAD_HANDLE.get().unwrap() as _ } + /// Spawn a separate FreeRTOS thread for the I2C handler task, then return a handle /// to it. /// @@ -101,7 +151,7 @@ pub fn initialize_i2c_thread( receiver: I2cReceiver<'static>, ) -> Result, AnyhowError> { let driver = I2cDriver::new(i2c, sda, scl, &I2cConfig { - baudrate: Hertz(100_000), // pressure sensor max is 400 kHz + baudrate: Hertz(100_000), // timeout: Some(Duration::from_millis(100).into()), // probably excessive ..Default::default() }).map_err(damn!("Failed to create i2c driver"))?; @@ -111,6 +161,7 @@ pub fn initialize_i2c_thread( .stack_size(64 * 1024) .spawn(move || { // This closure is the synchronous root of the i2c-thread. + I2C_THREAD_HANDLE.set(not_unsafe! { xTaskGetCurrentTaskHandle() } as usize).unwrap(/* unreachable */); block_on(i2c_thread(driver, receiver)) }) .map_err(damn!("Failed to spawn i2c thread"))?; @@ -121,65 +172,13 @@ pub fn initialize_i2c_thread( //////////////////////////////////////////////////////////////////////////////// -/// SHTC3 -mod th_sensor { - pub const ADDR: u8 = 0x70; - - pub const CMD_RESET: [u8; 2] = [0x80, 0x5D]; - pub const CMD_WAKE: [u8; 2] = [0x35, 0x17]; - pub const CMD_MEAS: [u8; 2] = [0x78, 0x66]; - pub const CMD_SLEEP: [u8; 2] = [0xB0, 0x98]; -} - -/// MS5837 -mod p_sensor { - pub const ADDR: u8 = 0x76; - - pub const CMD_RESET: u8 = 0x1E; - /// Must be ORed with the offset (0x00 -- 0x0C, even bytes only). Returns 2 bytes. - pub const CMD_READ_PROM: u8 = 0xA0; - - pub const CMD_READ_ADC: u8 = 0x00; - - pub const CMD_CONVERT_D1: u8 = 0x40; - pub const CMD_CONVERT_D2: u8 = 0x50; -} - -const TIMEOUT_1S: TickType_t = TickType::new_millis(1000).0; - -#[derive(Default)] -struct I2cDevicesStatus { - th_present: bool, - pressure_present: bool, - pressure_prom: [u8; 14], -} +pub const TIMEOUT_1S: TickType_t = TickType::new_millis(1000).0; async fn i2c_thread<'a>( mut i2c: I2cDriver<'_>, rx: I2cReceiver<'_> ) -> Never { - let i2c_init_data = reset_i2c_devices(&mut i2c); - - let mut status: I2cDevicesStatus = Default::default(); - - status.th_present = match i2c_init_data.th_init { - Ok(()) => true, - Err(error) => { - warn!("Failed to initialize T&H sensor: {:#?}", error); - false - } - }; - - status.pressure_present = match i2c_init_data.pressure_init { - Ok(prom) => { - status.pressure_prom = prom; - true - } - Err(error) => { - warn!("Failed to initialize pressure sensor: {:#?}", error); - false - } - }; + let status = reset_i2c_devices(&mut i2c).await; loop { let cmd = rx.receive().await; @@ -190,210 +189,40 @@ async fn i2c_thread<'a>( // so grab the name first in case of failure. let name = cmd.name(); - if let Err(error) = handle_i2c_command(&mut i2c, cmd, &status).await { + if let Err(error) = cmd.handle(&mut i2c, &status).await { warn!("Error processing I2C {name}: {error:#?}") } } } -struct I2cResetStatus { - th_init: Result<(), AnyhowError>, - pressure_init: Result<[u8; 14], AnyhowError>, -} - -#[inline] fn reset_i2c_devices( +#[inline] async fn reset_i2c_devices( i2c: &mut I2cDriver<'_>, -) -> I2cResetStatus { - // Reset depth sensor - let th_init = i2c.write( - th_sensor::ADDR, &th_sensor::CMD_RESET, TIMEOUT_1S - ).map_err(damn!("T&H sensor reset failed")); +) -> I2cDevicesStatus { + let mut status: I2cDevicesStatus = Default::default(); + // Reset depth sensor + status.th_present = reset_th_sensor(i2c) + .map_err(|error| warn!("Failed to initialize T&H sensor: {:#?}", error)) + .is_ok(); // No delay because we have a lot of other things to do first before processing I2C commands - let pressure_init = pressure_sensor_reset_and_read_prom(i2c); - - I2cResetStatus { - th_init, - pressure_init, - } -} - -#[inline] fn pressure_sensor_reset_and_read_prom(i2c: &mut I2cDriver<'_>) -> Result<[u8; 14], AnyhowError> { - i2c.write( - p_sensor::ADDR, &[p_sensor::CMD_RESET], TIMEOUT_1S - ).map_err(damn!("Pressure sensor reset failed"))?; - - // TODO: delay? - - // Read PROM data (ds. page 8) - let mut prom = [0_u8; 14]; - - for off in (0x00 ..= 0x0C).step_by(2) { - i2c.write(p_sensor::ADDR, &[p_sensor::CMD_READ_PROM | off as u8], TIMEOUT_1S) - .map_err(damn!("Pressure sensor PROM request failed at offset 0x{:02X}", off))?; - i2c.read(p_sensor::ADDR, &mut prom[off .. off+2], TIMEOUT_1S) - .map_err(damn!("Pressure sensor PROM read failed at offset 0x{:02X}", off))?; - } - - Ok(prom) -} - -/// Pulled out into a separate function to make error handling easier with `?`. -/// -/// Note that `#[inline]` doesn't force inlining, but is just a suggestion to the -/// compiler. -#[inline] async fn handle_i2c_command( - i2c: &mut I2cDriver<'_>, - cmd: I2cCommand, - device_status: &I2cDevicesStatus, -) -> Result<(), AnyhowError> { - debug!("Handling i2c command {}", cmd.name()); - - match cmd { - I2cCommand::GetTH { response } => Ok( - if device_status.th_present { - let packet = get_temp_humidity(i2c).await?; - - if response.send(packet).is_err() { - // The code that holds the receiver has somehow stopped waiting properly. - warn!("I2C T&H: receiver dropped"); - } - } else { - let _ = response.send(Default::default()); - } - ), - I2cCommand::GetDepth { response } => Ok( - if device_status.pressure_present { - let packet = get_pressure(i2c).await?; - - if response.send(packet).is_err() { - warn!("I2C Depth: receiver dropped"); - } - } else { - let _ = response.send(Default::default()); - } - ) - } -} - -//////////////////////////////////////////////////////////////////////////////// - -#[inline] async fn get_temp_humidity( - i2c: &mut I2cDriver<'_>, -) -> Result { - debug!("Waking up"); - // Wake up the SHTC3 - i2c.write(th_sensor::ADDR, &th_sensor::CMD_WAKE, TIMEOUT_1S).map_err(damn!("Wake fail"))?; - // TODO: check delay - - sleep_ms!(20); - - debug!("Requesting measurement"); - - // Request measurement - i2c.write(th_sensor::ADDR, &th_sensor::CMD_MEAS, TIMEOUT_1S).map_err(damn!("Request fail"))?; - sleep_ms!(20); // probably excessive - - let mut meas: [u8; 6] = [0; 6]; - - debug!("Reading measurement"); - - // Read measurement - i2c.read(th_sensor::ADDR, &mut meas, TIMEOUT_1S).map_err(damn!("Read fail"))?; - - if crc_shtc3(meas[0], meas[1]) != meas[2] { - warn!("I2C T&H: Temp failed CRC"); - } - - if crc_shtc3(meas[3], meas[4]) != meas[5] { - warn!("I2C T&H: Humidity failed CRC"); - } - - let temp_raw = (meas[0] as u32) << 8 | meas[1] as u32; - let hum_raw = (meas[3] as u32) << 8 | meas[4] as u32; - - let response = TempHumidityResponse { - celsius: (((temp_raw * 175) >> 16) - 45) as u8, - relative_humidity: ((hum_raw * 100) >> 16) as u8 + status.pressure_present = match pressure_sensor::pressure_sensor_reset_and_read_prom(i2c) { + Ok(prom) => { + status.pressure_prom = prom; + true + } + Err(error) => { + warn!("Failed to initialize pressure sensor: {:#?}", error); + false + } }; - debug!("{response:?}"); - - debug!("Putting to sleep"); - // Back to sleep - i2c.write(th_sensor::ADDR, &th_sensor::CMD_SLEEP, TIMEOUT_1S).map_err(damn!("Sleep fail"))?; - - info!("Done"); + status.power_sensor_present = reset_ina260(i2c).await + .map_err(|error| warn!("Failed to initialize ADC: {:#?}", error)) + .is_ok(); - Ok(response) -} - -#[inline] async fn get_pressure( - i2c: &mut I2cDriver<'_>, -) -> Result { - //! TODO - Ok(DepthResponse { depth: 0. }) + status } //////////////////////////////////////////////////////////////////////////////// -/// Adapted from pressure sensor datasheet page 10: -/// -/// ```c -/// unsigned char crc4(unsigned int n_prom[]) { // n_prom defined as 8x unsigned int (n_prom[8]) -/// int cnt; // simple counter -/// unsigned int n_rem=0; // crc remainder -/// unsigned char n_bit; -/// n_prom[0]=((n_prom[0]) & 0x0FFF); // CRC byte is replaced by 0 -/// n_prom[7]=0; // Subsidiary value, set to 0 -/// for (cnt = 0; cnt < 16; cnt++) {// operation is performed on bytes -/// // choose LSB or MSB -/// if (cnt%2==1) n_rem ^= (unsigned short) ((n_prom[cnt>>1]) & 0x00FF); -/// else n_rem ^= (unsigned short) (n_prom[cnt>>1]>>8); -/// for (n_bit = 8; n_bit > 0; n_bit--) { -/// if (n_rem & (0x8000)) n_rem = (n_rem << 1) ^ 0x3000; -/// else n_rem = (n_rem << 1); -/// } -/// } -/// n_rem= ((n_rem >> 12) & 0x000F); // final 4-bit remainder is CRC code -/// return (n_rem ^ 0x00); -/// } -/// ``` -fn crc_ms5837(mut n_prom: [u16; 8]) -> u8 { - n_prom[0] = n_prom[0] & 0x0FFF; - n_prom[7] = 0; - - let mut n_rem = 0; - - for cnt in 0..16 { - if cnt % 2 == 1 { - n_rem ^= n_prom[cnt >> 1] & 0x00FF; - } else { - n_rem ^= n_prom[cnt >> 1] >> 8; - } - cfor!{let mut n_bit = 8; n_bit > 0; n_bit -= 1; { - if n_rem & 0x8000 != 0 { - n_rem = (n_rem << 1) ^ 0x3000; - } else { - n_rem = n_rem << 1; - } - }} - } - - ((n_rem >> 12) & 0x000F) as u8 -} - -/// SHTC3 responses include a crc byte which we verify. -fn crc_shtc3(msb: u8, lsb: u8) -> u8 { - let mut crc = 0xFF; - crc ^= msb; - for _ in 0..8 { - crc = (crc << 1) ^ (if crc & 0x80 != 0 { 0x31 } else { 0 }) - } - crc ^= lsb; - for _ in 0..8 { - crc = (crc << 1) ^ (if crc & 0x80 != 0 { 0x31 } else { 0 }) - } - crc -} diff --git a/src/tasks/i2c/power_sensor.rs b/src/tasks/i2c/power_sensor.rs new file mode 100644 index 0000000..406eb2f --- /dev/null +++ b/src/tasks/i2c/power_sensor.rs @@ -0,0 +1,116 @@ +use crate::{ + prelude::*, + tasks::i2c::{PowerResponse, TIMEOUT_1S} +}; + +use esp_idf_svc::hal::i2c::I2cDriver; + +mod power_sensor { + /// The INA260 is a big-endian device. + pub const ADDR: u8 = 0x40; // A0 = A1 = GND + + // All registers are 16-bit + + /// Register format: RxxxAAAV VVIIIMBS + /// R: Reset active high + /// AAA: Average mode + /// 000: 1 + /// 001: 4 + /// 010: 16 + /// 011: 64 + /// 100: 128 + /// 101: 256 + /// 110: 512 + /// 111: 1024 + /// VVV = Voltage conversion time + /// III = Current conversion time (ditto) + /// 000: 140 us + /// 001: 204 us + /// 010: 332 us + /// 011: 588 us + /// 100: 1100 us + /// 101: 2116 us + /// 110: 4156 us + /// 111: 8244 us + /// M = Continuous mode active high + /// B = Measure bus voltage + /// S = Measure shunt voltage + pub const REG_CONFIG: u8 = 0x00; + pub const REG_CURRENT: u8 = 0x01; + pub const REG_VOLTAGE: u8 = 0x02; + pub const REG_POWER: u8 = 0x03; + pub const REG_ALERT_SOURCE: u8 = 0x06; + pub const REG_ALERT_SETPOINT: u8 = 0x07; + pub const REG_MFG_ID: u8 = 0xFE; + pub const REG_DEVICE_ID: u8 = 0xFF; +} + +#[inline] +pub(crate) async fn reset_ina260( + i2c: &mut I2cDriver<'_> +) -> Result<(), AnyhowError> { + i2c.write(power_sensor::ADDR, &[ + power_sensor::REG_CONFIG, 0b_1000_0000, 0b_0000_0000 + ], TIMEOUT_1S).map_err(damn!("INA260 reset failed"))?; + + // The data sheet doesn't specify reset time + sleep_ms!(100); + + // We have two factors that influence the conversion accuracy: conversion time, and + // average count. + + // We want to have readings every 100ms. Currently snapshots are taken every 500ms + // but I want to be able to decrease it. + // The voltage will be relatively stable, so high accuracy is not super important + + // t_sample = avg * (t_v + t_i) = 64 * (140us + 1.1ms) = 79.36 ms + + i2c.write(power_sensor::ADDR, &[ + power_sensor::REG_CONFIG, + // Avg=64 --\ + // ||| + 0b_0_000__011_0, + // | + // /- V=140us / + // || /-- Mode = Continuous V & I + // || ||| + 0b_00_100_111 + // ||| + // \-- I=1.1ms + ], TIMEOUT_1S).map_err(damn!("INA260 config failed"))?; + + // Currently, we do not use the ALERT pin feature. In the future, this could be a + // useful overcurrent (i.e. short) detection feature. + // If we were to, then we would possibly want to lower the current detection times + // (and increase averaging to make up for the accuracy loss) to make response time + // faster. + + Ok(()) +} + +#[inline] pub async fn get_power( + i2c: &mut I2cDriver<'_> +) -> Result { + let mut voltage_raw = [0_u8; 2]; + i2c.write_read( + power_sensor::ADDR, + &[power_sensor::REG_VOLTAGE], voltage_raw.as_mut_slice(), + TIMEOUT_1S).map_err(damn!("ADC voltage read failed"))?; + + let mut current_raw = [0_u8; 2]; + i2c.write_read( + power_sensor::ADDR, + &[power_sensor::REG_CURRENT], current_raw.as_mut_slice(), + TIMEOUT_1S).map_err(damn!("ADC current read failed"))?; + + // Voltage LSB = 1.25mV + // Current LSB = 1.25mA + + let voltage = (voltage_raw[0] as u16) << 8 | voltage_raw[1] as u16; + let current = (current_raw[0] as u16) << 8 | current_raw[1] as u16; + + let mv = voltage as f32 * 1.25; + let ma = current as f32 * 1.25; + + Ok(PowerResponse { mv, ma }) +} \ No newline at end of file diff --git a/src/tasks/i2c/pressure_sensor.rs b/src/tasks/i2c/pressure_sensor.rs new file mode 100644 index 0000000..aec45a3 --- /dev/null +++ b/src/tasks/i2c/pressure_sensor.rs @@ -0,0 +1,107 @@ +use crate::{ + prelude::*, + tasks::i2c::{ + DepthResponse, + TIMEOUT_1S + } +}; + +use esp_idf_svc::hal::i2c::I2cDriver; + +use cfor::cfor; + +/// MS5837 +mod p_sensor { + pub const ADDR: u8 = 0x76; + + pub const CMD_RESET: u8 = 0x1E; + /// Must be ORed with the offset (0x00 -- 0x0C, even bytes only). Returns 2 bytes. + pub const CMD_READ_PROM: u8 = 0xA0; + pub const fn read_prom(offset: usize) -> u8 { + CMD_READ_PROM | ((offset as u8) & 0x0C) + } + + pub const CMD_READ_ADC: u8 = 0x00; + + pub const CMD_CONVERT_D1: u8 = 0x40; + pub const CMD_CONVERT_D2: u8 = 0x50; +} + +#[inline] +pub fn pressure_sensor_reset_and_read_prom( + i2c: &mut I2cDriver<'_> +) -> Result<[u8; 14], AnyhowError> { + i2c.write( + p_sensor::ADDR, &[p_sensor::CMD_RESET], TIMEOUT_1S + ).map_err(damn!("Pressure sensor reset failed"))?; + + // TODO: delay? + + // Read PROM data (ds. page 8) + let mut prom = [0_u8; 14]; + + for off in (0x00 ..= 0x0C).step_by(2) { + i2c.write(p_sensor::ADDR, &[p_sensor::read_prom(off)], TIMEOUT_1S) + .map_err(damn!("Pressure sensor PROM request failed at offset 0x{:02X}", off))?; + i2c.read(p_sensor::ADDR, &mut prom[off .. off+2], TIMEOUT_1S) + .map_err(damn!("Pressure sensor PROM read failed at offset 0x{:02X}", off))?; + } + + Ok(prom) +} + +#[inline] pub async fn get_pressure( + i2c: &mut I2cDriver<'_>, +) -> Result { + //! TODO + Ok(Default::default()) +} + +/// Adapted from pressure sensor datasheet page 10: +/// +/// ```c +/// unsigned char crc4(unsigned int n_prom[]) { // n_prom defined as 8x unsigned int (n_prom[8]) +/// int cnt; // simple counter +/// unsigned int n_rem=0; // crc remainder +/// unsigned char n_bit; +/// n_prom[0]=((n_prom[0]) & 0x0FFF); // CRC byte is replaced by 0 +/// n_prom[7]=0; // Subsidiary value, set to 0 +/// for (cnt = 0; cnt < 16; cnt++) {// operation is performed on bytes +/// // choose LSB or MSB +/// if (cnt%2==1) n_rem ^= (unsigned short) ((n_prom[cnt>>1]) & 0x00FF); +/// else n_rem ^= (unsigned short) (n_prom[cnt>>1]>>8); +/// for (n_bit = 8; n_bit > 0; n_bit--) { +/// if (n_rem & (0x8000)) n_rem = (n_rem << 1) ^ 0x3000; +/// else n_rem = (n_rem << 1); +/// } +/// } +/// n_rem= ((n_rem >> 12) & 0x000F); // final 4-bit remainder is CRC code +/// return (n_rem ^ 0x00); +/// } +/// ``` +fn crc_ms5837(mut n_prom: [u16; 8]) -> u8 { + n_prom[0] = n_prom[0] & 0x0FFF; + n_prom[7] = 0; + + let mut n_rem = 0; + + for cnt in 0..16 { + if cnt % 2 == 1 { + n_rem ^= n_prom[cnt >> 1] & 0x00FF; + } else { + n_rem ^= n_prom[cnt >> 1] >> 8; + } + cfor!{let mut n_bit = 8; n_bit > 0; n_bit -= 1; { + if n_rem & 0x8000 != 0 { + n_rem = (n_rem << 1) ^ 0x3000; + } else { + n_rem = n_rem << 1; + } + }} + } + + ((n_rem >> 12) & 0x000F) as u8 +} + +/// TODO based on I2C depth sensor driver code +pub type Depth = f32; \ No newline at end of file diff --git a/src/tasks/i2c/temp_sensor.rs b/src/tasks/i2c/temp_sensor.rs new file mode 100644 index 0000000..ce688e1 --- /dev/null +++ b/src/tasks/i2c/temp_sensor.rs @@ -0,0 +1,88 @@ +use crate::{ + prelude::*, + tasks::i2c::{TempHumidityResponse, TIMEOUT_1S} +}; + +use esp_idf_svc::hal::i2c::I2cDriver; + +/// SHTC3 +pub mod th_sensor { + pub const ADDR: u8 = 0x70; + + pub const CMD_RESET: [u8; 2] = [0x80, 0x5D]; + pub const CMD_WAKE: [u8; 2] = [0x35, 0x17]; + pub const CMD_MEAS: [u8; 2] = [0x78, 0x66]; + pub const CMD_SLEEP: [u8; 2] = [0xB0, 0x98]; +} + +pub fn reset_th_sensor( + i2c: &mut I2cDriver<'_>, +) -> Result<(), AnyhowError> { + i2c.write(th_sensor::ADDR, &th_sensor::CMD_RESET, TIMEOUT_1S) + .map_err(AnyhowError::from) +} + +#[inline] +pub(crate) async fn get_temp_humidity( + i2c: &mut I2cDriver<'_>, +) -> Result { + debug!("Waking up"); + // Wake up the SHTC3 + i2c.write(th_sensor::ADDR, &th_sensor::CMD_WAKE, TIMEOUT_1S).map_err(damn!("Wake fail"))?; + // TODO: check delay + + sleep_ms!(20); + + debug!("Requesting measurement"); + + // Request measurement + i2c.write(th_sensor::ADDR, &th_sensor::CMD_MEAS, TIMEOUT_1S).map_err(damn!("Request fail"))?; + sleep_ms!(20); // probably excessive + + let mut meas: [u8; 6] = [0; 6]; + + debug!("Reading measurement"); + + // Read measurement + i2c.read(th_sensor::ADDR, &mut meas, TIMEOUT_1S).map_err(damn!("Read fail"))?; + + if crc_shtc3(meas[0], meas[1]) != meas[2] { + warn!("I2C T&H: Temp failed CRC"); + } + + if crc_shtc3(meas[3], meas[4]) != meas[5] { + warn!("I2C T&H: Humidity failed CRC"); + } + + let temp_raw = (meas[0] as u32) << 8 | meas[1] as u32; + let hum_raw = (meas[3] as u32) << 8 | meas[4] as u32; + + let response = TempHumidityResponse { + celsius: (((temp_raw * 175) >> 16) - 45) as u8, + relative_humidity: ((hum_raw * 100) >> 16) as u8 + }; + + debug!("{response:?}"); + + debug!("Putting to sleep"); + // Back to sleep + i2c.write(th_sensor::ADDR, &th_sensor::CMD_SLEEP, TIMEOUT_1S).map_err(damn!("Sleep fail"))?; + + info!("Done"); + + Ok(response) +} + +/// SHTC3 responses include a crc byte which we verify. +fn crc_shtc3(msb: u8, lsb: u8) -> u8 { + let mut crc = 0xFF; + crc ^= msb; + for _ in 0..8 { + crc = (crc << 1) ^ (if crc & 0x80 != 0 { 0x31 } else { 0 }) + } + crc ^= lsb; + for _ in 0..8 { + crc = (crc << 1) ^ (if crc & 0x80 != 0 { 0x31 } else { 0 }) + } + crc +} \ No newline at end of file diff --git a/src/tasks/led.rs b/src/tasks/led.rs index 7b1ddfc..15502c8 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,36 +33,38 @@ 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; +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 { - 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()?, - }) - } +impl LedState { + 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")) } } } -impl Index for LEDState { +impl Index for LedState { type Output = RGB8; fn index(&self, index: usize) -> &Self::Output { @@ -66,59 +72,36 @@ 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 { 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; } @@ -130,12 +113,50 @@ pub(crate) async fn led_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: SystemStatusReceiver<'_>, + button_led_signal: &LedColorSignal, +) -> Never { + loop { + 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. + 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); + + if let Some(button_led) = button_led { + led_vec.push(button_led); } - set_led(current_led_state[led_index]); + led_vec.push(charter_led); + + led_state_signal.signal(LedState::new(led_vec.as_slice())?); } } diff --git a/src/tasks.rs b/src/tasks/mod.rs similarity index 87% rename from src/tasks.rs rename to src/tasks/mod.rs index 2649b9f..bf0e1ae 100644 --- a/src/tasks.rs +++ b/src/tasks/mod.rs @@ -5,7 +5,6 @@ pub mod shutdown; pub mod i2c; pub mod charter; pub mod leak_detection; -pub mod power_measurement; pub mod stepper_controller; pub mod sd_card; pub mod status; diff --git a/src/tasks/power_measurement.rs b/src/tasks/power_measurement.rs deleted file mode 100644 index ec0878f..0000000 --- a/src/tasks/power_measurement.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::{ - prelude::*, -}; - -use esp_idf_svc::{ - hal::{ - adc::{ - attenuation, - Adc, - AdcChannel, - AdcUnit as AdcUnitType, - oneshot::{ - AdcChannelDriver, - AdcDriver, - config::{ - AdcChannelConfig, - Calibration - } - }, - Resolution - }, - gpio::ADCPin, - } -}; - -//////////////////////////////////////////////////////////////// - -/// Currently uses the raw 12-bit ADC output -#[derive(Debug, Clone)] -pub struct PowerMeasurement { - pub voltage: f32, - pub current: f32, -} - -impl Default for PowerMeasurement { - fn default() -> Self { Self { voltage: f32::NAN, current: f32::NAN } } -} - -const ADC_MAX_VAL: f32 = 4095.; - -/// TODO measure -const NOMINAL_VOLTAGE: f32 = 12.; -/// TODO measure -const VOLTAGE_DIVIDER_RATIO: f32 = 12.; - -/// TODO measure -const CURRENT_SENSOR_V_PER_A: f32 = 0.04; - -//////////////////////////////////////////////////////////////// - -macro_rules! adc_read_check_error { - ($channel:ident) => { - match ($channel).read() { - Ok(v) => f32::from(v) / ADC_MAX_VAL, - Err(error) => { - warn!(concat!("Failed to read ", stringify!($channel), ": {:#?}"), error); - f32::NAN - } - } - }; -} - -/// Wait for power measurement requests to come in, and then service them by reading -/// the ADCs. **Note that the ADC inputs must not exceed 1V.** -pub async fn power_measurement_task< - TheAdcUnit: AdcUnitType, - AdcVoltageChannel: AdcChannel, - AdcCurrentChannel: AdcChannel, ->( - adc_unit: impl Adc, - voltage_pin: impl ADCPin, - current_pin: impl ADCPin, - power_measurement_requests: PowerMeasurementRequestReceiver<'_> -) -> Never { - let adc_driver = AdcDriver::new(adc_unit) - .map_err(damn!("Failed to initialize ADC driver"))?; - - let config = AdcChannelConfig { - // Maximum input voltage is ~1.1 V. So a voltage divider will be needed. - // Input voltage tolerance can be increased with greater attenuation but - // the greatest level only gets us to about 4.4V so we might as well use - // an external divider and then no attenuation, which nets higher precision. - attenuation: attenuation::NONE, - // The only available resolution on esp32c3 - resolution: Resolution::Resolution12Bit, - // I think that doing this just makes the output more accurate for free? - // If results are weird, consider switching to None. - calibration: Calibration::Curve, - }; - - let mut voltage_channel = AdcChannelDriver::new(&adc_driver, voltage_pin, &config) - .map_err(damn!("Voltage ADC setup failure"))?; - let mut current_channel = AdcChannelDriver::new(&adc_driver, current_pin, &config) - .map_err(damn!("Current ADC setup failure"))?; - - loop { - // Instead of acting on each request individually, wait for potentially multiple to arrive, - // and send them all the same measurement. - power_measurement_requests.ready_to_receive().await; - - let measurement = PowerMeasurement { - voltage: adc_read_check_error!(voltage_channel) * NOMINAL_VOLTAGE * VOLTAGE_DIVIDER_RATIO, - current: adc_read_check_error!(current_channel) / CURRENT_SENSOR_V_PER_A, - }; - - while !power_measurement_requests.is_empty() { - if let Err(error) = match power_measurement_requests.try_receive() { - Ok(a) => a, - Err(_) => break, - }.send(measurement.clone()) { - warn!("Failed to send measurement: {error:#?}"); - } - } - } -} diff --git a/src/tasks/sd_card.rs b/src/tasks/sd_card.rs index 8233e10..5595072 100644 --- a/src/tasks/sd_card.rs +++ b/src/tasks/sd_card.rs @@ -1,29 +1,36 @@ +//! Filenames must be all-caps, and extensions must be three or less characters. +//! TODO: SD card eject? + use crate::{ prelude::*, tasks::{ - charter::Charter, status::SystemStatus - } + }, + config::SystemConfig }; use std::{ - fs::{File, OpenOptions}, - io::Write, - io::ErrorKind + fs::{ + OpenOptions, + read_dir + }, + io::{ + ErrorKind, + Write + }, + time::Instant, }; - +use std::fs::read_to_string; 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, @@ -40,15 +47,15 @@ use esp_idf_svc::{ io::vfs::MountedFatfs, }; -use heapless::String as HeaplessString; +use itertools::Itertools; use serde::Deserialize; //////////////////////////////////////////////////////////////////////////////// macro_rules! open_file { - ($path:expr) => { OpenOptions::new().read(true).append(true).create(true).open(sd!($path)).map_err(damn!("Failed to open {}", $path))? }; ($path:literal) => { OpenOptions::new().read(true).append(true).create(true).open(sd!($path)).map_err(damn!(concat!("Failed to open ", $path)))? }; + ($path:expr) => { OpenOptions::new().read(true).append(true).create(true).open(sd!($path)).map_err(damn!("Failed to open {}", $path))? }; } pub type FSHandle<'a> = MountedFatfs>>>>; @@ -71,6 +78,8 @@ pub fn setup_sd_card<'a>( } ).map_err(damn!("Failed to initialize SPI bus driver"))?; + info!("Created SPI bus driver"); + // Spi device config (CS etc) let sd_spi_device_driver = SdSpiHostDriver::new( spi_bus_driver, @@ -81,10 +90,14 @@ pub fn setup_sd_card<'a>( None // Write protection config ).map_err(damn!("Failed to initialize underlying SD SPI device driver"))?; + info!("Initialized SDSPI"); + let sd_card_driver = SdCardDriver::new_spi( sd_spi_device_driver, &SdConfiguration::default(/* todo */) ).map_err(damn!("Failed to initialize SD card driver"))?; + info!("Initialized SD driver"); + Ok(sd_card_driver) } @@ -94,6 +107,8 @@ pub fn mount_sd_card<'a>( sd_card_driver: SdCardDriver>>, should_format: bool ) -> Result, AnyhowError> { + info!("Beginning SD fs mount"); + let mut fatfs = Fatfs::new_sdcard( 0, // Drive number. This is the first & only SD card, so 0. sd_card_driver @@ -104,17 +119,28 @@ pub fn mount_sd_card<'a>( let mut buf = [0u8; 4096 /* TODO */]; - fatfs.format(&FormatConfiguration { + let config = &FormatConfiguration { fs_type: FatFsType::ExFat, // so we don't worry about log file sizes fat_backup_copy: true, // place a backup FAT table at the end(?) of the card ..Default::default() - }, &mut buf).map_err(damn!("Failed to format SD card"))?; + }; + let start = Instant::now(); + let result = fatfs.format(config, &mut buf); + let elapsed = start.elapsed(); + + match result { + Ok(()) => info!("Formatted SD card in {} us", elapsed.as_micros()), + Err(error) => { + warn!("Failed to format SD card: {error} ({} us)", elapsed.as_micros()); + return Err(error.into()); + } + } } // Not to be confused with esp_idf_svc::fs::fatfs::MountedFatfs // This is the vfs fatfs mount, which provides a higher layer of abstraction and allows // using Rust's std File objects. - MountedFatfs::mount( + let fs = MountedFatfs::mount( fatfs, // Mountpoint to prepend to file paths concat!("/", SD_CARD_NAME!()), @@ -122,34 +148,57 @@ pub fn mount_sd_card<'a>( // you exceed this number, nor the lifetime of an fd here (related to `File` object // lifetime, presumably). So TODO investigate FD count 4 - ).map_err(damn!("Failed to mount filesystem")) + ).map_err(damn!("Failed to mount filesystem"))?; + + info!("Successfully mounted SD card, found files: {}", read_dir(sd!("")) + .map_err(damn!("Failed to read /sd/"))? + .filter_map(Result::ok) + .format_with(", ", |entry, f| f(&entry.file_name().to_string_lossy()) + )); + + Ok(fs) } //////////////////////////////////////////////////////////////////////////////// -#[derive(Deserialize, Debug)] -pub struct SystemConfig { - pub wifi_ssid: HeaplessString<32>, - pub wifi_pass: HeaplessString<64>, - pub charter: Charter -} +pub fn read_config_from_sd() -> Result { + match read_to_string(sd!("CONFIG.JSN")) { + Ok(string) => SystemConfig::deserialize( + &mut serde_json::Deserializer::from_str(&string) + ).map_err(damn!("Failed to deserialize config")), + + Err(error) => { + warn!("Failed to read config file: {error}"); + + let config = SystemConfig::default(); + + let mut file = match OpenOptions::new() + .create_new(true) + .write(true) + .open(sd!("CONFIG.JSN")) { + Ok(file) => file, + Err(error) => { + warn!("Failed to create config file: {error}"); + return Ok(config); + }, + }; -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() + let content = match serde_json::to_string_pretty(&config) { + Ok(content) => content, + Err(error) => { + warn!("Failed to serialize default config: {error}"); + return Ok(config); + }, + }; + + if let Err(error) = file.write_all(content.as_bytes()) { + warn!("Failed to write default config: {error}") + } + + Ok(config) } } -} -pub fn read_config_from_sd() -> Result { - SystemConfig::deserialize( - &mut serde_json::Deserializer::from_reader( - File::open(sd!("config.json")).map_err(damn!("Failed to read config"))? - ) - ).map_err(damn!("Failed to deserialize config")) } //////////////////////////////////////////////////////////////////////////////// @@ -157,10 +206,10 @@ pub fn read_config_from_sd() -> Result { /// Appends to a series of log files whenever it receives [SystemStatus] objects /// with `create_log_entry == true`. /// -/// **Blocks entire FreeRTOS thread on write!** If the write operations are +/// **Blocks the 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, @@ -170,12 +219,10 @@ pub async fn sd_logging_task( } let mut log_file_counter = 0u8; - let mut log_file = open_file!("status_log_000.txt"); + let mut log_file = open_file!("LOG000.txt"); loop { - let snapshot = status_receiver.get_and(SystemStatus::create_log_entry).await; - - info!("LOGGING: {snapshot}"); + let snapshot = status_receiver.changed_and(SystemStatus::create_log_entry).await; loop { match write!(&mut log_file, "{snapshot}\n") { diff --git a/src/tasks/status.rs b/src/tasks/status.rs index 3ec09a9..cd6062d 100644 --- a/src/tasks/status.rs +++ b/src/tasks/status.rs @@ -1,29 +1,37 @@ -use crate::{prelude::*, get_time, tasks::{ - charter::{ - Depth, - CharterState +use crate::{ + prelude::*, + tasks::{ + charter::CharterState, + i2c::{ + I2cCommand, + PowerResponse, + pressure_sensor::Depth + } }, - power_measurement::PowerMeasurement, - i2c::I2cCommand, - led::LEDState -}, TimeContainer, tasks}; + timekeeping::{get_time, TimeContainer} +}; + +use std::fmt::{ + Debug, + Display, + Error as FmtError, + Formatter, +}; -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; +const LOG_INTERVAL_SNAPSHOT_COUNT: u32 = 10; #[derive(Debug, Clone)] pub struct SystemStatus { pub depth: Depth, - pub power_measurement: PowerMeasurement, + pub power_measurement: PowerResponse, pub charter_state: Option, pub timestamp: TimeContainer, @@ -35,11 +43,11 @@ 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, - self.power_measurement.current))?; + "[{}] depth: {}; voltage: {} mV; current: {} mA; charter state: ", + self.timestamp, self.depth, self.power_measurement.mv, + self.power_measurement.ma))?; if let Some(ref state) = self.charter_state { Display::fmt(&state, fmt) @@ -55,29 +63,29 @@ impl Display for SystemStatus { /// Tasks can read their channel to see the current system state without having to /// 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<'_>, - led_signal: &LedStateSignal, ) -> Never { - let mut counter = 0u8; + sleep_ms!(SNAPSHOT_INTERVAL_MS); + + let mut counter = 0; loop { counter += 1; - let create_log_entry = counter == 10; + let create_log_entry = counter == LOG_INTERVAL_SNAPSHOT_COUNT; if create_log_entry { counter = 0; } // 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(); let (_, power, _, depth) = join!( - power_measurement_request_sender.send(power_tx), + i2c_sender.send(I2cCommand::GetPower { response: power_tx }), power_rx, i2c_sender.send(I2cCommand::GetDepth { response: depth_tx }), depth_rx, @@ -97,40 +105,18 @@ 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 { + let status = SystemStatus { depth, power_measurement, charter_state, timestamp: get_time(), create_log_entry, - }); + }; + + if create_log_entry { info!("{status}"); } + + status_sender.send(status); sleep_ms!(SNAPSHOT_INTERVAL_MS); } diff --git a/src/tasks/stepper_controller.rs b/src/tasks/stepper_controller.rs index 15ff6f5..4a0f45d 100644 --- a/src/tasks/stepper_controller.rs +++ b/src/tasks/stepper_controller.rs @@ -3,38 +3,90 @@ use crate::prelude::*; use esp_idf_svc::{ hal::{ gpio::{ + Pin, AnyIOPin, - InputPin, OutputPin, PinDriver }, uart::{ config::Config as UartConfig, AsyncUartDriver, - Uart - }, - } + Uart, + UartDriver + } + }, + 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}, }; //////////////////////////////////////////////////////////////////////////////// -pub type StepperState = (/* TODO */); +/// TODO +#[derive(Debug)] +pub enum StepperState { + WeAreTooHigh, + WeAreTooLow, +} + +macro_rules! esp_unsafe_try { + ($invocation:expr, $($args:tt)+) => { + esp!( + not_unsafe! { $invocation } + ).map_err(damn!($($args)+))? + }; +} + +fn create_uart_driver<'a>( + uart: impl Uart + 'a, + mut uart_pin: AnyIOPin<'a>, +) -> Result>, AnyhowError> { + 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_try!( + gpio_set_level(pin, 1), + "Unable to set UART pin high" + ); + + esp_unsafe_try!( + gpio_set_direction(pin, gpio_mode_t_GPIO_MODE_INPUT_OUTPUT_OD), + "Failed to configure UART pin as OD" + ); + + esp_unsafe_try!( + gpio_pulldown_dis(pin), + "Failed to disable UART pulldown" + ); + + esp_unsafe_try!( + 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(damn!("Failed to initialize UART driver")) +} -/// TODO: Tharuka pub async fn stepper_control_task( - uart_controller: impl Uart, - rx_pin: impl InputPin, - tx_pin: impl OutputPin, + uart: impl Uart, + uart_pin: AnyIOPin<'_>, dir_pin: impl OutputPin, step_pin: impl OutputPin, stepper_state_channel: &StepperStateSignal, -) -> Never { - 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"))?; +) -> 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 + + let uart_driver = create_uart_driver(uart, uart_pin)?; let dir_driver = PinDriver::output(dir_pin) .map_err(damn!("Failed to initialize stepper DIR pin"))?; @@ -42,7 +94,9 @@ 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 stepper_state = stepper_state_channel.wait().await; + + info!("Stepper state: {:?}", stepper_state); } } diff --git a/src/timekeeping.rs b/src/timekeeping.rs new file mode 100644 index 0000000..bf791c1 --- /dev/null +++ b/src/timekeeping.rs @@ -0,0 +1,80 @@ +use std::{ + fmt::{Debug, Display, Error as FmtError, Formatter}, + sync::OnceLock, + time::{Duration, Instant} +}; + +use time::{ + macros::format_description, + Timestamp +}; + +//////////////////////////////////////////////////////////////////////////////// + +/// Set once on boot. Always safe to unwrap. +static BOOT_TIME: OnceLock = OnceLock::new(); +/// Requires synchronization data sent from the controlling computer via http, so +/// may not be populated. +static BOOT_TIMESTAMP: OnceLock = OnceLock::new(); + +#[inline(always)] pub(super) fn register_boot_instant() { + BOOT_TIME.set(Instant::now()).unwrap(/* unreachable */); +} + +#[inline] pub fn get_on_duration() -> Duration { + BOOT_TIME.get().unwrap(/* unreachable */).elapsed() +} + +/// Return either the time since boot, or the current Unix time. +pub fn get_time() -> TimeContainer { + BOOT_TIMESTAMP.get() + .map(|ts| (*ts + get_on_duration()).into()) + .unwrap_or_else(|| get_on_duration().into()) +} + +/// Return false if the time has previously been set. +#[must_use] pub fn set_boot_timestamp(now: Timestamp) -> bool { + BOOT_TIMESTAMP.set(now).is_ok() +} + +//////////////////////////////////////////////////////////////////////////////// + +#[derive(Clone)] +pub enum TimeContainer { + SinceBoot(Duration), + Timestamp(Timestamp), +} + +impl From for TimeContainer { + fn from(value: Timestamp) -> Self { Self::Timestamp(value) } +} + +impl From for TimeContainer { + fn from(value: Duration) -> Self { Self::SinceBoot(value) } +} + +impl Display for TimeContainer { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { + match self { + Self::SinceBoot(duration) => + f.write_fmt(format_args!("{}.{:03}", duration.as_secs(), duration.subsec_millis())), + Self::Timestamp(timestamp) => { + f.write_str(match ×tamp.format(format_description!( + "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]" + )) { + Ok(s) => s.as_str(), + Err(_) => "