Skip to main content

device_envoy_esp/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(target_os = "none", no_std)]
3
4pub mod button;
5#[cfg(target_os = "none")]
6pub mod clock_sync {
7    //! A device abstraction that combines NTP time synchronization with a local clock.
8    //!
9    //! See [`ClockSyncEsp`] for constructors and [`ClockSync`] for clock operations.
10    //!
11    //! You can create up to two concurrent `ClockSyncEsp` instances per program; a third is expected to fail at runtime because the `clock_sync` task pool uses `pool_size = 2`.
12    //!
13    //! # Example: WiFi + ClockSync logging
14    //!
15    //! ```rust,no_run
16    //! # #![no_std]
17    //! # #![no_main]
18    //! use device_envoy_esp::{
19    //!     Error,
20    //!     Result,
21    //!     button::PressedTo,
22    //!     button_watch,
23    //!     clock_sync::{ClockSync as _, ClockSyncEsp, ClockSyncStaticEsp, ONE_SECOND, h12_m_s},
24    //!     flash_block::FlashBlockEsp,
25    //!     wifi_auto::{
26    //!         WifiAuto as _, WifiAutoEsp, WifiAutoEvent,
27    //!         fields::{TimezoneField, TimezoneFieldStatic},
28    //!     },
29    //! };
30    //! use log::info;
31    //!
32    //! button_watch! {
33    //!     ButtonWatch6 {
34    //!         pin: GPIO6,
35    //!     }
36    //! }
37    //!
38    //! async fn run(
39    //!     spawner: embassy_executor::Spawner,
40    //!     p: esp_hal::peripherals::Peripherals,
41    //! ) -> Result<()> {
42    //!     let [wifi_credentials_flash_block, timezone_flash_block] =
43    //!         FlashBlockEsp::new_array::<2>(p.FLASH)?;
44    //!
45    //!     static TIMEZONE_STATIC: TimezoneFieldStatic = TimezoneField::new_static();
46    //!     let timezone_field = TimezoneField::new(&TIMEZONE_STATIC, timezone_flash_block);
47    //!
48    //!     let button_watch6 = ButtonWatch6::new(p.GPIO6, PressedTo::Ground, spawner).await?;
49    //!     let wifi_auto = WifiAutoEsp::new(
50    //!         p.WIFI,
51    //!         wifi_credentials_flash_block,
52    //!         "ClockSync",
53    //!         [timezone_field],
54    //!         spawner,
55    //!     )?;
56    //!
57    //!     let stack = wifi_auto
58    //!         .connect(&mut *button_watch6, |event| async move {
59    //!             match event {
60    //!                 WifiAutoEvent::CaptivePortalReady => {
61    //!                     info!("WifiAutoEsp: setup mode ready");
62    //!                 }
63    //!                 WifiAutoEvent::Connecting { .. } => {
64    //!                     info!("WifiAutoEsp: connecting");
65    //!                 }
66    //!                 WifiAutoEvent::ConnectionFailed => {
67    //!                     info!("WifiAutoEsp: connection failed");
68    //!                 }
69    //!             }
70    //!             Ok(())
71    //!         })
72    //!         .await?;
73    //!
74    //!     let offset_minutes = timezone_field
75    //!         .offset_minutes()?
76    //!         .ok_or(Error::MissingCustomWifiAutoField)?;
77    //!     static CLOCK_SYNC_STATIC: ClockSyncStaticEsp = ClockSyncEsp::new_static();
78    //!     let clock_sync = ClockSyncEsp::new(
79    //!         &CLOCK_SYNC_STATIC,
80    //!         stack,
81    //!         offset_minutes,
82    //!         Some(ONE_SECOND),
83    //!         spawner,
84    //!     );
85    //!
86    //!     loop {
87    //!         let tick = clock_sync.wait_for_tick().await;
88    //!         let (hours, minutes, seconds) = h12_m_s(&tick.local_time);
89    //!         info!(
90    //!             "Time {:02}:{:02}:{:02}, since sync {}s",
91    //!             hours,
92    //!             minutes,
93    //!             seconds,
94    //!             tick.since_last_sync.as_secs()
95    //!         );
96    //!     }
97    //! }
98    //! ```
99    /// A device abstraction that combines NTP time synchronization with a local clock.
100    pub use device_envoy_core::clock_sync::ClockSyncRuntime as ClockSyncEsp;
101    /// Resources needed to construct [`ClockSyncEsp`].
102    pub use device_envoy_core::clock_sync::ClockSyncStatic as ClockSyncStaticEsp;
103    pub use device_envoy_core::clock_sync::{
104        h12_m_s, ClockSync, ClockSyncTick, UnixSeconds, ONE_DAY, ONE_MINUTE, ONE_SECOND,
105    };
106}
107#[cfg(target_os = "none")]
108#[doc(hidden)]
109pub mod time_sync {
110    //! A device abstraction for Network Time Protocol (NTP) time synchronization over Wi-Fi.
111    //! See the [`clock_sync` module](crate::clock_sync) for the high-level clock API.
112    pub use device_envoy_core::clock_sync::UnixSeconds;
113    pub use device_envoy_core::time_sync::{TimeSync, TimeSyncEvent, TimeSyncStatic};
114}
115pub mod audio_player;
116pub mod flash_block;
117pub mod init_and_start;
118pub mod ir;
119#[cfg(target_os = "none")]
120pub mod lcd_text;
121#[cfg(target_os = "none")]
122pub mod led;
123pub mod led2d;
124pub mod led4;
125pub mod led_strip;
126#[cfg(target_os = "none")]
127pub mod rfid;
128mod rmt;
129mod rmt_mode;
130#[cfg(target_os = "none")]
131pub mod servo;
132#[cfg(target_os = "none")]
133mod servo_player;
134pub mod wifi_auto;
135
136#[cfg(doc)]
137pub mod docs {
138    //! Documentation-only pages for this crate.
139    pub mod development_guide {
140        #![doc = include_str!("docs/development_guide.md")]
141    }
142}
143
144pub use device_envoy_core::tone;
145use device_envoy_core::wifi_auto::WifiAutoError;
146/// Used internally by other macros.
147#[doc(hidden)]
148pub use paste::paste as __paste;
149
150/// Public for macro expansion in downstream crates.
151#[doc(hidden)]
152#[macro_export]
153macro_rules! __validate_keyword_fields_expr {
154    (
155        macro_name: $macro_name:literal,
156        allowed_macro: $allowed_macro:path,
157        fields: [ $( $field:ident : $value:expr ),* $(,)? ]
158    ) => {
159        const _: () = {
160            $( $allowed_macro!($field, $macro_name); )*
161            #[allow(non_snake_case)]
162            mod __device_envoy_keyword_fields_uniqueness {
163                $( pub(super) mod $field {} )*
164            }
165        };
166    };
167
168    (
169        macro_name: $macro_name:literal,
170        allowed_macro: $allowed_macro:path,
171        fields: [ $($fields:tt)* ]
172    ) => {
173        compile_error!(concat!($macro_name, " fields must use `name: value` syntax"));
174    };
175}
176
177// Workaround for esp-radio 0.17 bug: the linker script for esp32c6 declares EXTERN for
178// __esp_radio_misc_nvs_init and __esp_radio_misc_nvs_deinit under the wifi section, but
179// esp-radio only defines them with #[cfg(xtensa)], leaving RISC-V targets with unresolved
180// symbols in release builds.  These no-op stubs reproduce exactly what the Xtensa
181// implementation does.  Remove this block when the upstream bug is fixed.
182//
183// SAFETY: `no_mangle` is required because the linker script demands these exact C symbol
184// names.  The functions are no-ops that match the Xtensa stubs in esp-radio's
185// common_adapter.rs; they are called by the wifi blob and must have C linkage.
186#[cfg(all(target_arch = "riscv32", target_os = "none"))]
187mod _esp_radio_nvs_stubs {
188    #[unsafe(no_mangle)]
189    unsafe extern "C" fn __esp_radio_misc_nvs_deinit() {}
190
191    #[unsafe(no_mangle)]
192    unsafe extern "C" fn __esp_radio_misc_nvs_init() -> i32 {
193        0
194    }
195}
196
197#[doc(hidden)]
198#[cfg(target_os = "none")]
199pub use esp_hal;
200#[doc(hidden)]
201#[cfg(target_os = "none")]
202pub use esp_rtos;
203
204pub type Result<T, E = Error> = core::result::Result<T, E>;
205
206#[derive(Debug)]
207#[non_exhaustive]
208pub enum Error {
209    TaskSpawn(embassy_executor::SpawnError),
210    #[cfg(target_os = "none")]
211    FlashStorage(esp_storage::FlashStorageError),
212    InvalidFlashRegion,
213    IndexOutOfBounds,
214    FormatError,
215    StorageCorrupted,
216    FlashRegionMismatch,
217    Led4BitsToIndexesFull,
218    MissingCustomWifiAutoField,
219    Ntp(&'static str),
220    #[cfg(target_os = "none")]
221    Rmt(esp_hal::rmt::Error),
222    #[cfg(target_os = "none")]
223    SpiConfig(esp_hal::spi::master::ConfigError),
224    #[cfg(target_os = "none")]
225    Spi(esp_hal::spi::Error),
226    #[cfg(target_os = "none")]
227    Mfrc522Init(esp_hal_mfrc522::consts::PCDErrorCode),
228    #[cfg(target_os = "none")]
229    Mfrc522Version(esp_hal_mfrc522::consts::PCDErrorCode),
230    #[cfg(target_os = "none")]
231    I2cConfig(esp_hal::i2c::master::ConfigError),
232    #[cfg(target_os = "none")]
233    LedcTimer(esp_hal::ledc::timer::Error),
234    #[cfg(target_os = "none")]
235    LedcChannel(esp_hal::ledc::channel::Error),
236    #[cfg(target_os = "none")]
237    WifiInit(esp_radio::InitializationError),
238    #[cfg(target_os = "none")]
239    Wifi(esp_radio::wifi::WifiError),
240}
241
242impl From<embassy_executor::SpawnError> for Error {
243    fn from(e: embassy_executor::SpawnError) -> Self {
244        Self::TaskSpawn(e)
245    }
246}
247
248impl From<device_envoy_core::led4::Led4BitsToIndexesError> for Error {
249    fn from(error: device_envoy_core::led4::Led4BitsToIndexesError) -> Self {
250        match error {
251            device_envoy_core::led4::Led4BitsToIndexesError::Full => Self::Led4BitsToIndexesFull,
252        }
253    }
254}
255
256impl From<WifiAutoError> for Error {
257    fn from(error: WifiAutoError) -> Self {
258        match error {
259            WifiAutoError::FormatError => Self::FormatError,
260            WifiAutoError::StorageCorrupted => Self::StorageCorrupted,
261            WifiAutoError::MissingCustomWifiAutoField => Self::MissingCustomWifiAutoField,
262        }
263    }
264}
265
266#[cfg(target_os = "none")]
267impl From<esp_storage::FlashStorageError> for Error {
268    fn from(error: esp_storage::FlashStorageError) -> Self {
269        Self::FlashStorage(error)
270    }
271}
272
273#[cfg(target_os = "none")]
274impl From<esp_radio::InitializationError> for Error {
275    fn from(error: esp_radio::InitializationError) -> Self {
276        Self::WifiInit(error)
277    }
278}
279
280#[cfg(target_os = "none")]
281impl From<esp_hal::rmt::Error> for Error {
282    fn from(error: esp_hal::rmt::Error) -> Self {
283        Self::Rmt(error)
284    }
285}
286
287#[cfg(target_os = "none")]
288impl From<esp_hal::spi::master::ConfigError> for Error {
289    fn from(error: esp_hal::spi::master::ConfigError) -> Self {
290        Self::SpiConfig(error)
291    }
292}
293
294#[cfg(target_os = "none")]
295impl From<esp_hal::i2c::master::ConfigError> for Error {
296    fn from(error: esp_hal::i2c::master::ConfigError) -> Self {
297        Self::I2cConfig(error)
298    }
299}
300
301#[cfg(target_os = "none")]
302impl From<esp_hal::spi::Error> for Error {
303    fn from(error: esp_hal::spi::Error) -> Self {
304        Self::Spi(error)
305    }
306}
307
308#[cfg(target_os = "none")]
309impl From<esp_hal::ledc::timer::Error> for Error {
310    fn from(error: esp_hal::ledc::timer::Error) -> Self {
311        Self::LedcTimer(error)
312    }
313}
314
315#[cfg(target_os = "none")]
316impl From<esp_hal::ledc::channel::Error> for Error {
317    fn from(error: esp_hal::ledc::channel::Error) -> Self {
318        Self::LedcChannel(error)
319    }
320}
321
322#[cfg(target_os = "none")]
323impl From<esp_radio::wifi::WifiError> for Error {
324    fn from(error: esp_radio::wifi::WifiError) -> Self {
325        Self::Wifi(error)
326    }
327}