use std::fmt::Debug; use std::thread::JoinHandle; use chrono::DateTime; use chrono::Timelike; use chrono::Utc; use esp_idf_svc::eventloop::{EspSubscription, EspSystemEventLoop, System}; use esp_idf_svc::hal::gpio::Pins; use esp_idf_svc::hal::modem::Modem; use esp_idf_svc::hal::prelude::Peripherals; use esp_idf_svc::hal::rmt::RMT; use esp_idf_svc::hal::task::thread::ThreadSpawnConfiguration; use esp_idf_svc::mqtt::client::EspMqttClient; use esp_idf_svc::mqtt::client::MqttClientConfiguration; use esp_idf_svc::netif::IpEvent; use esp_idf_svc::nvs::{EspDefaultNvsPartition, EspNvsPartition, NvsDefault}; use esp_idf_svc::sntp::EspSntp; use esp_idf_svc::sntp::SyncStatus; use esp_idf_svc::sys::esp_efuse_mac_get_default; use esp_idf_svc::wifi::{AuthMethod, ClientConfiguration, Configuration, EspWifi, WifiEvent}; use rgb::Rgb; use super::smart_leds_lib::rmt::FastWs2812Esp32Rmt; use super::smart_leds_lib::StrideOutput; use super::Board; use crate::buffers::BufferedSurfacePool; use crate::buffers::Pixbuf; use crate::events::Event; use crate::events::EventBus; use crate::events::Variant; use crate::lib8::interpolate::lerp8by8; use crate::mappings::StrideMapping; use crate::task::FixedSizeScheduler; use crate::task::Task; use crate::time::Periodically; pub struct Esp32Board { sys_loop: EspSystemEventLoop, modem: Option, pins: Option, rmt: Option, surfaces: BufferedSurfacePool, } impl Board for Esp32Board { type Output = StrideOutput<[Rgb; 310], FastWs2812Esp32Rmt<'static>>; type Surfaces = BufferedSurfacePool; type Scheduler = FixedSizeScheduler<4>; fn chip_id() -> u64 { let mut chip_id: [u8; 8] = [0; 8]; unsafe { esp_efuse_mac_get_default(&mut chip_id as *mut u8); } return u64::from_be_bytes(chip_id); } fn take() -> Self { // It is necessary to call this function once. Otherwise some patches to the runtime // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71 esp_idf_svc::sys::link_patches(); // Bind the log crate to the ESP Logging facilities esp_idf_svc::log::EspLogger::initialize_default(); let peripherals = Peripherals::take().unwrap(); let sys_loop = EspSystemEventLoop::take().unwrap(); Esp32Board { modem: Some(peripherals.modem), sys_loop: sys_loop, surfaces: BufferedSurfacePool::new(), pins: Some(peripherals.pins), rmt: Some(peripherals.rmt) } } fn output(&mut self) -> Self::Output { log::info!("Setting up output for chip ID {:x?}", Self::chip_id()); const POWER_VOLTS : u32 = 5; const POWER_MA : u32 = 500; const MAX_POWER_MW : u32 = POWER_VOLTS * POWER_MA; let pins = self.pins.take().unwrap(); let rmt = self.rmt.take().unwrap(); ThreadSpawnConfiguration { pin_to_core: Some(esp_idf_svc::hal::cpu::Core::Core1), ..Default::default() }.set().unwrap(); // Wifi driver creates too many interrupts on core0, so we need to use RMT on core1. // But the implementation spawns a thread based on the core the driver was created in, // so we create the driver in another thread briefly. // Fun stuff. let output = match Self::chip_id().to_be_bytes() { // panel test board [72, 202, 67, 89, 145, 204, 0, 0] => { StrideOutput::new( Pixbuf::new(), StrideMapping::new_panel(), std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), MAX_POWER_MW ) }, [0x8C, 0xAA, 0xB5, 0x83, 0x5f, 0x74, 0x0, 0x0] => { //ponderjar StrideOutput::new( Pixbuf::new(), StrideMapping::new_jar(), std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio14).unwrap() }).join().unwrap(), MAX_POWER_MW ) }, [0x4a, 0xca, 0x43, 0x59, 0x85, 0x58, 0x0, 0x0] => { // Albus the tree StrideOutput::new( Pixbuf::new(), StrideMapping::new_albus(), std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), MAX_POWER_MW ) }, [0x48, 0xca, 0x43, 0x59, 0x9d, 0x48, 0x0, 0x0] => { // kitchen cabinets StrideOutput::new( Pixbuf::new(), StrideMapping::new_fairylights(), std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), MAX_POWER_MW ) }, [0x48, 0xca, 0x43, 0x59, 0x9e, 0xdc, 0x0, 0x0] => { // front window StrideOutput::new( Pixbuf::new(), StrideMapping::new_fairylights(), std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), MAX_POWER_MW ) }, [0xfc, 0xf5, 0xc4, 0x05, 0xb8, 0x30, 0x0, 0x0] => { // cyberplague StrideOutput::new( Pixbuf::new(), StrideMapping::new_cyberplague(), std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio13).unwrap() }).join().unwrap(), MAX_POWER_MW ) }, _ => { StrideOutput::new( Pixbuf::new(), StrideMapping::new(), std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), MAX_POWER_MW ) } }; ThreadSpawnConfiguration { ..Default::default() }.set().unwrap(); output } fn surfaces(&mut self) -> Self::Surfaces { self.surfaces.clone() } fn system_tasks(&mut self) -> Self::Scheduler { let nvs = EspDefaultNvsPartition::take().unwrap(); FixedSizeScheduler::new([ Box::new(WifiTask::new(self.modem.take().unwrap(), self.sys_loop.clone(), &nvs)), Box::new(CircadianRhythm::new()), Box::new(MqttTask::new()), Box::new(self.surfaces.clone()) ]) } } #[derive(Debug, Clone)] struct ScheduleEntry { hour: u8, brightness: u8 } struct CircadianRhythm { time_check: Periodically, schedule: [ScheduleEntry;10] } impl CircadianRhythm { fn new() -> Self { CircadianRhythm { time_check: Periodically::new_every_n_seconds(60), schedule: [ ScheduleEntry { hour: 0, brightness: 0 }, ScheduleEntry { hour: 5, brightness: 0 }, ScheduleEntry { hour: 6, brightness: 10 }, ScheduleEntry { hour: 7, brightness: 20 }, ScheduleEntry { hour: 8, brightness: 80 }, ScheduleEntry { hour: 11, brightness: 120 }, ScheduleEntry { hour: 18, brightness: 200 }, ScheduleEntry { hour: 19, brightness: 255 }, ScheduleEntry { hour: 22, brightness: 120 }, ScheduleEntry { hour: 23, brightness: 5 } ] } } fn update_brightness(&self, bus: &mut EventBus) { let now: DateTime = std::time::SystemTime::now().into(); let next_brightness = self.brightness_for_time(now.hour() as u8, now.minute() as u8); bus.push(Event::new_property_change("output.brightness", next_brightness)); } fn brightness_for_time(&self, hour: u8, minute: u8) -> u8 { let mut start = self.schedule.last().unwrap(); let mut end = self.schedule.first().unwrap(); for cur in self.schedule.iter() { if cur.hour <= hour { start = cur; } else { end = cur; break; } } log::info!("hour={:?} minute={:?} start={:?} end={:?}", hour, minute, start, end); let mut adjusted_end = end.clone(); if start.hour > end.hour { adjusted_end.hour += 24; } let start_time = (start.hour as u16).wrapping_mul(60); let end_time = (end.hour as u16).wrapping_mul(60); let now_time = (hour as u16).wrapping_mul(60).wrapping_add(minute as u16); let duration = end_time - start_time; let cur_duration = now_time - start_time; let frac = map_range(cur_duration.into(), 0, duration.into(), 0, 255) as u8; lerp8by8(start.brightness, end.brightness, frac) } } fn map_range(x: u16, in_min: u16, in_max: u16, out_min: u16, out_max: u16) -> u16 { let run = in_max - in_min; if run == 0 { return 0; } let rise = out_max - out_min; let delta = x - in_min; return (delta * rise) / run + out_min; } impl Task for CircadianRhythm { fn on_ready(&mut self, bus: &mut EventBus) { self.update_brightness(bus); } fn on_property_change(&mut self, key: &'static str, value: &Variant, bus: &mut EventBus) { match (key, value) { ("system.time.synchronized", Variant::Boolean(true)) => self.update_brightness(bus), _ => () } } fn on_tick(&mut self, bus: &mut EventBus) { if self.time_check.tick() { self.update_brightness(bus); } } } struct MqttTask { client: Option>, conn_thread: Option>, } impl MqttTask { fn new() -> Self { MqttTask { conn_thread: None, client: None, } } fn start_mqtt(&mut self, bus: &EventBus) { log::info!("Starting MQTT"); let chip_id: u64 = bus.property("system.board.chip_id").unwrap().into(); let (client, mut conn) = EspMqttClient::new( "mqtt://10.0.0.2:1883", &MqttClientConfiguration { client_id: Some(&format!("{:X}", chip_id)), ..Default::default() } ).unwrap(); log::info!("Connected!"); self.conn_thread = Some(std::thread::Builder::new() .stack_size(6000) .spawn(move || { conn.next().unwrap(); }).unwrap()); self.client = Some(client); } fn topic_prefix(bus: &EventBus) -> String { let chip_id: u64 = bus.property("system.board.chip_id").unwrap().into(); format!("homeassistant-test/renderbug/{:X}", chip_id) } } impl Task for MqttTask { fn start(&mut self, bus: &mut EventBus) { bus.push(Event::new_property_change("mqtt.online", false)); } fn on_property_change(&mut self, key: &'static str, value: &Variant, bus: &mut EventBus) { match (key, value) { ("system.network.online", Variant::Boolean(true)) => { log::info!("Registering with MQTT"); self.start_mqtt(bus); if let Some(ref mut client) = self.client { client.enqueue( Self::topic_prefix(bus).as_str(), esp_idf_svc::mqtt::client::QoS::AtLeastOnce, false, "hello, world".as_bytes() ).unwrap(); log::info!("MQTT should be online!"); bus.push(Event::new_property_change("mqtt.online", true)); } }, (name, value) => { if let Some(ref mut client) = self.client { let prefix = Self::topic_prefix(bus); client.enqueue( format!("{}/properties/{}", prefix, name).as_str(), esp_idf_svc::mqtt::client::QoS::AtLeastOnce, false, format!("{}", value).as_bytes() ).unwrap(); } } } } } impl Debug for WifiTask { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("WifiTask").finish() } } struct WifiTask { wifi: EspWifi<'static>, ntp: EspSntp<'static>, connection_check: Periodically, sys_loop: EspSystemEventLoop, wifi_sub: Option>, ip_sub: Option>, } impl WifiTask { fn new(modem: Modem, sys_loop: EspSystemEventLoop, nvs: &EspNvsPartition) -> Self { log::info!("Installing wifi driver"); let wifi = EspWifi::new( modem, sys_loop.clone(), Some(nvs.clone()) ).unwrap(); WifiTask { wifi, ntp: EspSntp::new_default().unwrap(), connection_check: Periodically::new_every_n_seconds(1), sys_loop, wifi_sub: None, ip_sub: None, } } fn connect(&mut self) { log::info!("Connecting wifi"); let wifi_config = Configuration::Client(ClientConfiguration { ssid: "The Frequency".try_into().unwrap(), bssid: None, auth_method: AuthMethod::WPA2Personal, password: "thepasswordkenneth".try_into().unwrap(), channel: None, ..Default::default() }); self.wifi.set_configuration(&wifi_config).unwrap(); self.wifi.start().unwrap(); self.wifi.connect().unwrap(); } fn disconnect(&mut self) { log::info!("Disconnecting wifi"); self.wifi.disconnect().unwrap(); self.wifi.stop().unwrap(); } } impl Task for WifiTask { fn start(&mut self, bus: &mut EventBus) { log::info!("Starting wifi!"); let mut wifi_bus = bus.clone(); self.wifi_sub = Some(self.sys_loop.subscribe::( move |evt| { log::debug!("wifi event {:?}", evt); wifi_bus.push(Event::new_property_change("system.network.online", false)); }).unwrap()); let mut ip_bus = bus.clone(); self.ip_sub = Some(self.sys_loop.subscribe::(move |evt| { log::debug!("ip event {:?}", evt); match evt { IpEvent::DhcpIpAssigned(addr) => { ip_bus.push(Event::new_property_change("system.network.ip", addr.ip().to_string())); ip_bus.push(Event::new_property_change("system.network.gateway", addr.gateway().to_string())); ip_bus.push(Event::new_property_change("system.network.online", true)); }, _ => () } }).unwrap()); self.connect(); } fn on_tick(&mut self, bus: &mut EventBus) { if self.connection_check.tick() { if bus.property("system.network.online").unwrap() == Variant::Boolean(true) { match self.ntp.get_sync_status() { SyncStatus::Completed => bus.push(Event::new_property_change("system.time.synchronized", true)), _ => bus.push(Event::new_property_change("system.time.synchronized", false)) } } } } fn stop(&mut self, bus: &mut EventBus) { log::info!("Stopping wifi"); self.wifi_sub.take().unwrap(); self.ip_sub.take().unwrap(); self.disconnect(); bus.push(Event::new_property_change("system.network.online", false)); } }