#![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!()]
#[cfg(not(any(
feature = "xiao-esp32c3",
feature = "xiao-esp32c6",
feature = "xiao-esp32s3"
)))]
compile_error!("At least one Xiao device feature must be enabled");
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 = "power-board")]
use embedded_sdmmc::{BlockDevice, Mode, 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(feature = "xiao-esp32c6")]
pub use boards::xiao_esp32c6::uferris_init;
#[cfg(feature = "xiao-esp32s3")]
pub use boards::xiao_esp32s3::uferris_init;
#[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 {}
#[cfg(feature = "power-board")]
const INA219_ADDR: u8 = 0x45;
#[derive(Debug, Clone)]
pub struct InitError;
impl fmt::Display for InitError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "uFerris initialization error")
}
}
pub struct Uferris<LED, BTN, BUZZ, I2C, ADC, BD>
where
LED: OutputPin,
BTN: InputPin,
BUZZ: SetDutyCycle,
I2C: I2c,
ADC: components::ldr::OneShot,
BD: PowerConstraints,
{
pub led1: components::led::Led<LED>,
pub sw_btn5: components::button::Button<BTN>,
pub buzzer: components::buzzer::Buzzer<BUZZ>,
pub ldr: ADC,
pub expander: components::io_expander::IoExpander<I2C>,
pub rtc: components::rtc::Rtc<I2C>,
pub i2c: I2C,
#[cfg(feature = "power-board")]
pub vol_mgr: Option<VolumeManager<BD, DummyTimeSource>>,
#[cfg(feature = "power-board")]
pub power_monitor: SyncIna219<I2C, IntCalibration>,
pub _phantom: PhantomData<BD>,
}
impl<LED, BTN, BUZZ, I2C, ADC, BD> Uferris<LED, BTN, BUZZ, I2C, ADC, BD>
where
LED: OutputPin,
BTN: InputPin,
BUZZ: SetDutyCycle,
I2C: I2c,
ADC: components::ldr::OneShot,
BD: PowerConstraints,
{
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)?;
let led1 = components::led::Led { pin: led1_pin };
let sw_btn5 = components::button::Button { pin: 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)?
};
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)?;
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);
}
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, Mode::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)?;
let volume = mgr.open_volume(VolumeIdx(0)).map_err(|_| InitError)?;
let root_dir = volume.open_root_dir().map_err(|_| InitError)?;
let file = root_dir
.open_file_in_dir(name, Mode::ReadOnly)
.map_err(|_| InitError)?;
let mut buffer = [0u8; 64];
while !file.is_eof() {
let bytes_read = file.read(&mut buffer).map_err(|_| InitError)?;
if bytes_read > 0 {
f(&buffer[..bytes_read]);
}
}
Ok(())
}
}
#[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,
}
}
}