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}