lilkaoxide 0.1.0

Rust support library for Lilka console
#![no_std]
//! ## Memory/lifetime rules for future changes
//! 1. Peripherals are single-owner resources: move each pin/peripheral exactly once into its driver.
//! 2. Shared SPI bus state is placed in `StaticCell`, so returned bus references are `'static`.
//! 3. DMA descriptors/buffers come from static storage (`dma_buffers!`), so their size must be const.
//! 4. Keep long-lived drivers (display, SD manager, I2S TX) inside `Lilka` to preserve ownership and lifetimes.
//! 5. For embassy-async builds, never store borrowed stack data across `.await`; keep data owned or `'static`.
//! 6. Feature pairs are exclusive: `blocking` vs `async`, `RefCellBus` vs `CriticalSectionBus`.

use crate::cfg::Configuration;
use esp_hal::Config;
#[cfg(feature = "async")]
use esp_hal::interrupt::software::SoftwareInterruptControl;
use esp_hal::peripherals::Peripherals;
#[cfg(feature = "async")]
use esp_hal::timer::timg::TimerGroup;
use esp_println as _;
mod adcbattery;
mod buzzer;
mod cfg;
mod controller;
mod display;
mod i2s;
#[cfg(feature = "radio")]
mod radio;
mod sd;
mod spi;

pub use adcbattery::{AdcCfg, Adcbattery};
pub use buzzer::{Buzzer, BuzzerInitError};
pub use cfg::{BuzzerConfig, Configuration as LilkaConfiguration};
pub use controller::ControllerState;
pub use display::DisplayInitError;
pub use display::LilkaDisplay;
pub use i2s::{I2sInitError, I2sTx, I2sTxConfig};
pub use sd::{SdInitError, SdVolMgr};
pub use spi::{
    InnerDelay as LilkaDelay, SpiBusInner as LilkaSpiBus, SpiBusWrapper, SpiDev, SpiInitError,
    init_bus, init_spi_master,
};

use crate::sd::SDC;
pub use defmt;
#[cfg(feature = "async")]
pub use embassy_time;
pub use embedded_sdmmc;
pub use esp_hal as hal;
#[cfg(feature = "async")]
pub use esp_rtos;
pub use mipidsi;

pub mod prelude {
    #[cfg(feature = "async")]
    pub use crate::RtosRuntime;
    pub use crate::cfg::{BuzzerConfig, Configuration};
    pub use crate::{Lilka, LilkaConfiguration};
    #[cfg(feature = "async")]
    pub use embassy_executor;
    #[cfg(feature = "async")]
    pub use embassy_time;
    pub use embedded_sdmmc;
    pub use esp_hal;
    #[cfg(feature = "async")]
    pub use esp_rtos;
    pub use mipidsi;
}
pub struct Lilka {
    pub peripherals: Peripherals,
    #[cfg(feature = "async")]
    pub rtos: RtosRuntime,
    pub delay: LilkaDelay,
    pub controller: ControllerState,
    pub adc_battery: Adcbattery<'static>,
    pub display: LilkaDisplay,
    pub sd: SDC<'static>,
    pub i2s_tx: I2sTx<'static>,
    pub buzzer: Buzzer,
}

#[cfg(feature = "async")]
pub struct RtosRuntime {
    started: bool,
}

#[cfg(feature = "async")]
impl RtosRuntime {
    fn start(
        timg0: esp_hal::peripherals::TIMG0<'static>,
        sw_interrupt: esp_hal::peripherals::SW_INTERRUPT<'static>,
    ) -> Self {
        let timg0 = TimerGroup::new(timg0);
        let sw_interrupt = SoftwareInterruptControl::new(sw_interrupt);
        esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);

        Self { started: true }
    }

    pub const fn is_started(&self) -> bool {
        self.started
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum InitError {
    Spi(SpiInitError),
    Display(DisplayInitError),
    Sd(SdInitError),
    I2s(I2sInitError),
    Buzzer(BuzzerInitError),
}

#[derive(Clone, Copy)]
enum StepStatus {
    Ok,
    Error(&'static str),
    Skipped,
}

impl StepStatus {
    fn log(self, step: &'static str) {
        match self {
            Self::Ok => defmt::info!("[OK] {=str}", step),
            Self::Error(reason) => defmt::error!("[ERR] {=str}: {=str}", step, reason),
            Self::Skipped => defmt::warn!("[SKIP] {=str}", step),
        }
    }
}

#[derive(Clone, Copy)]
struct InitStatusLog {
    spi: StepStatus,
    display: StepStatus,
    sd: StepStatus,
    controller: StepStatus,
    adc_battery: StepStatus,
    i2s_tx: StepStatus,
    buzzer: StepStatus,
}

impl Default for InitStatusLog {
    fn default() -> Self {
        Self {
            spi: StepStatus::Skipped,
            display: StepStatus::Skipped,
            sd: StepStatus::Skipped,
            controller: StepStatus::Skipped,
            adc_battery: StepStatus::Skipped,
            i2s_tx: StepStatus::Skipped,
            buzzer: StepStatus::Skipped,
        }
    }
}

fn log_init_status(status: &InitStatusLog) {
    defmt::info!("Init status summary:");
    status.spi.log("SPI");
    status.display.log("Display");
    status.sd.log("SD");
    status.controller.log("Controller");
    status.adc_battery.log("ADC battery");
    status.i2s_tx.log("I2S TX");
    status.buzzer.log("Buzzer");
}

impl Lilka {
    /// Initializes all core subsystems (controller, ADC battery, display, SD, I2S TX, buzzer).
    ///
    /// Expected flow:
    /// 1. Pass `config`
    /// 2. Call this method; delay is selected internally by active feature
    /// 3. SPI2 uses board pins configured internally (SCK=GPIO12, MOSI=GPIO13, MISO=GPIO14)
    pub fn init(config: Configuration) -> Result<Self, InitError> {
        let mut status = InitStatusLog::default();
        let Configuration {
            cpu_clock,
            spi_frequency,
            display_refresh_order,
            display_rotation,
            display_tearing,
            adc_cfg,
            i2s_tx_config,
            buzzer_config,
        } = config;
        let peripherals = esp_hal::init(Config::default().with_cpu_clock(cpu_clock));
        #[cfg(feature = "async")]
        let rtos = RtosRuntime::start(peripherals.TIMG0, peripherals.SW_INTERRUPT);
        let spi = match init_spi_master(
            peripherals.SPI2,
            peripherals.GPIO18,
            peripherals.GPIO17,
            peripherals.GPIO8,
            spi_frequency,
        ) {
            Ok(spi) => {
                status.spi = StepStatus::Ok;
                spi
            }
            Err(err) => {
                status.spi = StepStatus::Error(err.as_str());
                log_init_status(&status);
                return Err(InitError::Spi(err));
            }
        };
        #[cfg(feature = "blocking")]
        let delay = spi::InnerDelay::new();
        #[cfg(feature = "async")]
        let delay = embassy_time::Delay;
        let spi_bus = init_bus(spi);
        let display = match display::init_display(
            peripherals.GPIO46,
            peripherals.GPIO15,
            peripherals.GPIO7,
            spi_bus,
            delay.clone(),
            display_refresh_order,
            display_rotation,
            display_tearing,
        ) {
            Ok(display) => {
                status.display = StepStatus::Ok;
                display
            }
            Err(err) => {
                status.display = StepStatus::Error(err.as_str());
                log_init_status(&status);
                return Err(InitError::Display(err));
            }
        };
        let sd = match sd::init_sd(spi_bus, peripherals.GPIO16, delay.clone()) {
            Ok(sd) => {
                status.sd = StepStatus::Ok;
                sd
            }
            Err(err) => {
                status.sd = StepStatus::Error(err.as_str());
                log_init_status(&status);
                return Err(InitError::Sd(err));
            }
        };
        let controller = ControllerState::init_controller(
            peripherals.GPIO5,
            peripherals.GPIO6,
            peripherals.GPIO10,
            peripherals.GPIO9,
            peripherals.GPIO38,
            peripherals.GPIO41,
            peripherals.GPIO39,
            peripherals.GPIO40,
            peripherals.GPIO0,
            peripherals.GPIO4,
        );
        status.controller = StepStatus::Ok;
        let adc_battery = adcbattery::init_adcbattery(peripherals.GPIO3, adc_cfg, peripherals.ADC1);
        status.adc_battery = StepStatus::Ok;
        let i2s_tx = match i2s::init_i2s_tx(
            i2s_tx_config,
            peripherals.GPIO42,
            peripherals.GPIO2,
            peripherals.GPIO1,
            peripherals.I2S0,
            peripherals.DMA_CH0,
        ) {
            Ok(i2s_tx) => {
                status.i2s_tx = StepStatus::Ok;
                i2s_tx
            }
            Err(err) => {
                status.i2s_tx = StepStatus::Error(err.as_str());
                log_init_status(&status);
                return Err(InitError::I2s(err));
            }
        };

        let buzzer = match buzzer::init_buzzer(buzzer_config, peripherals.LEDC, peripherals.GPIO11)
        {
            Ok(buzzer) => {
                status.buzzer = StepStatus::Ok;
                buzzer
            }
            Err(err) => {
                status.buzzer = StepStatus::Error(err.as_str());
                log_init_status(&status);
                return Err(InitError::Buzzer(err));
            }
        };

        log_init_status(&status);

        Ok(Self {
            peripherals: unsafe { Peripherals::steal() },
            #[cfg(feature = "async")]
            rtos,
            delay,
            controller,
            adc_battery,
            display,
            sd,
            i2s_tx,
            buzzer,
        })
    }
}