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, async |event| -> Result<(), device_envoy_esp::Error> {
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;
182#[cfg(target_os = "none")]
183pub mod cyd;
184// The buffer implementation is hardware-independent, so exercise the same
185// private storage code in the host test harness even though the full CYD
186// peripheral module is embedded-only.
187#[cfg(all(test, feature = "host"))]
188#[path = "cyd/buffer.rs"]
189mod cyd_buffer_host_tests;
190pub mod flash_block;
191pub mod init_and_start;
192#[cfg(esp_has_rmt)]
193pub mod ir;
194#[cfg(target_os = "none")]
195pub mod lcd_text;
196#[cfg(target_os = "none")]
197pub mod led;
198#[cfg(any(feature = "host", target_os = "none"))]
199pub mod led2d;
200pub mod led4;
201#[cfg(target_os = "none")]
202pub mod led_strip;
203#[cfg(target_os = "none")]
204pub mod rfid;
205#[cfg(esp_has_rmt)]
206mod rmt;
207mod rmt_mode;
208#[cfg(all(target_os = "none", esp_has_ledc))]
209pub mod servo;
210#[cfg(all(target_os = "none", esp_has_ledc))]
211mod servo_player;
212#[cfg(any(feature = "host", esp_has_wifi))]
213pub mod wifi_auto;
214
215#[cfg(doc)]
216pub mod docs {
217    //! Documentation-only pages for this crate.
218    pub mod development_guide {
219        #![doc = include_str!("docs/development_guide.md")]
220    }
221}
222
223pub use device_envoy_core::tone;
224/// Used internally by other macros.
225#[doc(hidden)]
226pub use paste::paste as __paste;
227
228/// Public for macro expansion in downstream crates.
229#[doc(hidden)]
230#[macro_export]
231macro_rules! __validate_keyword_fields_expr {
232    (
233        macro_name: $macro_name:literal,
234        allowed_macro: $allowed_macro:path,
235        fields: [ $( $field:ident : $value:expr ),* $(,)? ]
236    ) => {
237        const _: () = {
238            $( $allowed_macro!($field, $macro_name); )*
239            #[allow(non_snake_case)]
240            mod __device_envoy_keyword_fields_uniqueness {
241                $( pub(super) mod $field {} )*
242            }
243        };
244    };
245
246    (
247        macro_name: $macro_name:literal,
248        allowed_macro: $allowed_macro:path,
249        fields: [ $($fields:tt)* ]
250    ) => {
251        compile_error!(concat!($macro_name, " fields must use `name: value` syntax"));
252    };
253}
254
255// Workaround for esp-radio 0.17 bug: the linker script for esp32c6 declares EXTERN for
256// __esp_radio_misc_nvs_init and __esp_radio_misc_nvs_deinit under the wifi section, but
257// esp-radio only defines them with #[cfg(xtensa)], leaving RISC-V targets with unresolved
258// symbols in release builds.  These no-op stubs reproduce exactly what the Xtensa
259// implementation does.  Remove this block when the upstream bug is fixed.
260//
261// SAFETY: `no_mangle` is required because the linker script demands these exact C symbol
262// names.  The functions are no-ops that match the Xtensa stubs in esp-radio's
263// common_adapter.rs; they are called by the wifi blob and must have C linkage.
264#[cfg(all(target_arch = "riscv32", target_os = "none"))]
265mod _esp_radio_nvs_stubs {
266    #[unsafe(no_mangle)]
267    unsafe extern "C" fn __esp_radio_misc_nvs_deinit() {}
268
269    #[unsafe(no_mangle)]
270    unsafe extern "C" fn __esp_radio_misc_nvs_init() -> i32 {
271        0
272    }
273}
274
275#[doc(hidden)]
276#[cfg(target_os = "none")]
277pub use esp_hal;
278#[doc(hidden)]
279#[cfg(target_os = "none")]
280pub use esp_rtos;
281
282pub type Result<T, E = Error> = core::result::Result<T, E>;
283
284#[derive(Debug, derive_more::From)]
285#[non_exhaustive]
286pub enum Error {
287    TaskSpawn(embassy_executor::SpawnError),
288    #[from(ignore)]
289    Core(device_envoy_core::Error),
290    #[cfg(target_os = "none")]
291    FlashStorage(esp_storage::FlashStorageError),
292    InvalidFlashRegion,
293    IndexOutOfBounds,
294    FormatError,
295    StorageCorrupted,
296    FlashRegionMismatch,
297    Led4BitsToIndexesFull,
298    MissingCustomWifiAutoField,
299    Ntp(&'static str),
300    #[cfg(all(target_os = "none", esp_has_rmt))]
301    RmtConfig(esp_hal::rmt::ConfigError),
302    #[cfg(all(target_os = "none", esp_has_rmt))]
303    Rmt(esp_hal::rmt::Error),
304    #[cfg(target_os = "none")]
305    SpiConfig(esp_hal::spi::master::ConfigError),
306    #[cfg(target_os = "none")]
307    Spi(esp_hal::spi::Error),
308    #[cfg(target_os = "none")]
309    #[from(ignore)]
310    Mfrc522Init(esp_hal_mfrc522::consts::PCDErrorCode),
311    #[cfg(target_os = "none")]
312    #[from(ignore)]
313    Mfrc522Version(esp_hal_mfrc522::consts::PCDErrorCode),
314    #[cfg(target_os = "none")]
315    I2cConfig(esp_hal::i2c::master::ConfigError),
316    #[cfg(all(target_os = "none", esp_has_ledc))]
317    LedcTimer(esp_hal::ledc::timer::Error),
318    #[cfg(all(target_os = "none", esp_has_ledc))]
319    LedcChannel(esp_hal::ledc::channel::Error),
320    #[cfg(all(target_os = "none", esp_has_wifi))]
321    Wifi(esp_radio::wifi::WifiError),
322    #[cfg(target_os = "none")]
323    #[from(ignore)]
324    // `cyd::Error` carries the detailed operation/source diagnostics.
325    Cyd(cyd::Error),
326    #[cfg(target_os = "none")]
327    CydTouchUnavailable,
328}
329
330#[cfg(target_os = "none")]
331impl From<cyd::Error> for Error {
332    fn from(error: cyd::Error) -> Self {
333        Self::Cyd(error)
334    }
335}
336
337impl From<device_envoy_core::Error> for Error {
338    fn from(error: device_envoy_core::Error) -> Self {
339        match error {
340            device_envoy_core::Error::TaskSpawn(spawn_error) => Self::TaskSpawn(spawn_error),
341            core_error => Self::Core(core_error),
342        }
343    }
344}
345
346impl From<device_envoy_core::led4::Led4BitsToIndexesError> for Error {
347    fn from(error: device_envoy_core::led4::Led4BitsToIndexesError) -> Self {
348        match error {
349            device_envoy_core::led4::Led4BitsToIndexesError::Full => Self::Led4BitsToIndexesFull,
350        }
351    }
352}