Skip to main content

device_envoy_esp/
lib.rs

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