#![no_std]
#![doc(html_logo_url = "https://i.imgur.com/gAPf1TI.png")]
#![doc(html_favicon_url = "https://i.imgur.com/L8Y0m57.png")]
#![doc = document_features::document_features!()]
use core::fmt;
use core::marker::PhantomData; use embedded_hal::digital::{InputPin, OutputPin};
use embedded_hal::i2c::I2c;
use embedded_hal::pwm::SetDutyCycle;
#[cfg(feature = "async")]
use embedded_hal_async::i2c::I2c as AsyncI2c;
#[cfg(feature = "power-board")]
use embedded_sdmmc::{
BlockDevice, Mode as FileMode, TimeSource, Timestamp, VolumeIdx, VolumeManager,
};
#[cfg(feature = "power-board")]
use ina219::{SyncIna219, address::Address, calibration::IntCalibration};
pub mod components;
pub use components::io_expander::SwPos;
pub mod boards;
pub use crate::components::io_expander::SevenSegDigit;
#[cfg(feature = "xiao-esp32c3")]
pub use boards::xiao_esp32c3::uferris_init;
#[cfg(all(feature = "xiao-esp32c3", feature = "async"))]
pub use boards::xiao_esp32c3::uferris_init_async;
#[cfg(feature = "xiao-esp32c5")]
pub use boards::xiao_esp32c5::uferris_init;
#[cfg(all(feature = "xiao-esp32c5", feature = "async"))]
pub use boards::xiao_esp32c5::uferris_init_async;
#[cfg(feature = "xiao-esp32c6")]
pub use boards::xiao_esp32c6::uferris_init;
#[cfg(all(feature = "xiao-esp32c6", feature = "async"))]
pub use boards::xiao_esp32c6::uferris_init_async;
#[cfg(feature = "xiao-esp32s3")]
pub use boards::xiao_esp32s3::uferris_init;
#[cfg(all(feature = "xiao-esp32s3", feature = "async"))]
pub use boards::xiao_esp32s3::uferris_init_async;
#[cfg(feature = "xiao-nrf54l15")]
pub use boards::xiao_nrf54l15::uferris_init;
#[cfg(all(feature = "xiao-nrf54l15", feature = "async"))]
pub use boards::xiao_nrf54l15::uferris_init_async;
#[cfg(feature = "xiao-nrf52840")]
pub use boards::xiao_nrf52840::uferris_init;
#[cfg(all(feature = "xiao-nrf52840", feature = "async"))]
pub use boards::xiao_nrf52840::uferris_init_async;
#[cfg(feature = "xiao-rp2040")]
pub use boards::xiao_rp2040::uferris_init;
#[cfg(all(feature = "xiao-rp2040", feature = "async"))]
pub use boards::xiao_rp2040::uferris_init_async;
#[cfg(feature = "xiao-rp2350")]
pub use boards::xiao_rp2350::uferris_init;
#[cfg(all(feature = "xiao-rp2350", feature = "async"))]
pub use boards::xiao_rp2350::uferris_init_async;
#[cfg(feature = "power-board")]
pub trait PowerConstraints: BlockDevice {}
#[cfg(feature = "power-board")]
impl<T: BlockDevice> PowerConstraints for T {}
#[cfg(not(feature = "power-board"))]
pub trait PowerConstraints {}
#[cfg(not(feature = "power-board"))]
impl<T> PowerConstraints for T {}
mod sealed {
pub trait Sealed {}
}
pub trait Mode: sealed::Sealed {
#[cfg(feature = "power-board")]
type VolMgr<BD: PowerConstraints>;
#[cfg(feature = "power-board")]
type Ina<I2C>;
}
pub struct Blocking;
impl sealed::Sealed for Blocking {}
impl Mode for Blocking {
#[cfg(feature = "power-board")]
type VolMgr<BD: PowerConstraints> = Option<VolumeManager<BD, DummyTimeSource>>;
#[cfg(feature = "power-board")]
type Ina<I2C> = SyncIna219<I2C, IntCalibration>;
}
#[cfg(feature = "async")]
pub struct Async;
#[cfg(feature = "async")]
impl sealed::Sealed for Async {}
#[cfg(feature = "async")]
impl Mode for Async {
#[cfg(feature = "power-board")]
type VolMgr<BD: PowerConstraints> = ();
#[cfg(feature = "power-board")]
type Ina<I2C> = ();
}
#[cfg(feature = "power-board")]
const INA219_ADDR: u8 = 0x45;
#[derive(Debug, Clone)]
pub enum InitError {
IoExpander,
Rtc,
PowerMonitor,
SdCard,
}
impl fmt::Display for InitError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
InitError::IoExpander => write!(f, "I/O expander init failed (I2C)"),
InitError::Rtc => write!(f, "RTC init failed (I2C)"),
InitError::PowerMonitor => write!(f, "INA219 power monitor init failed (I2C)"),
InitError::SdCard => write!(f, "SD card init failed (SPI)"),
}
}
}
pub struct Uferris<LED, BTN, BUZZ, I2C, ADC, BD, M: Mode = Blocking>
where
BD: PowerConstraints,
{
pub led1: components::led::Led<LED>,
pub sw_btn5: components::button::Button<BTN, M>,
pub buzzer: components::buzzer::Buzzer<BUZZ>,
pub ldr: ADC,
pub expander: components::io_expander::IoExpander<I2C, M>,
pub rtc: components::rtc::Rtc<I2C, M>,
pub i2c: I2C,
#[cfg(feature = "power-board")]
pub vol_mgr: M::VolMgr<BD>,
#[cfg(feature = "power-board")]
pub power_monitor: M::Ina<I2C>,
pub _phantom: PhantomData<(BD, M)>,
}
impl<LED, BTN, BUZZ, I2C, ADC, BD> Uferris<LED, BTN, BUZZ, I2C, ADC, BD, Blocking>
where
LED: OutputPin,
BTN: InputPin,
BUZZ: SetDutyCycle,
I2C: I2c,
ADC: components::ldr::OneShot,
BD: PowerConstraints,
{
#[cfg_attr(
not(any(
feature = "xiao-esp32c3",
feature = "xiao-esp32c5",
feature = "xiao-esp32c6",
feature = "xiao-esp32s3",
feature = "xiao-nrf52840",
feature = "xiao-nrf54l15",
feature = "xiao-rp2040",
feature = "xiao-rp2350"
)),
allow(dead_code)
)]
fn new(
led1_pin: LED,
sw_btn5_pin: BTN,
pwm_pin: BUZZ,
ldr_driver: ADC,
expander_i2c: I2C,
rtc_i2c: I2C,
raw_i2c: I2C,
#[cfg(feature = "power-board")] vol_mgr: Option<VolumeManager<BD, DummyTimeSource>>,
#[cfg(feature = "power-board")] ina_i2c: I2C,
) -> Result<Self, InitError> {
let mut expander = components::io_expander::IoExpander::new(expander_i2c);
let rtc = components::rtc::Rtc::new(rtc_i2c);
expander.init().map_err(|_| InitError::IoExpander)?;
let led1 = components::led::Led { pin: led1_pin };
let sw_btn5 = components::button::Button::new(sw_btn5_pin);
let buzzer = components::buzzer::Buzzer { pin: pwm_pin };
#[cfg(feature = "power-board")]
let power_monitor = {
let calib =
IntCalibration::new(ina219::calibration::MicroAmpere(1000), 100_000).unwrap();
SyncIna219::new_calibrated(ina_i2c, Address::from_byte(INA219_ADDR).unwrap(), calib)
.map_err(|_| InitError::PowerMonitor)?
};
Ok(Self {
led1,
sw_btn5,
buzzer,
ldr: ldr_driver,
expander,
rtc,
i2c: raw_i2c,
#[cfg(feature = "power-board")]
vol_mgr,
#[cfg(feature = "power-board")]
power_monitor,
_phantom: PhantomData,
})
}
pub fn led1_on(&mut self) {
let _ = self.led1.pin.set_high();
}
pub fn led1_off(&mut self) {
let _ = self.led1.pin.set_low();
}
pub fn led2_on(&mut self) -> Result<(), I2C::Error> {
self.expander.led2_on()
}
pub fn led2_off(&mut self) -> Result<(), I2C::Error> {
self.expander.led2_off()
}
pub fn led3_on(&mut self) -> Result<(), I2C::Error> {
self.expander.led3_on()
}
pub fn led3_off(&mut self) -> Result<(), I2C::Error> {
self.expander.led3_off()
}
pub fn read_sw1(&mut self) -> Result<bool, I2C::Error> {
self.expander.read_sw1()
}
pub fn read_sw2(&mut self) -> Result<bool, I2C::Error> {
self.expander.read_sw2()
}
pub fn read_sw3(&mut self) -> Result<bool, I2C::Error> {
self.expander.read_sw3()
}
pub fn read_sw4(&mut self) -> Result<bool, I2C::Error> {
self.expander.read_sw4()
}
pub fn read_sw5(&mut self) -> bool {
self.sw_btn5.pin.is_low().unwrap_or(false)
}
pub fn read_sw6(&mut self) -> Result<SwPos, I2C::Error> {
self.expander.read_slide_sw6_position()
}
pub fn read_sw7(&mut self) -> Result<SwPos, I2C::Error> {
self.expander.read_slide_sw7_position()
}
pub fn write_seven_segment_digit(
&mut self,
digit: SevenSegDigit,
value: Option<u8>,
) -> Result<(), I2C::Error> {
self.expander.write_seven_segment_digit(digit, value)
}
pub fn seven_segment_display_colon_en(&mut self, enable: bool) -> Result<(), I2C::Error> {
self.expander.seven_segment_display_colon_en(enable)
}
pub fn read_ldr(&mut self) -> u16 {
self.ldr.read_raw()
}
pub fn buzz_on(&mut self, duty: u16) {
let _ = self.buzzer.pin.set_duty_cycle(duty);
}
pub fn buzz_off(&mut self) {
let _ = self.buzzer.pin.set_duty_cycle_fully_off();
}
pub fn set_rtc_time(
&mut self,
year: u16,
month: u8,
day: u8,
hour: u8,
min: u8,
sec: u8,
) -> Result<(), I2C::Error> {
self.rtc.set_time(year, month, day, hour, min, sec)
}
pub fn read_rtc_time(&mut self) -> Result<(u16, u8, u8, u8, u8, u8), I2C::Error> {
self.rtc.read_time()
}
pub fn i2c_write(&mut self, addr: u8, data: &[u8]) -> Result<(), I2C::Error> {
self.i2c.write(addr, data)
}
pub fn i2c_read(&mut self, addr: u8, buffer: &mut [u8]) -> Result<(), I2C::Error> {
self.i2c.read(addr, buffer)
}
pub fn i2c_write_read(
&mut self,
addr: u8,
data: &[u8],
buffer: &mut [u8],
) -> Result<(), I2C::Error> {
self.i2c.write_read(addr, data, buffer)
}
#[cfg(feature = "power-board")]
pub fn read_system_voltage(&mut self) -> Option<u16> {
self.power_monitor
.bus_voltage()
.ok()
.map(|v| v.voltage_mv())
}
#[cfg(feature = "power-board")]
pub fn read_system_current(&mut self) -> Option<i64> {
match self.power_monitor.next_measurement() {
Ok(measurement) => measurement.map(|value| value.current.0),
Err(_) => None,
}
}
#[cfg(feature = "power-board")]
pub fn read_system_power(&mut self) -> Option<i64> {
match self.power_monitor.next_measurement() {
Ok(measurement) => measurement.map(|value| value.power.0),
Err(_) => None,
}
}
#[cfg(feature = "power-board")]
pub fn init_sd_card(&mut self) -> Result<u64, InitError> {
let mgr = self.vol_mgr.as_mut().ok_or(InitError::SdCard)?;
let mut size_bytes = 0u64;
mgr.device(|dev| {
if let Ok(num_blocks) = dev.num_blocks() {
size_bytes = num_blocks.0 as u64 * 512;
}
crate::DummyTimeSource::default()
});
if size_bytes == 0 {
return Err(InitError::SdCard);
}
Ok(size_bytes)
}
#[cfg(feature = "power-board")]
pub fn write_to_file_in_root(&mut self, filename: &str, data: &[u8]) {
let mgr = self.vol_mgr.as_mut().expect("VolMgr missing");
if let Ok(volume) = mgr.open_volume(VolumeIdx(0)) {
if let Ok(root_dir) = volume.open_root_dir() {
if let Ok(file) =
root_dir.open_file_in_dir(filename, FileMode::ReadWriteCreateOrTruncate)
{
let _ = file.write(data);
let _ = file.flush();
} } } }
#[cfg(feature = "power-board")]
pub fn read_file_chunked<F>(&mut self, name: &str, mut f: F) -> Result<(), InitError>
where
F: FnMut(&[u8]),
{
let mgr = self.vol_mgr.as_mut().ok_or(InitError::SdCard)?;
let volume = mgr
.open_volume(VolumeIdx(0))
.map_err(|_| InitError::SdCard)?;
let root_dir = volume.open_root_dir().map_err(|_| InitError::SdCard)?;
let file = root_dir
.open_file_in_dir(name, FileMode::ReadOnly)
.map_err(|_| InitError::SdCard)?;
let mut buffer = [0u8; 64];
while !file.is_eof() {
let bytes_read = file.read(&mut buffer).map_err(|_| InitError::SdCard)?;
if bytes_read > 0 {
f(&buffer[..bytes_read]);
}
}
Ok(())
}
}
#[cfg(feature = "async")]
impl<LED, BTN, BUZZ, I2C, ADC, BD> Uferris<LED, BTN, BUZZ, I2C, ADC, BD, Async>
where
LED: OutputPin,
BTN: embedded_hal_async::digital::Wait,
BUZZ: SetDutyCycle,
I2C: AsyncI2c,
ADC: components::ldr::OneShotAsync,
BD: PowerConstraints,
{
#[cfg_attr(
not(any(
feature = "xiao-nrf52840",
feature = "xiao-nrf54l15",
feature = "xiao-rp2040",
feature = "xiao-rp2350"
)),
allow(dead_code)
)]
async fn new_async(
led1_pin: LED,
sw_btn5_pin: BTN,
pwm_pin: BUZZ,
ldr_driver: ADC,
expander_i2c: I2C,
rtc_i2c: I2C,
raw_i2c: I2C,
) -> Result<Self, InitError> {
let mut board = Self::new_async_preinit(
led1_pin,
sw_btn5_pin,
pwm_pin,
ldr_driver,
expander_i2c,
rtc_i2c,
raw_i2c,
);
board
.expander
.init()
.await
.map_err(|_| InitError::IoExpander)?;
Ok(board)
}
#[cfg_attr(
not(any(
feature = "xiao-esp32c3",
feature = "xiao-esp32c5",
feature = "xiao-esp32c6",
feature = "xiao-esp32s3",
feature = "xiao-nrf52840",
feature = "xiao-nrf54l15",
feature = "xiao-rp2040",
feature = "xiao-rp2350"
)),
allow(dead_code)
)]
fn new_async_preinit(
led1_pin: LED,
sw_btn5_pin: BTN,
pwm_pin: BUZZ,
ldr_driver: ADC,
expander_i2c: I2C,
rtc_i2c: I2C,
raw_i2c: I2C,
) -> Self {
let expander = components::io_expander::IoExpander::new(expander_i2c);
let rtc = components::rtc::Rtc::new(rtc_i2c);
let led1 = components::led::Led { pin: led1_pin };
let sw_btn5 = components::button::Button::new(sw_btn5_pin);
let buzzer = components::buzzer::Buzzer { pin: pwm_pin };
Self {
led1,
sw_btn5,
buzzer,
ldr: ldr_driver,
expander,
rtc,
i2c: raw_i2c,
#[cfg(feature = "power-board")]
vol_mgr: (),
#[cfg(feature = "power-board")]
power_monitor: (),
_phantom: PhantomData,
}
}
pub fn led1_on(&mut self) {
let _ = self.led1.pin.set_high();
}
pub fn led1_off(&mut self) {
let _ = self.led1.pin.set_low();
}
pub async fn led2_on(&mut self) -> Result<(), I2C::Error> {
self.expander.led2_on().await
}
pub async fn led2_off(&mut self) -> Result<(), I2C::Error> {
self.expander.led2_off().await
}
pub async fn led3_on(&mut self) -> Result<(), I2C::Error> {
self.expander.led3_on().await
}
pub async fn led3_off(&mut self) -> Result<(), I2C::Error> {
self.expander.led3_off().await
}
pub async fn read_sw1(&mut self) -> Result<bool, I2C::Error> {
self.expander.read_sw1().await
}
pub async fn read_sw2(&mut self) -> Result<bool, I2C::Error> {
self.expander.read_sw2().await
}
pub async fn read_sw3(&mut self) -> Result<bool, I2C::Error> {
self.expander.read_sw3().await
}
pub async fn read_sw4(&mut self) -> Result<bool, I2C::Error> {
self.expander.read_sw4().await
}
pub async fn wait_for_sw5(&mut self) {
self.sw_btn5.wait_for_press().await;
}
pub async fn read_sw6(&mut self) -> Result<SwPos, I2C::Error> {
self.expander.read_slide_sw6_position().await
}
pub async fn read_sw7(&mut self) -> Result<SwPos, I2C::Error> {
self.expander.read_slide_sw7_position().await
}
pub async fn write_seven_segment_digit(
&mut self,
digit: SevenSegDigit,
value: Option<u8>,
) -> Result<(), I2C::Error> {
self.expander.write_seven_segment_digit(digit, value).await
}
pub async fn seven_segment_display_colon_en(&mut self, enable: bool) -> Result<(), I2C::Error> {
self.expander.seven_segment_display_colon_en(enable).await
}
pub async fn read_ldr(&mut self) -> u16 {
self.ldr.read_raw().await
}
pub fn buzz_on(&mut self, duty: u16) {
let _ = self.buzzer.pin.set_duty_cycle(duty);
}
pub fn buzz_off(&mut self) {
let _ = self.buzzer.pin.set_duty_cycle_fully_off();
}
pub async fn set_rtc_time(
&mut self,
year: u16,
month: u8,
day: u8,
hour: u8,
min: u8,
sec: u8,
) -> Result<(), I2C::Error> {
self.rtc.set_time(year, month, day, hour, min, sec).await
}
pub async fn read_rtc_time(&mut self) -> Result<(u16, u8, u8, u8, u8, u8), I2C::Error> {
self.rtc.read_time().await
}
pub async fn i2c_write(&mut self, addr: u8, data: &[u8]) -> Result<(), I2C::Error> {
self.i2c.write(addr, data).await
}
pub async fn i2c_read(&mut self, addr: u8, buffer: &mut [u8]) -> Result<(), I2C::Error> {
self.i2c.read(addr, buffer).await
}
pub async fn i2c_write_read(
&mut self,
addr: u8,
data: &[u8],
buffer: &mut [u8],
) -> Result<(), I2C::Error> {
self.i2c.write_read(addr, data, buffer).await
}
}
#[cfg(feature = "power-board")]
#[derive(Default, Clone, Copy)]
pub struct DummyTimeSource;
#[cfg(feature = "power-board")]
impl TimeSource for DummyTimeSource {
fn get_timestamp(&self) -> Timestamp {
Timestamp {
year_since_1970: 0,
zero_indexed_month: 0,
zero_indexed_day: 0,
hours: 0,
minutes: 0,
seconds: 0,
}
}
}