Skip to main content

uferris_bsp/
lib.rs

1#![no_std]
2#![doc(html_logo_url = "https://i.imgur.com/gAPf1TI.png")]
3#![doc(html_favicon_url = "https://i.imgur.com/L8Y0m57.png")]
4
5//! # uFerris Board Support Package Crate
6//!
7//! <div align="center">
8//!     <img src="https://i.imgur.com/KcvXhPw.png"
9//!         width="300"
10//!         style="margin-top: 40px; margin-bottom: 40px;"
11//!     />
12//! </div>
13//!
14//! uFerris is a flexible Rust embedded learning kit that can accomodate several SeeedStudio Xiao controllers.
15//! uFerris is essentially a carrier board that can accomodate mutliple different controllers.
16//!
17//! The `uferris-bsp` crate provides a generic Board Support Package for the uFerris carrier board. As such, `uferris-bsp` is architecture-agnostic and can support for several MCUs (ESP32, RP2040...etc.)
18//! Controller support is provided via feature flags.
19//!
20//! In summary, this crate is meant to provide a software abstraction to easily drive the uFerris board with any supported Xiao Controller.
21//!
22//! ## Crate Architechture
23//! The uFerris BSP architechture follows the layered scheme shown in the figure below. The upper uFerris board logic layer is meant to provide a hardware agnositc uniform interface across all Xiao controllers.
24//! The second adapter layer is introduced to create the mappings between the logic and the individual device HALs. The adapter layer also utilizes the `embedded-hal` traits where possible. In most cases the controller HALs provide implementations for `embedded-hal` traits.
25//!
26//! <div align="center">
27//!     <img src="https://i.imgur.com/SD77pGl.png"
28//!         width="500"
29//!         style="margin-top: 40px; margin-bottom: 40px;"
30//!     />
31//! </div>
32//!
33//! ## Currently Supported Xiaos:
34//! - Xiao ESP32-C3
35//! - Xiao ESP32-C5 (buzzer stubbed - no PWM driver in `esp-hal` yet)
36//! - Xiao ESP32-C6
37//! - Xiao ESP32-S3
38//! - Xiao nRF52840 (and nRF52840 Sense)
39//! - Xiao nRF54L15 (and nRF54L15 Sense)
40//! - Xiao RP2040
41//! - Xiao RP2350
42//!
43//! ## `async` Support
44//! The `async` feature enables the `async` board API. [`Uferris`] carries a
45//! [`Mode`] type parameter that selects which set of methods it exposes:
46//! [`Blocking`], the default, is the API described above and is what every
47//! existing program already gets, and `Async` is the same board with the I2C
48//! and ADC operations turned into `async fn`s plus `Uferris::wait_for_sw5`,
49//! which suspends until button 5 is pressed instead of spinning on it.
50//!
51//! The executor and the time driver are the application's responsibility. The
52//! BSP starts neither: it hands back a board whose methods are futures, and the
53//! program decides what runs them and where its delays come from. A board opts
54//! in by exposing an `uferris_init_async` alongside its blocking
55//! `uferris_init`, and all eight supported Xiaos now have one.
56//!
57//! The ESP boards differ in one respect, because their runtime does. `esp-rtos`
58//! is started from two peripherals — `TIMG0` and `SW_INTERRUPT` — that live in
59//! the same `esp_hal::Peripherals` struct their `uferris_init_async` consumes
60//! whole, and its thread mode executor cannot suspend a task before the
61//! scheduler is running. Their init is therefore synchronous, and hands those
62//! two peripherals back next to the board so that the application can start the
63//! scheduler with them before its first `.await`. See any of the ESP board
64//! modules for the details.
65//!
66//! ## Contributing to the uFerris BSP - Adding a New Xiao Board Support:
67//! Adding support for a new Xiao board entails two parts:
68//! 1. **Device Feature Flag in `Cargo.toml`**: A feature flag that imports the new device HAL needs to be added.
69//! 2. **Device Board Adapter**: This entails adding a new board definition (adapter layer) under the crate `boards/` folder.
70//!
71//! Other files in the crate should remain unchanged.
72//! It is recommended to view the existing board implementations for guidance on creating an adapter layer.
73//!
74//! ## Feature Flags
75#![doc = document_features::document_features!()]
76//!
77//! ## Usage
78//! The abstractions in this crate are designed in a way where they are common for any Xiao device.
79//! The only difference is that the correct controller board needs to be chosen as a feature.
80//! The steps to use this crate include the following:
81//!
82//! 1- Import the board init function:
83//! ```
84//! use uferris_bsp::uferris_init;
85//! ```
86//!
87//! 2- Acquire the controller peripherals and pass them to initialize the board:
88//! ```
89//! let mut uferris = uferris_init(peripherals);
90//! ```
91//!
92//! 3- Use the board methods:
93//! ```
94//! // Turn on LED 1 on the board
95//! uferris.led1_on();
96//! ```
97//!
98//! If no device feature is enabled, only the generic board API is available —
99//! `uferris_init` requires selecting your Xiao's feature. This is what code
100//! written against the board API alone, without a controller in the picture,
101//! builds against.
102//!
103
104use core::fmt;
105use core::marker::PhantomData; // Added this import
106use embedded_hal::digital::{InputPin, OutputPin};
107use embedded_hal::i2c::I2c;
108use embedded_hal::pwm::SetDutyCycle;
109#[cfg(feature = "async")]
110use embedded_hal_async::i2c::I2c as AsyncI2c;
111
112// `Mode` is the crate's own board mode typestate, so the `embedded-sdmmc` file
113// open mode is brought in under a name of its own.
114#[cfg(feature = "power-board")]
115use embedded_sdmmc::{
116    BlockDevice, Mode as FileMode, TimeSource, Timestamp, VolumeIdx, VolumeManager,
117};
118#[cfg(feature = "power-board")]
119use ina219::{SyncIna219, address::Address, calibration::IntCalibration};
120
121// Export generic components
122pub mod components;
123pub use components::io_expander::SwPos;
124
125// Export the specific board implementation based on features
126pub mod boards;
127
128// Re-exports
129pub use crate::components::io_expander::SevenSegDigit;
130#[cfg(feature = "xiao-esp32c3")]
131pub use boards::xiao_esp32c3::uferris_init;
132#[cfg(all(feature = "xiao-esp32c3", feature = "async"))]
133pub use boards::xiao_esp32c3::uferris_init_async;
134#[cfg(feature = "xiao-esp32c5")]
135pub use boards::xiao_esp32c5::uferris_init;
136#[cfg(all(feature = "xiao-esp32c5", feature = "async"))]
137pub use boards::xiao_esp32c5::uferris_init_async;
138#[cfg(feature = "xiao-esp32c6")]
139pub use boards::xiao_esp32c6::uferris_init;
140#[cfg(all(feature = "xiao-esp32c6", feature = "async"))]
141pub use boards::xiao_esp32c6::uferris_init_async;
142#[cfg(feature = "xiao-esp32s3")]
143pub use boards::xiao_esp32s3::uferris_init;
144#[cfg(all(feature = "xiao-esp32s3", feature = "async"))]
145pub use boards::xiao_esp32s3::uferris_init_async;
146#[cfg(feature = "xiao-nrf54l15")]
147pub use boards::xiao_nrf54l15::uferris_init;
148#[cfg(all(feature = "xiao-nrf54l15", feature = "async"))]
149pub use boards::xiao_nrf54l15::uferris_init_async;
150#[cfg(feature = "xiao-nrf52840")]
151pub use boards::xiao_nrf52840::uferris_init;
152#[cfg(all(feature = "xiao-nrf52840", feature = "async"))]
153pub use boards::xiao_nrf52840::uferris_init_async;
154#[cfg(feature = "xiao-rp2040")]
155pub use boards::xiao_rp2040::uferris_init;
156#[cfg(all(feature = "xiao-rp2040", feature = "async"))]
157pub use boards::xiao_rp2040::uferris_init_async;
158#[cfg(feature = "xiao-rp2350")]
159pub use boards::xiao_rp2350::uferris_init;
160#[cfg(all(feature = "xiao-rp2350", feature = "async"))]
161pub use boards::xiao_rp2350::uferris_init_async;
162
163// ------------------------------------------
164// Feature-Gated Trait Alias
165// ------------------------------------------
166
167#[cfg(feature = "power-board")]
168pub trait PowerConstraints: BlockDevice {}
169#[cfg(feature = "power-board")]
170impl<T: BlockDevice> PowerConstraints for T {}
171
172#[cfg(not(feature = "power-board"))]
173pub trait PowerConstraints {}
174#[cfg(not(feature = "power-board"))]
175impl<T> PowerConstraints for T {}
176
177// ------------------------------------------
178// Board Mode Typestate
179// ------------------------------------------
180
181mod sealed {
182    pub trait Sealed {}
183}
184
185/// Which flavour of the board API a [`Uferris`] exposes.
186///
187/// This is a typestate: it carries no data and exists only as the last type
188/// parameter of [`Uferris`], where it selects between the two `impl` blocks.
189/// [`Blocking`] is the default, so `Uferris<..>` written without it means what
190/// it has always meant, and `Async` turns every operation that talks to the
191/// I2C bus or the ADC into an `async fn`.
192///
193/// The trait is sealed: the two modes below are the only ones there are, and a
194/// board is only ever built by a board adapter.
195///
196/// The associated types park the power board fields per mode. The `async` power
197/// board API is not implemented yet, so under `Async` both of them are `()`
198/// and the fields are present but empty; under [`Blocking`] they are the
199/// concrete driver types they have always been, which is what keeps the
200/// blocking API unchanged.
201pub trait Mode: sealed::Sealed {
202    /// Type of the [`Uferris::vol_mgr`] field in this mode.
203    #[cfg(feature = "power-board")]
204    type VolMgr<BD: PowerConstraints>;
205
206    /// Type of the [`Uferris::power_monitor`] field in this mode.
207    #[cfg(feature = "power-board")]
208    type Ina<I2C>;
209}
210
211/// The blocking board API: every method returns its result directly.
212///
213/// This is the default mode and the one every board adapter's `uferris_init`
214/// hands back.
215pub struct Blocking;
216
217impl sealed::Sealed for Blocking {}
218
219impl Mode for Blocking {
220    #[cfg(feature = "power-board")]
221    type VolMgr<BD: PowerConstraints> = Option<VolumeManager<BD, DummyTimeSource>>;
222
223    #[cfg(feature = "power-board")]
224    type Ina<I2C> = SyncIna219<I2C, IntCalibration>;
225}
226
227/// The `async` board API: the I2C and ADC operations are `async fn`s.
228///
229/// A board in this mode is built by a board adapter's `uferris_init_async`. The
230/// executor that polls the resulting futures, and the time driver behind any
231/// delay the program uses, are the application's to bring.
232#[cfg(feature = "async")]
233pub struct Async;
234
235#[cfg(feature = "async")]
236impl sealed::Sealed for Async {}
237
238#[cfg(feature = "async")]
239impl Mode for Async {
240    // The power board is blocking-only for now: see [`Mode`].
241    #[cfg(feature = "power-board")]
242    type VolMgr<BD: PowerConstraints> = ();
243
244    #[cfg(feature = "power-board")]
245    type Ina<I2C> = ();
246}
247
248// ------------------------------------------
249// Constants & Errors
250// ------------------------------------------
251#[cfg(feature = "power-board")]
252const INA219_ADDR: u8 = 0x45;
253
254#[derive(Debug, Clone)]
255pub enum InitError {
256    IoExpander,
257    Rtc,
258    PowerMonitor,
259    SdCard,
260}
261
262impl fmt::Display for InitError {
263    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
264        match self {
265            InitError::IoExpander => write!(f, "I/O expander init failed (I2C)"),
266            InitError::Rtc => write!(f, "RTC init failed (I2C)"),
267            InitError::PowerMonitor => write!(f, "INA219 power monitor init failed (I2C)"),
268            InitError::SdCard => write!(f, "SD card init failed (SPI)"),
269        }
270    }
271}
272
273// ------------------------------------------
274// Generic Board Struct
275// ------------------------------------------
276
277/// The uFerris Board Driver.
278///
279/// `M` selects the board API: see [`Mode`]. It defaults to [`Blocking`], so
280/// `Uferris<LED, BTN, BUZZ, I2C, ADC, BD>` still names the blocking board.
281///
282/// The struct itself carries no bounds beyond the ones its fields need — the
283/// `OutputPin`/`InputPin`/`I2c`/... bounds live on the `impl` blocks, where they
284/// describe what each mode's methods actually require rather than gating the
285/// type as a whole. `BD: PowerConstraints` is the exception and has to stay: it
286/// is what makes the `M::VolMgr<BD>` field type well formed under
287/// `power-board`, where [`PowerConstraints`] is `BlockDevice`. Without that
288/// feature it is a blanket-implemented marker and constrains nothing.
289pub struct Uferris<LED, BTN, BUZZ, I2C, ADC, BD, M: Mode = Blocking>
290where
291    BD: PowerConstraints,
292{
293    pub led1: components::led::Led<LED>,
294    pub sw_btn5: components::button::Button<BTN, M>,
295    pub buzzer: components::buzzer::Buzzer<BUZZ>,
296    pub ldr: ADC,
297    pub expander: components::io_expander::IoExpander<I2C, M>,
298    pub rtc: components::rtc::Rtc<I2C, M>,
299    pub i2c: I2C,
300    #[cfg(feature = "power-board")]
301    pub vol_mgr: M::VolMgr<BD>,
302    #[cfg(feature = "power-board")]
303    pub power_monitor: M::Ina<I2C>,
304    // This phantom member uses BD (block Device) when the power-board feature is
305    // disabled, and carries the mode marker in every configuration.
306    pub _phantom: PhantomData<(BD, M)>,
307}
308
309impl<LED, BTN, BUZZ, I2C, ADC, BD> Uferris<LED, BTN, BUZZ, I2C, ADC, BD, Blocking>
310where
311    LED: OutputPin,
312    BTN: InputPin,
313    BUZZ: SetDutyCycle,
314    I2C: I2c,
315    ADC: components::ldr::OneShot,
316    BD: PowerConstraints,
317{
318    /// Create a new generic uFerris board instance.
319    // Only the board adapters call this, so with no device feature enabled
320    // there is no caller and the compiler would otherwise flag it as dead.
321    #[cfg_attr(
322        not(any(
323            feature = "xiao-esp32c3",
324            feature = "xiao-esp32c5",
325            feature = "xiao-esp32c6",
326            feature = "xiao-esp32s3",
327            feature = "xiao-nrf52840",
328            feature = "xiao-nrf54l15",
329            feature = "xiao-rp2040",
330            feature = "xiao-rp2350"
331        )),
332        allow(dead_code)
333    )]
334    fn new(
335        led1_pin: LED,
336        sw_btn5_pin: BTN,
337        pwm_pin: BUZZ,
338        ldr_driver: ADC,
339        expander_i2c: I2C,
340        rtc_i2c: I2C,
341        raw_i2c: I2C,
342        #[cfg(feature = "power-board")] vol_mgr: Option<VolumeManager<BD, DummyTimeSource>>,
343        #[cfg(feature = "power-board")] ina_i2c: I2C,
344    ) -> Result<Self, InitError> {
345        let mut expander = components::io_expander::IoExpander::new(expander_i2c);
346        let rtc = components::rtc::Rtc::new(rtc_i2c);
347
348        expander.init().map_err(|_| InitError::IoExpander)?;
349
350        let led1 = components::led::Led { pin: led1_pin };
351        let sw_btn5 = components::button::Button::new(sw_btn5_pin);
352        let buzzer = components::buzzer::Buzzer { pin: pwm_pin };
353
354        #[cfg(feature = "power-board")]
355        let power_monitor = {
356            let calib =
357                IntCalibration::new(ina219::calibration::MicroAmpere(1000), 100_000).unwrap();
358
359            SyncIna219::new_calibrated(ina_i2c, Address::from_byte(INA219_ADDR).unwrap(), calib)
360                .map_err(|_| InitError::PowerMonitor)?
361        };
362
363        Ok(Self {
364            led1,
365            sw_btn5,
366            buzzer,
367            ldr: ldr_driver,
368            expander,
369            rtc,
370            i2c: raw_i2c,
371            #[cfg(feature = "power-board")]
372            vol_mgr,
373            #[cfg(feature = "power-board")]
374            power_monitor,
375            _phantom: PhantomData,
376        })
377    }
378
379    /// Turn on LED 1
380    pub fn led1_on(&mut self) {
381        let _ = self.led1.pin.set_high();
382    }
383
384    /// Turn off LED 1
385    pub fn led1_off(&mut self) {
386        let _ = self.led1.pin.set_low();
387    }
388
389    /// Turn on LED 2
390    pub fn led2_on(&mut self) -> Result<(), I2C::Error> {
391        self.expander.led2_on()
392    }
393
394    /// Turn off LED 2
395    pub fn led2_off(&mut self) -> Result<(), I2C::Error> {
396        self.expander.led2_off()
397    }
398
399    /// Turn on LED 3
400    pub fn led3_on(&mut self) -> Result<(), I2C::Error> {
401        self.expander.led3_on()
402    }
403
404    /// Turn off LED 3
405    pub fn led3_off(&mut self) -> Result<(), I2C::Error> {
406        self.expander.led3_off()
407    }
408
409    /// Read Button Switch 1
410    pub fn read_sw1(&mut self) -> Result<bool, I2C::Error> {
411        self.expander.read_sw1()
412    }
413
414    /// Read Button Switch 2
415    pub fn read_sw2(&mut self) -> Result<bool, I2C::Error> {
416        self.expander.read_sw2()
417    }
418
419    /// Read Button Switch 3
420    pub fn read_sw3(&mut self) -> Result<bool, I2C::Error> {
421        self.expander.read_sw3()
422    }
423
424    /// Read Button Switch 4
425    pub fn read_sw4(&mut self) -> Result<bool, I2C::Error> {
426        self.expander.read_sw4()
427    }
428
429    /// Read Button Switch 5
430    pub fn read_sw5(&mut self) -> bool {
431        self.sw_btn5.pin.is_low().unwrap_or(false)
432    }
433
434    /// Read Slide Switch 6
435    pub fn read_sw6(&mut self) -> Result<SwPos, I2C::Error> {
436        self.expander.read_slide_sw6_position()
437    }
438
439    /// Read Slide Switch 7
440    pub fn read_sw7(&mut self) -> Result<SwPos, I2C::Error> {
441        self.expander.read_slide_sw7_position()
442    }
443
444    /// Write a Digit to the Seven Segment Display
445    pub fn write_seven_segment_digit(
446        &mut self,
447        digit: SevenSegDigit,
448        value: Option<u8>,
449    ) -> Result<(), I2C::Error> {
450        self.expander.write_seven_segment_digit(digit, value)
451    }
452
453    /// Activate/Deeactivate the Seven Segment Display Colon
454    pub fn seven_segment_display_colon_en(&mut self, enable: bool) -> Result<(), I2C::Error> {
455        self.expander.seven_segment_display_colon_en(enable)
456    }
457
458    /// Read LDR Value (12-bit Resolution)
459    pub fn read_ldr(&mut self) -> u16 {
460        self.ldr.read_raw()
461    }
462
463    /// Turn on Buzzer wit a Duty Cycle Value (0-100)
464    pub fn buzz_on(&mut self, duty: u16) {
465        let _ = self.buzzer.pin.set_duty_cycle(duty);
466    }
467
468    /// Turn off Buzzer
469    pub fn buzz_off(&mut self) {
470        let _ = self.buzzer.pin.set_duty_cycle_fully_off();
471    }
472
473    /// Set the uFerris RTC time
474    pub fn set_rtc_time(
475        &mut self,
476        year: u16,
477        month: u8,
478        day: u8,
479        hour: u8,
480        min: u8,
481        sec: u8,
482    ) -> Result<(), I2C::Error> {
483        self.rtc.set_time(year, month, day, hour, min, sec)
484    }
485
486    /// Read the uFerris RTC time
487    pub fn read_rtc_time(&mut self) -> Result<(u16, u8, u8, u8, u8, u8), I2C::Error> {
488        self.rtc.read_time()
489    }
490
491    /// Perform a raw I2C write operation
492    pub fn i2c_write(&mut self, addr: u8, data: &[u8]) -> Result<(), I2C::Error> {
493        self.i2c.write(addr, data)
494    }
495
496    /// Perform a raw I2C read operation
497    pub fn i2c_read(&mut self, addr: u8, buffer: &mut [u8]) -> Result<(), I2C::Error> {
498        self.i2c.read(addr, buffer)
499    }
500
501    /// Perform a raw I2C write-read operation
502    pub fn i2c_write_read(
503        &mut self,
504        addr: u8,
505        data: &[u8],
506        buffer: &mut [u8],
507    ) -> Result<(), I2C::Error> {
508        self.i2c.write_read(addr, data, buffer)
509    }
510
511    /// Read the System Voltage in milliVolts
512    #[cfg(feature = "power-board")]
513    pub fn read_system_voltage(&mut self) -> Option<u16> {
514        self.power_monitor
515            .bus_voltage()
516            .ok()
517            .map(|v| v.voltage_mv())
518    }
519
520    /// Read the System Current in microAmps
521    #[cfg(feature = "power-board")]
522    pub fn read_system_current(&mut self) -> Option<i64> {
523        match self.power_monitor.next_measurement() {
524            Ok(measurement) => measurement.map(|value| value.current.0),
525            Err(_) => None,
526        }
527    }
528
529    /// Read the System Power in milliWatts
530    #[cfg(feature = "power-board")]
531    pub fn read_system_power(&mut self) -> Option<i64> {
532        match self.power_monitor.next_measurement() {
533            Ok(measurement) => measurement.map(|value| value.power.0),
534            Err(_) => None,
535        }
536    }
537
538    /// Initialize / Check SD Card
539    /// Returns the size of the SD card in bytes if detected.
540    #[cfg(feature = "power-board")]
541    pub fn init_sd_card(&mut self) -> Result<u64, InitError> {
542        let mgr = self.vol_mgr.as_mut().ok_or(InitError::SdCard)?;
543
544        // Temporary variable to hold the size
545        let mut size_bytes = 0u64;
546
547        mgr.device(|dev| {
548            if let Ok(num_blocks) = dev.num_blocks() {
549                size_bytes = num_blocks.0 as u64 * 512;
550            }
551            crate::DummyTimeSource::default()
552        });
553
554        if size_bytes == 0 {
555            return Err(InitError::SdCard);
556        }
557
558        Ok(size_bytes)
559    }
560
561    /// Write data to a file in the root directory
562    /// Creates the file if it doesn't exist, or truncates it if it does.
563    #[cfg(feature = "power-board")]
564    pub fn write_to_file_in_root(&mut self, filename: &str, data: &[u8]) {
565        let mgr = self.vol_mgr.as_mut().expect("VolMgr missing");
566
567        // Open volume and directory
568        if let Ok(volume) = mgr.open_volume(VolumeIdx(0)) {
569            if let Ok(root_dir) = volume.open_root_dir() {
570                // 'file' is dropped/closed immediately after write
571                if let Ok(file) =
572                    root_dir.open_file_in_dir(filename, FileMode::ReadWriteCreateOrTruncate)
573                {
574                    let _ = file.write(data);
575                    let _ = file.flush();
576                } // end of 'file' scope
577            } // end of 'root_dir' scope
578        } // end of 'volume' scope
579    }
580
581    /// Read a file from the root directory in chunks
582    #[cfg(feature = "power-board")]
583    pub fn read_file_chunked<F>(&mut self, name: &str, mut f: F) -> Result<(), InitError>
584    where
585        F: FnMut(&[u8]),
586    {
587        let mgr = self.vol_mgr.as_mut().ok_or(InitError::SdCard)?;
588
589        let volume = mgr
590            .open_volume(VolumeIdx(0))
591            .map_err(|_| InitError::SdCard)?;
592        let root_dir = volume.open_root_dir().map_err(|_| InitError::SdCard)?;
593        let file = root_dir
594            .open_file_in_dir(name, FileMode::ReadOnly)
595            .map_err(|_| InitError::SdCard)?;
596
597        let mut buffer = [0u8; 64];
598        while !file.is_eof() {
599            let bytes_read = file.read(&mut buffer).map_err(|_| InitError::SdCard)?;
600            if bytes_read > 0 {
601                f(&buffer[..bytes_read]);
602            }
603        }
604        Ok(())
605    }
606}
607
608// ------------------------------------------
609// Generic Board Struct - `async` Mode
610// ------------------------------------------
611
612/// The `async` board API.
613///
614/// Every method that has to reach the I2C bus or the ADC is an `async fn` here,
615/// and [`Uferris::wait_for_sw5`] replaces the `read_sw5` spin loop with a wait.
616/// The methods that drive a pin directly — LED 1 and the buzzer — stay
617/// synchronous, because `OutputPin` and `SetDutyCycle` are blocking traits in
618/// both worlds: writing a GPIO or a PWM duty cycle is a register store and has
619/// nothing to await.
620///
621/// The power board API is not part of this mode yet. Under `power-board` the
622/// `vol_mgr` and `power_monitor` fields are still there, parked as `()` by
623/// [`Mode`], and the SD card and INA219 methods are blocking-only.
624#[cfg(feature = "async")]
625impl<LED, BTN, BUZZ, I2C, ADC, BD> Uferris<LED, BTN, BUZZ, I2C, ADC, BD, Async>
626where
627    LED: OutputPin,
628    BTN: embedded_hal_async::digital::Wait,
629    BUZZ: SetDutyCycle,
630    I2C: AsyncI2c,
631    ADC: components::ldr::OneShotAsync,
632    BD: PowerConstraints,
633{
634    /// Create a new generic uFerris board instance in `async` mode.
635    ///
636    /// This configures the I/O expander over the `async` bus on the way, which
637    /// is why it is a future. A board adapter that cannot await during its init
638    /// calls [`new_async_preinit`][Self::new_async_preinit] instead and brings
639    /// its own expander configuration — see there.
640    // Only the board adapters that await during their `uferris_init_async` call
641    // this, so unless one of those device features is enabled there is no
642    // caller and the compiler would otherwise flag it as dead. The ESP boards
643    // are deliberately absent: they go through `new_async_preinit`.
644    //
645    // Named `new_async` rather than `new`: the mode parameter of the board a
646    // call site is building is only pinned by the type it is being assigned or
647    // returned into, which is too late for method resolution, so two inherent
648    // `new`s would leave every adapter's call ambiguous.
649    #[cfg_attr(
650        not(any(
651            feature = "xiao-nrf52840",
652            feature = "xiao-nrf54l15",
653            feature = "xiao-rp2040",
654            feature = "xiao-rp2350"
655        )),
656        allow(dead_code)
657    )]
658    async fn new_async(
659        led1_pin: LED,
660        sw_btn5_pin: BTN,
661        pwm_pin: BUZZ,
662        ldr_driver: ADC,
663        expander_i2c: I2C,
664        rtc_i2c: I2C,
665        raw_i2c: I2C,
666    ) -> Result<Self, InitError> {
667        let mut board = Self::new_async_preinit(
668            led1_pin,
669            sw_btn5_pin,
670            pwm_pin,
671            ldr_driver,
672            expander_i2c,
673            rtc_i2c,
674            raw_i2c,
675        );
676
677        board
678            .expander
679            .init()
680            .await
681            .map_err(|_| InitError::IoExpander)?;
682
683        Ok(board)
684    }
685
686    /// Assemble an `async` board, leaving the I/O expander alone.
687    ///
688    /// The same board [`new_async`][Self::new_async] builds, minus the one
689    /// thing in that function that has to await: configuring the expander's
690    /// port directions and clearing its outputs. A caller that uses this owes
691    /// the board that configuration, done some other way, before it drives
692    /// anything.
693    ///
694    /// The ESP boards are why this exists. `esp-rtos`'s thread mode executor
695    /// will not suspend a task before the scheduler is started, and the
696    /// scheduler is started from peripherals their `uferris_init_async` only
697    /// hands back when it returns, so nothing in their init may await. They
698    /// configure the expander over the blocking I2C driver instead — the same
699    /// registers with the same values, polled rather than awaited — and turn
700    /// the driver `async` afterwards. Every other board goes through
701    /// `new_async` and never sees this.
702    // Every board adapter with an `uferris_init_async` reaches this, four of
703    // them by way of `new_async`, so the list here is the full set: add a board
704    // to it when its adapter gains an `uferris_init_async`.
705    #[cfg_attr(
706        not(any(
707            feature = "xiao-esp32c3",
708            feature = "xiao-esp32c5",
709            feature = "xiao-esp32c6",
710            feature = "xiao-esp32s3",
711            feature = "xiao-nrf52840",
712            feature = "xiao-nrf54l15",
713            feature = "xiao-rp2040",
714            feature = "xiao-rp2350"
715        )),
716        allow(dead_code)
717    )]
718    fn new_async_preinit(
719        led1_pin: LED,
720        sw_btn5_pin: BTN,
721        pwm_pin: BUZZ,
722        ldr_driver: ADC,
723        expander_i2c: I2C,
724        rtc_i2c: I2C,
725        raw_i2c: I2C,
726    ) -> Self {
727        let expander = components::io_expander::IoExpander::new(expander_i2c);
728        let rtc = components::rtc::Rtc::new(rtc_i2c);
729
730        let led1 = components::led::Led { pin: led1_pin };
731        let sw_btn5 = components::button::Button::new(sw_btn5_pin);
732        let buzzer = components::buzzer::Buzzer { pin: pwm_pin };
733
734        Self {
735            led1,
736            sw_btn5,
737            buzzer,
738            ldr: ldr_driver,
739            expander,
740            rtc,
741            i2c: raw_i2c,
742            #[cfg(feature = "power-board")]
743            vol_mgr: (),
744            #[cfg(feature = "power-board")]
745            power_monitor: (),
746            _phantom: PhantomData,
747        }
748    }
749
750    /// Turn on LED 1
751    pub fn led1_on(&mut self) {
752        let _ = self.led1.pin.set_high();
753    }
754
755    /// Turn off LED 1
756    pub fn led1_off(&mut self) {
757        let _ = self.led1.pin.set_low();
758    }
759
760    /// Turn on LED 2
761    pub async fn led2_on(&mut self) -> Result<(), I2C::Error> {
762        self.expander.led2_on().await
763    }
764
765    /// Turn off LED 2
766    pub async fn led2_off(&mut self) -> Result<(), I2C::Error> {
767        self.expander.led2_off().await
768    }
769
770    /// Turn on LED 3
771    pub async fn led3_on(&mut self) -> Result<(), I2C::Error> {
772        self.expander.led3_on().await
773    }
774
775    /// Turn off LED 3
776    pub async fn led3_off(&mut self) -> Result<(), I2C::Error> {
777        self.expander.led3_off().await
778    }
779
780    /// Read Button Switch 1
781    pub async fn read_sw1(&mut self) -> Result<bool, I2C::Error> {
782        self.expander.read_sw1().await
783    }
784
785    /// Read Button Switch 2
786    pub async fn read_sw2(&mut self) -> Result<bool, I2C::Error> {
787        self.expander.read_sw2().await
788    }
789
790    /// Read Button Switch 3
791    pub async fn read_sw3(&mut self) -> Result<bool, I2C::Error> {
792        self.expander.read_sw3().await
793    }
794
795    /// Read Button Switch 4
796    pub async fn read_sw4(&mut self) -> Result<bool, I2C::Error> {
797        self.expander.read_sw4().await
798    }
799
800    /// Wait until Button Switch 5 is pressed.
801    ///
802    /// Button 5 is the one push button wired straight to the controller rather
803    /// than to the I/O expander, so it is the one the board can wait on instead
804    /// of poll. The button is active low — the same reading the blocking
805    /// `read_sw5` reports as pressed — so this waits for the pin to be low.
806    ///
807    /// The wait is on the level, not on the edge: if the button is already held
808    /// down when this is called it returns immediately, exactly as a
809    /// `while !uferris.read_sw5() {}` loop would fall straight through.
810    pub async fn wait_for_sw5(&mut self) {
811        self.sw_btn5.wait_for_press().await;
812    }
813
814    /// Read Slide Switch 6
815    pub async fn read_sw6(&mut self) -> Result<SwPos, I2C::Error> {
816        self.expander.read_slide_sw6_position().await
817    }
818
819    /// Read Slide Switch 7
820    pub async fn read_sw7(&mut self) -> Result<SwPos, I2C::Error> {
821        self.expander.read_slide_sw7_position().await
822    }
823
824    /// Write a Digit to the Seven Segment Display
825    pub async fn write_seven_segment_digit(
826        &mut self,
827        digit: SevenSegDigit,
828        value: Option<u8>,
829    ) -> Result<(), I2C::Error> {
830        self.expander.write_seven_segment_digit(digit, value).await
831    }
832
833    /// Activate/Deeactivate the Seven Segment Display Colon
834    pub async fn seven_segment_display_colon_en(&mut self, enable: bool) -> Result<(), I2C::Error> {
835        self.expander.seven_segment_display_colon_en(enable).await
836    }
837
838    /// Read LDR Value (12-bit Resolution)
839    pub async fn read_ldr(&mut self) -> u16 {
840        self.ldr.read_raw().await
841    }
842
843    /// Turn on Buzzer wit a Duty Cycle Value (0-100)
844    pub fn buzz_on(&mut self, duty: u16) {
845        let _ = self.buzzer.pin.set_duty_cycle(duty);
846    }
847
848    /// Turn off Buzzer
849    pub fn buzz_off(&mut self) {
850        let _ = self.buzzer.pin.set_duty_cycle_fully_off();
851    }
852
853    /// Set the uFerris RTC time
854    pub async fn set_rtc_time(
855        &mut self,
856        year: u16,
857        month: u8,
858        day: u8,
859        hour: u8,
860        min: u8,
861        sec: u8,
862    ) -> Result<(), I2C::Error> {
863        self.rtc.set_time(year, month, day, hour, min, sec).await
864    }
865
866    /// Read the uFerris RTC time
867    pub async fn read_rtc_time(&mut self) -> Result<(u16, u8, u8, u8, u8, u8), I2C::Error> {
868        self.rtc.read_time().await
869    }
870
871    /// Perform a raw I2C write operation
872    pub async fn i2c_write(&mut self, addr: u8, data: &[u8]) -> Result<(), I2C::Error> {
873        self.i2c.write(addr, data).await
874    }
875
876    /// Perform a raw I2C read operation
877    pub async fn i2c_read(&mut self, addr: u8, buffer: &mut [u8]) -> Result<(), I2C::Error> {
878        self.i2c.read(addr, buffer).await
879    }
880
881    /// Perform a raw I2C write-read operation
882    pub async fn i2c_write_read(
883        &mut self,
884        addr: u8,
885        data: &[u8],
886        buffer: &mut [u8],
887    ) -> Result<(), I2C::Error> {
888        self.i2c.write_read(addr, data, buffer).await
889    }
890}
891
892// ------------------------------------------
893// Helper Types
894// ------------------------------------------
895#[cfg(feature = "power-board")]
896#[derive(Default, Clone, Copy)]
897pub struct DummyTimeSource;
898
899#[cfg(feature = "power-board")]
900impl TimeSource for DummyTimeSource {
901    fn get_timestamp(&self) -> Timestamp {
902        Timestamp {
903            year_since_1970: 0,
904            zero_indexed_month: 0,
905            zero_indexed_day: 0,
906            hours: 0,
907            minutes: 0,
908            seconds: 0,
909        }
910    }
911}