File
Blob: firmware/crates/esp32-radio/src/platform/mod.rs
| 1 | //! Safe application-facing boundary around ESP-IDF and its Rust HAL. |
| 2 | //! Native handles never escape. |
| 3 | //! This module exposes no application callbacks to C and adds no unsafe |
| 4 | //! Send/Sync implementations. |
| 5 | mod audio; |
| 6 | mod certificate; |
| 7 | mod crypto; |
| 8 | #[cfg(feature = "crypto-profile")] |
| 9 | mod crypto_profile; |
| 10 | mod ffi; |
| 11 | mod http; |
| 12 | mod metrics; |
| 13 | mod music; |
| 14 | mod task; |
| 15 | |
| 16 | use crate::error::{Error, Result, check}; |
| 17 | pub(crate) use audio::{Dsp, History}; |
| 18 | pub(crate) use certificate::{certificate, ipv4}; |
| 19 | pub(crate) use crypto::provider as crypto_provider; |
| 20 | #[cfg(feature = "crypto-profile")] |
| 21 | pub(crate) use crypto_profile::CryptoProfile; |
| 22 | pub(crate) use http::Http; |
| 23 | pub(crate) use metrics::Metrics; |
| 24 | pub(crate) use music::MusicStorage; |
| 25 | use radio_core::protocol::Color; |
| 26 | use std::{ |
| 27 | cell::Cell, |
| 28 | ffi::CString, |
| 29 | marker::PhantomData, |
| 30 | sync::atomic::{AtomicBool, Ordering}, |
| 31 | }; |
| 32 | pub(crate) use task::SpawnConfig; |
| 33 | |
| 34 | static BOARD_TAKEN: AtomicBool = AtomicBool::new(false); |
| 35 | |
| 36 | /// Unique LED owner; moved to the radio thread after initialization, never shared. |
| 37 | #[derive(Debug)] |
| 38 | pub(crate) struct Board { |
| 39 | _not_sync: PhantomData<Cell<()>>, |
| 40 | } |
| 41 | |
| 42 | impl Board { |
| 43 | pub(crate) fn init() -> Result<Self> { |
| 44 | if BOARD_TAKEN.swap(true, Ordering::AcqRel) { |
| 45 | return Err(Error::new("board already initialized")); |
| 46 | } |
| 47 | #[cfg(target_os = "espidf")] |
| 48 | esp_idf_hal::sys::link_patches(); |
| 49 | // SAFETY: the atomic guard permits exactly one initialization; C owns all |
| 50 | // driver state. It registers only C callbacks and borrows no Rust memory. |
| 51 | let code = unsafe { ffi::radio_board_init() }; |
| 52 | check(code, "board initialization failed")?; |
| 53 | loop { |
| 54 | // SAFETY: Board initialization is the sole SNTP initializer, before |
| 55 | // tasks needing certificate timestamps or verified HTTPS are spawned. |
| 56 | match unsafe { ffi::radio_clock_sync() } { |
| 57 | 1 => break, |
| 58 | -1 => return Err(Error::new("clock initialization failed")), |
| 59 | _ => log("Waiting for time synchronization"), |
| 60 | } |
| 61 | } |
| 62 | Ok(Self { |
| 63 | _not_sync: PhantomData, |
| 64 | }) |
| 65 | } |
| 66 | pub(crate) fn set_led(&mut self, Color([r, g, b]): Color) -> Result<()> { |
| 67 | // SAFETY: a Board exists only after initialization; &mut self serializes |
| 68 | // LED calls and this sole owner is not Sync. Components fit the C ABI. |
| 69 | check(unsafe { ffi::radio_led(r, g, b) }, "LED write failed") |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | pub(crate) fn now_us() -> u64 { |
| 74 | // SAFETY: thread-safe IDF monotonic timer; no borrowed state or pointers. |
| 75 | unsafe { ffi::radio_now_us() } |
| 76 | } |
| 77 | pub(crate) fn random() -> u32 { |
| 78 | // SAFETY: thread-safe IDF random API; no borrowed state or retained pointers. |
| 79 | unsafe { ffi::radio_random() } |
| 80 | } |
| 81 | pub(crate) fn heap_free() -> u32 { |
| 82 | // SAFETY: IDF serializes heap inspection internally; scalar result only. |
| 83 | unsafe { ffi::radio_heap_free() } |
| 84 | } |
| 85 | pub(crate) fn stack_free() -> u32 { |
| 86 | // SAFETY: query of the calling FreeRTOS task, with no external pointers. |
| 87 | unsafe { ffi::radio_stack_free() } |
| 88 | } |
| 89 | pub(crate) fn console_byte() -> i32 { |
| 90 | // SAFETY: called only by app_main's console loop; C stdio owns its buffer. |
| 91 | unsafe { ffi::radio_console_byte() } |
| 92 | } |
| 93 | pub(crate) fn log(message: &str) { |
| 94 | if let Ok(message) = CString::new(message) { |
| 95 | // SAFETY: valid terminated string remains live until synchronous logging |
| 96 | // returns. C uses "%s", so text cannot become a format string. |
| 97 | unsafe { ffi::radio_log(message.as_ptr()) }; |
| 98 | } |
| 99 | } |
| 100 | pub(crate) fn recovery_attempt(reset: bool) -> u32 { |
| 101 | // SAFETY: called only by signaling (or fatal recovery which never returns). |
| 102 | // C retains the bounded scalar counter across software resets. |
| 103 | unsafe { ffi::radio_recovery_attempt(i32::from(reset)) } |
| 104 | } |
| 105 | pub(crate) fn restart() -> ! { |
| 106 | // SAFETY: IDF reset does not return or unwind and takes no borrowed memory. |
| 107 | unsafe { ffi::radio_restart() } |
| 108 | } |