#![no_std] #![no_main] #![feature(type_alias_impl_trait)] use core::fmt::Debug; use core::{cell::RefCell, str::from_utf8}; use bme280::{i2c::AsyncBME280, Measurements}; use const_format::formatcp; use cyw43::PowerManagementMode; use cyw43_pio::PioSpi; use defmt::{debug, error, info, warn, Display2Format, Format}; use embassy_boot::{AlignedBuffer, BlockingFirmwareUpdater, FirmwareUpdaterConfig}; use embassy_executor::Spawner; use embassy_futures::select::{select3, select4, Either3}; use embassy_net::{dns::DnsQueryType, tcp::TcpSocket, Config, Stack, StackResources}; use embassy_rp::{ bind_interrupts, clocks::RoscRng, flash::{Flash, WRITE_SIZE}, gpio::{Flex, Input, Level, Output, Pin, Pull}, i2c, peripherals::{DMA_CH1, I2C0, PIN_23, PIN_25, PIO0}, pio::{self, Pio}, Peripheral, }; use embassy_sync::blocking_mutex::Mutex; use embassy_time::{Delay, Duration, Ticker, Timer}; use heapless::Vec; use nourl::Url; use rand::{ rngs::{SmallRng, StdRng}, RngCore, SeedableRng, }; use rust_mqtt::{ client::{ client::MqttClient, client_config::{ClientConfig, MqttVersion}, }, packet::v5::publish_packet::QualityOfService, }; use serde::{Deserialize, Serialize}; use static_cell::make_static; use {defmt_rtt as _, panic_probe as _}; bind_interrupts!(struct Irqs { PIO0_IRQ_0 => pio::InterruptHandler; I2C0_IRQ => i2c::InterruptHandler; }); const ID: &str = env!("ID"); const TOPIC_BASE: &str = formatcp!("pico/{}", ID); const TOPIC_STATUS: &str = formatcp!("{}/status", TOPIC_BASE); const TOPIC_UPDATE: &str = formatcp!("{}/update", TOPIC_BASE); const TOPIC_SET: &str = formatcp!("{}/set", TOPIC_BASE); const VERSION: &str = git_version::git_version!(); const PUBLIC_SIGNING_KEY: &[u8; 32] = include_bytes!("../key.pub"); const FLASH_SIZE: usize = 2 * 1024 * 1024; #[derive(Deserialize)] struct SetMessage { state: State, } #[derive(Serialize)] struct StateMessage { state: State, manual: bool, temperature: f32, humidity: f32, pressure: f32, } impl StateMessage { pub fn new((state, manual): (State, bool), measurements: Measurements) -> Self { Self { state, manual, temperature: measurements.temperature, humidity: measurements.humidity, pressure: measurements.pressure, } } pub fn vec(&self) -> Vec { serde_json_core::to_vec(self) .expect("The buffer should be large enough to contain all the data") } } #[derive(Format, PartialEq, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] enum State { Off, Low, Medium, High, } impl SetMessage { fn get_state(&self) -> State { self.state } } struct Controller<'a, O: Pin, L: Pin, M: Pin, H: Pin> { off: Input<'a, O>, low: Flex<'a, L>, medium: Flex<'a, M>, high: Flex<'a, H>, } impl<'a, O: Pin, L: Pin, M: Pin, H: Pin> Controller<'a, O, L, M, H> { 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) -> (State, bool) { let manual = self.off.is_high(); let state = match (self.low.is_low(), self.medium.is_low(), self.high.is_low()) { (false, false, false) => State::Off, (true, false, false) => State::Low, (false, true, false) => State::Medium, (false, false, true) => State::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_state(State::Off); State::Off } }; (state, manual) } pub fn set_state(&mut self, state: State) { let manual = self.off.is_high(); if manual && state != State::Off { warn!("Filter is manual controlled, cannot control remotely"); return; } debug!("Setting state: {}", state); match state { State::Off => { self.low.set_as_input(); self.medium.set_as_input(); self.high.set_as_input(); } State::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(); } State::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(); } State::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); } } } pub async fn watch(&mut self) -> (State, bool) { // Wait for change on any of the pins select4( self.off.wait_for_any_edge(), self.low.wait_for_any_edge(), self.medium.wait_for_any_edge(), self.high.wait_for_any_edge(), ) .await; // Give it some time to stabilze Timer::after(Duration::from_millis(500)).await; if self.off.is_high() { // If the filter is in manual mode, set the pico outputs to off self.set_state(State::Off); } // Get the current state self.get_state() } } /// 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) } } } async fn wait_for_config( stack: &'static Stack>, ) -> embassy_net::StaticConfigV4 { for _ in 0..120 { // We are essentially busy looping here since there is no Async API for this if let Some(config) = stack.config_v4() { return config; } Timer::after_secs(1).await; } info!("Restarting..."); cortex_m::peripheral::SCB::sys_reset(); } #[embassy_executor::task] async fn wifi_task( runner: cyw43::Runner< 'static, Output<'static, PIN_23>, PioSpi<'static, PIN_25, PIO0, 0, DMA_CH1>, >, ) -> ! { runner.run().await } #[embassy_executor::task] async fn net_task(stack: &'static Stack>) -> ! { stack.run().await } #[embassy_executor::main] async fn main(spawner: Spawner) { info!("Starting..."); let p = embassy_rp::init(Default::default()); // TODO: Ideally we use async flash // This has issues with alignment right now let flash = Flash::<_, _, FLASH_SIZE>::new_blocking(p.FLASH); let flash = Mutex::new(RefCell::new(flash)); let config = FirmwareUpdaterConfig::from_linkerfile_blocking(&flash); let mut aligned = AlignedBuffer([0; WRITE_SIZE]); let updater = BlockingFirmwareUpdater::new(config, &mut aligned.0); let mut updater = updater::Updater::new(updater, TOPIC_STATUS, VERSION, PUBLIC_SIGNING_KEY); let mut 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, pio.irq0, cs, p.PIN_24, p.PIN_29, p.DMA_CH1, ); let (fw, clm) = unsafe { get_firmware() }; let state = make_static!(cyw43::State::new()); let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await; spawner.spawn(wifi_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 config = Config::dhcpv4(Default::default()); // 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(); let stack = make_static!(Stack::new( net_device, config, make_static!(StackResources::<6>::new()), rng.next_u64(), )); spawner.spawn(net_task(stack)).unwrap(); // Connect to wifi loop { match control .join_wpa2(env!("WIFI_NETWORK"), env!("WIFI_PASSWORD")) .await { Ok(_) => break, Err(err) => { info!("Failed to join with status = {}", err.status) } } } info!("Waiting for DHCP..."); let cfg = wait_for_config(stack).await; info!("IP Address: {}", cfg.address.address()); let mut rx_buffer = [0; 1024]; let mut tx_buffer = [0; 1024]; let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); // socket.set_timeout(Some(Duration::from_secs(10))); let url = Url::parse(env!("MQTT_ADDRESS")).unwrap(); debug!("MQTT URL: {}", url); let ip = stack.dns_query(url.host(), DnsQueryType::A).await.unwrap()[0]; let addr = (ip, url.port_or_default()); debug!("MQTT ADDR: {}", addr); while let Err(e) = socket.connect(addr).await { warn!("Connect error: {:?}", e); Timer::after(Duration::from_secs(1)).await; } info!("TCP Connected!"); let mut config = ClientConfig::new( MqttVersion::MQTTv5, // Use fast and simple PRNG to generate packet identifiers, there is no need for this to be // cryptographically secure SmallRng::from_rng(&mut RoscRng).unwrap(), ); config.add_username(env!("MQTT_USERNAME")); config.add_password(env!("MQTT_PASSWORD")); config.add_max_subscribe_qos(QualityOfService::QoS1); config.add_client_id(ID); updater.add_will(&mut config); let mut recv_buffer = [0; 1024]; let mut write_buffer = [0; 1024]; let mut client = MqttClient::<_, 5, _>::new( socket, &mut write_buffer, 1024, &mut recv_buffer, 1024, config, ); info!("Connecting to MQTT..."); if client.connect_to_broker().await.is_err() { cortex_m::peripheral::SCB::sys_reset(); }; info!("MQTT Connected!"); // We wait with marking as booted until everything is connected client.subscribe_to_topic(TOPIC_UPDATE).await.unwrap(); client.subscribe_to_topic(TOPIC_SET).await.unwrap(); updater.ready(&mut client).await.unwrap(); // Turn LED off when connected control.gpio_set(0, false).await; let mut keep_alive = Ticker::every(Duration::from_secs(30)); loop { let message = match select3( keep_alive.next(), client.receive_message(), controller.watch(), ) .await { Either3::First(_) => Some(StateMessage::new( controller.get_state(), bme280.measure(&mut delay).await.unwrap(), )), Either3::Second(message) => match message { Ok((TOPIC_UPDATE, url)) => { let url: Vec<_, 256> = match Vec::from_slice(url) { Ok(url) => url, Err(_) => { error!("URL is longer then buffer size"); continue; } }; let url = match from_utf8(&url) { Ok(url) => url, Err(err) => { error!("Url is not valid utf-8 string: {}", Display2Format(&err)); continue; } }; let url = match Url::parse(url) { Ok(url) => url, Err(err) => { error!("Failed to parse url: {}", err); continue; } }; if let Err(err) = updater.update(url, stack, &mut rng, &mut client).await { error!("Update failed: {}", err); } None } Ok((TOPIC_SET, message)) => { let message: SetMessage = match serde_json_core::from_slice(message) { Ok((message, _)) => message, Err(_) => { error!("Unable to parse set message"); continue; } }; controller.set_state(message.get_state()); Some(StateMessage::new( controller.get_state(), bme280.measure(&mut delay).await.unwrap(), )) } Ok(_) => None, Err(err) => { error!("{}", err); info!("Restarting in 5s..."); Timer::after(Duration::from_secs(5)).await; info!("Restarting..."); cortex_m::peripheral::SCB::sys_reset(); } }, Either3::Third(state) => Some(StateMessage::new( state, bme280.measure(&mut delay).await.unwrap(), )), }; if let Some(message) = message { client .send_message(TOPIC_BASE, &message.vec(), QualityOfService::QoS1, true) .await .unwrap(); } } }