#![no_std] #![no_main] #![feature(impl_trait_in_assoc_type)] #![feature(type_alias_impl_trait)] use core::{cell::RefCell, net::Ipv4Addr, str::FromStr}; use approuter::{AppRouter, AppState}; use bme280::i2c::AsyncBME280; use cyw43::{JoinOptions, PowerManagementMode}; use cyw43_pio::{PioSpi, DEFAULT_CLOCK_DIVIDER}; use defmt::{debug, info, warn}; use embassy_boot::{AlignedBuffer, BlockingFirmwareUpdater, FirmwareUpdaterConfig}; use embassy_embedded_hal::flash::partition::BlockingPartition; use embassy_executor::Spawner; use embassy_net::{ tcp::{ client::{TcpClient, TcpClientState}, TcpSocket, }, Config, DhcpConfig, Stack, StackResources, }; use embassy_rp::{ bind_interrupts, clocks::RoscRng, flash::{Flash, WRITE_SIZE}, gpio::{Flex, Input, Level, Output, Pin, Pull}, i2c::{self}, peripherals::{DMA_CH1, FLASH, I2C0, PIO0}, pio::{self, Pio}, Peripheral, }; use embassy_sync::{ blocking_mutex::{ self, raw::{CriticalSectionRawMutex, NoopRawMutex}, }, mutex::Mutex, }; use embassy_time::{Delay, Duration, Timer}; use heapless::String; use picoserve::{make_static, Router}; use rand::{rngs::StdRng, RngCore, SeedableRng}; use static_cell::StaticCell; use {defmt_rtt as _, panic_probe as _}; use air_filter_types::{FanSpeed, FanState}; bind_interrupts!(struct Irqs { PIO0_IRQ_0 => pio::InterruptHandler; I2C0_IRQ => i2c::InterruptHandler; }); const FLASH_SIZE: usize = 2 * 1024 * 1024; struct Controller<'a> { off: Input<'a>, low: Flex<'a>, medium: Flex<'a>, high: Flex<'a>, } impl<'a> Controller<'a> { pub fn new( off: impl Peripheral

+ 'a, low: impl Peripheral

+ 'a, medium: impl Peripheral

+ 'a, high: impl Peripheral

+ 'a, ) -> Self { let off = Input::new(off, Pull::None); let mut low = Flex::new(low); low.set_low(); low.set_as_input(); let mut medium = Flex::new(medium); medium.set_low(); medium.set_as_input(); let mut high = Flex::new(high); high.set_low(); high.set_as_input(); Self { off, low, medium, high, } } pub fn get_state(&mut self) -> FanState { let manual = self.off.is_high(); let speed = match (self.low.is_low(), self.medium.is_low(), self.high.is_low()) { (false, false, false) => FanSpeed::Off, (true, false, false) => FanSpeed::Low, (false, true, false) => FanSpeed::Medium, (false, false, true) => FanSpeed::High, (a, b, c) => { // This happens if the user turns the knob, in this case we should turn off remote // control debug!("Unknown state: ({}, {}, {})", a, b, c); self.set_speed(FanSpeed::Off); FanSpeed::Off } }; FanState { speed, manual } } pub fn set_speed(&mut self, speed: FanSpeed) -> bool { let manual = self.off.is_high(); if manual && speed != FanSpeed::Off { warn!("Filter is manual controlled, cannot control remotely"); return false; } debug!("Setting state: {}", speed); match speed { FanSpeed::Off => { self.low.set_as_input(); self.medium.set_as_input(); self.high.set_as_input(); } FanSpeed::Low => { self.low.set_as_output(); self.low.set_drive_strength(embassy_rp::gpio::Drive::_12mA); self.medium.set_as_input(); self.high.set_as_input(); } FanSpeed::Medium => { self.low.set_as_input(); self.medium.set_as_output(); self.medium .set_drive_strength(embassy_rp::gpio::Drive::_12mA); self.high.set_as_input(); } FanSpeed::High => { self.low.set_as_input(); self.medium.set_as_input(); self.high.set_as_output(); self.high.set_drive_strength(embassy_rp::gpio::Drive::_12mA); } } true } pub fn check_for_manual(&mut self) { // If off is high, that means that the knob is used to override the state if self.off.is_high() { self.set_speed(FanSpeed::Off); } } } // We need this because otherwise the compiler yells at us mod approuter { use air_filter_types::{SensorData, SetFanSpeed}; use bme280::i2c::AsyncBME280; use defmt::debug; use embassy_boot::BlockingFirmwareUpdater; use embassy_rp::{ i2c::{Async, I2c}, peripherals::I2C0, }; use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; use embassy_time::Delay; use picoserve::{ extract, make_static, routing::{get, PathRouter}, Router, }; use updater::firmware_router; use crate::{Controller, Partition}; const VERSION: &str = git_version::git_version!(); const PUBLIC_SIGNING_KEY: &[u8; 32] = include_bytes!("../key.pub"); #[derive(Clone, Copy)] pub struct SharedController(pub &'static Mutex>); impl SharedController { fn new(controller: Controller<'static>) -> Self { Self( make_static!(Mutex>, Mutex::new(controller)), ) } } #[derive(Clone, Copy)] pub struct SharedBME280( pub &'static Mutex>>, ); impl SharedBME280 { fn new(bme280: AsyncBME280>) -> Self { Self( make_static!(Mutex>>, Mutex::new(bme280)), ) } } #[derive(Clone)] pub struct AppState { pub shared_controller: SharedController, pub shared_bme280: SharedBME280, } impl AppState { pub fn new( controller: Controller<'static>, bme280: AsyncBME280>, ) -> Self { Self { shared_controller: SharedController::new(controller), shared_bme280: SharedBME280::new(bme280), } } } impl picoserve::extract::FromRef for SharedController { fn from_ref(state: &AppState) -> Self { state.shared_controller } } impl picoserve::extract::FromRef for SharedBME280 { fn from_ref(state: &AppState) -> Self { state.shared_bme280 } } fn state_router() -> Router, AppState> { Router::new() .route( "/fan", get( |extract::State(SharedController(controller)): extract::State< SharedController, >| async { debug!("Getting fan state"); let state = controller.lock().await.get_state(); picoserve::response::Json(state) }, ) .put( |extract::State(SharedController(controller)): extract::State< SharedController, >, extract::Json(message): extract::Json| async move { debug!("Setting fan state"); let success = controller.lock().await.set_speed(message.speed()); if success { picoserve::response::StatusCode::OK } else { picoserve::response::StatusCode::SERVICE_UNAVAILABLE } }, ), ) .route( "/sensor", get( |extract::State(SharedBME280(bme280)): extract::State| async { debug!("Getting sensor state"); let measurement = bme280 .lock() .await .measure(&mut Delay {}) .await .expect("Measurement should work"); let sensor_data = SensorData::new(measurement); picoserve::response::Json(sensor_data) }, ), ) } pub type AppRouter = impl PathRouter; pub fn make_app( updater: &'static Mutex< CriticalSectionRawMutex, BlockingFirmwareUpdater<'static, Partition, Partition>, >, ) -> Router { Router::new().nest("/state", state_router()).nest( "/firmware", firmware_router(VERSION, updater, PUBLIC_SIGNING_KEY), ) } } /// Get the cyw43 firmware blobs /// /// # Safety /// When building without `include_firmwares` make sure to flash the firmwares using the following /// commands: /// ```bash /// probe-rs download firmware/43439A0.bin --format bin --chip RP2040 --base-address 0x101BE000 /// probe-rs download firmware/43439A0_clm.bin --format bin --chip RP2040 --base-address 0x101FE000 /// ``` unsafe fn get_firmware() -> (&'static [u8], &'static [u8]) { cfg_if::cfg_if! { if #[cfg(feature = "include_firmwares")] { let fw = include_bytes!("../firmware/43439A0.bin"); let clm = include_bytes!("../firmware/43439A0_clm.bin"); (fw, clm) } else { // TODO: It would be nice if it could automatically get the correct size extern "C" { #[link_name = "__fw_start"] static fw: [u8; 230321]; #[link_name = "__clm_start"] static clm: [u8; 4752]; } (&fw, &clm) } } } #[embassy_executor::task] async fn cyw43_task( runner: cyw43::Runner<'static, Output<'static>, PioSpi<'static, PIO0, 0, DMA_CH1>>, ) -> ! { runner.run().await } #[embassy_executor::task] async fn net_task(mut runner: embassy_net::Runner<'static, cyw43::NetDriver<'static>>) -> ! { runner.run().await } type Partition = BlockingPartition< 'static, NoopRawMutex, Flash<'static, FLASH, embassy_rp::flash::Blocking, FLASH_SIZE>, >; const WEB_TASK_POOL_SIZE: usize = 8; #[embassy_executor::task(pool_size = WEB_TASK_POOL_SIZE)] async fn web_task( id: usize, stack: Stack<'static>, app: &'static Router, config: &'static picoserve::Config, state: AppState, ) -> ! { let port = 80; let mut tcp_rx_buffer = [0; 1024]; let mut tcp_tx_buffer = [0; 1024]; let mut http_buffer = [0; 2048]; picoserve::listen_and_serve_with_state( id, app, config, stack, port, &mut tcp_rx_buffer, &mut tcp_tx_buffer, &mut http_buffer, &state, ) .await; } #[embassy_executor::task] async fn controller_task(state: AppState) { loop { state.shared_controller.0.lock().await.check_for_manual(); Timer::after(Duration::from_millis(500)).await; } } #[embassy_executor::main] async fn main(spawner: Spawner) { info!("Starting..."); let p = embassy_rp::init(Default::default()); let flash = Flash::<_, _, FLASH_SIZE>::new_blocking(p.FLASH); let flash = make_static!(blocking_mutex::Mutex>>, blocking_mutex::Mutex::new(RefCell::new(flash))); let config = FirmwareUpdaterConfig::from_linkerfile_blocking(flash, flash); let aligned = make_static!(AlignedBuffer, AlignedBuffer([0; WRITE_SIZE])); let updater = BlockingFirmwareUpdater::new(config, &mut aligned.0); let updater = make_static!(Mutex>, Mutex::new(updater)); let controller = Controller::new(p.PIN_28, p.PIN_27, p.PIN_26, p.PIN_22); let i2c = i2c::I2c::new_async(p.I2C0, p.PIN_9, p.PIN_8, Irqs, i2c::Config::default()); let mut bme280 = AsyncBME280::new_primary(i2c); let mut delay = Delay {}; bme280.init(&mut delay).await.unwrap(); let pwr = Output::new(p.PIN_23, Level::Low); let cs = Output::new(p.PIN_25, Level::High); let mut pio = Pio::new(p.PIO0, Irqs); let spi = PioSpi::new( &mut pio.common, pio.sm0, DEFAULT_CLOCK_DIVIDER, pio.irq0, cs, p.PIN_24, p.PIN_29, p.DMA_CH1, ); let (fw, clm) = unsafe { get_firmware() }; static STATE: StaticCell = StaticCell::new(); let state = STATE.init(cyw43::State::new()); let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await; spawner.spawn(cyw43_task(runner)).unwrap(); control.init(clm).await; control .set_power_management(PowerManagementMode::PowerSave) .await; // Turn LED on while trying to connect control.gpio_set(0, true).await; let mut config: DhcpConfig = Default::default(); config.hostname = Some(String::from_str("airfilter").expect("Is valid")); let config = Config::dhcpv4(config); // Use the Ring Oscillator of the RP2040 as a source of true randomness to seed the // cryptographically secure PRNG let mut rng = StdRng::from_rng(&mut RoscRng).unwrap(); static RESOURCES: StaticCell> = StaticCell::new(); let (stack, runner) = embassy_net::new( net_device, config, RESOURCES.init(StackResources::new()), rng.next_u64(), ); spawner.spawn(net_task(runner)).unwrap(); // Connect to wifi loop { match control .join( env!("WIFI_NETWORK"), JoinOptions::new(env!("WIFI_PASSWORD").as_bytes()), ) .await { Ok(_) => break, Err(err) => { info!("Failed to join with status = {}", err.status) } } } // Mark the update as successful after connecting to the wifi updater.lock().await.mark_booted().unwrap(); info!("Waiting for link up..."); stack.wait_link_up().await; info!("Link is up!"); info!("Waiting for stack to be up..."); stack.wait_config_up().await; info!("Stack is up!"); // Turn LED off when connected control.gpio_set(0, false).await; let cfg = stack.config_v4().expect("Config is up"); info!("IP Address: {}", cfg.address.address()); let config = make_static!( picoserve::Config, picoserve::Config::new(picoserve::Timeouts { start_read_request: Some(Duration::from_secs(5)), read_request: Some(Duration::from_secs(1)), write: Some(Duration::from_secs(1)), }) .keep_connection_alive() ); let app = make_static!(Router, approuter::make_app(updater)); let state = AppState::new(controller, bme280); for id in 0..WEB_TASK_POOL_SIZE { spawner.must_spawn(web_task(id, stack, app, config, state.clone())); } spawner.must_spawn(controller_task(state)); let mut rx_buffer = [0; 4096]; let mut tx_buffer = [0; 4096]; loop { // TODO: In the future, use reqless to push the current state to automation_rs control.gpio_set(0, true).await; let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); socket.set_timeout(Some(Duration::from_secs(1))); socket .connect(("10.0.0.2".parse::().unwrap(), 80)) .await .ok(); control.gpio_set(0, false).await; Timer::after(Duration::from_secs(60)).await; } }