extern crate blurz; use blurz::bluetooth_adapter::BluetoothAdapter; use blurz::bluetooth_device::BluetoothDevice; use blurz::bluetooth_gatt_characteristic::BluetoothGATTCharacteristic; use blurz::bluetooth_gatt_service::BluetoothGATTService; use blurz::bluetooth_session::BluetoothSession; use globset::{Glob, GlobMatcher}; use serde::Deserialize; use crate::message::{Level, Message}; pub use crate::output::*; pub struct BluetoothLightbulb { config: BluetoothLightbulbConfig, session: BluetoothSession, levels: HashMap, msg_types: Vec<(GlobMatcher, LightConfig)>, } #[derive(Debug, Deserialize)] struct BluetoothLightbulbConfig { #[serde(default)] mac: HashMap, #[serde(default)] levels: HashMap, #[serde(default)] msg_types: HashMap, } #[derive(Debug, Clone, Deserialize)] enum Animation { None, Smooth, Bounce, Blink, RampUp, RampDown, } #[derive(Debug, Clone, Deserialize)] struct LightConfig { #[serde(default)] color: Option, #[serde(default)] animation: Option, #[serde(default)] repeat: Option, #[serde(default)] speed: Option, } impl LightConfig { fn merge(&mut self, other: &Self) { if other.color.is_some() { self.color = other.color; } if other.animation.is_some() { self.animation = other.animation.clone(); } if other.repeat.is_some() { self.repeat = other.repeat; } if other.speed.is_some() { self.speed = other.speed; } } } impl BluetoothLightbulb { fn set_color(&self, characs: &Vec, r: f64, g: f64, b: f64) { let r = (r * 255.0) as u8; let g = (g * 255.0) as u8; let b = (b * 255.0) as u8; // println!("r={} g={} b={}", r, g, b); for charac in characs { charac .write_value(vec![0x55, 0x13, r, g, b, '\r' as u8, '\n' as u8], None) .unwrap(); } } fn initiate_scan(&self, adapter: &BluetoothAdapter) { if !adapter.is_discovering().unwrap() { log::info!("bluetooth adapter {}: Set scan ON", adapter.get_id()); adapter .start_discovery() .expect("Cannot activate scanning on bluetooth device"); } } } impl Output for BluetoothLightbulb { fn new( config: &HashMap, ) -> Result> { let config_node = config .get("config") .unwrap_or(&serde_yaml::Mapping::new().into()) .clone(); let config: BluetoothLightbulbConfig = serde_yaml::from_value(config_node).expect("Invalid config for light_beewi_bbl227"); let msg_types = config .msg_types .iter() .map(|(k, v)| { ( Glob::new(k) .expect("Invalid glob pattern") .compile_matcher(), v.clone(), ) }) .collect(); let levels = config .levels .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(); let session = BluetoothSession::create_session(None).expect("Could not initialize bluetooth session"); let ret = BluetoothLightbulb { config: config, msg_types: msg_types, levels: levels, session: session, }; // Initialize bluetooth device let adapter = BluetoothAdapter::init(&ret.session).expect("Could not initialize bluetooth adapter"); // Power on if !adapter.is_powered().unwrap() { log::info!("bluetooth adapter {}: Set power ON", adapter.get_id()); adapter .set_powered(true) .expect("Cannot power on bluetooth device"); } // Start scan ret.initiate_scan(&adapter); Ok(ret) } fn process_message(&mut self, message: Message) { let adapter = BluetoothAdapter::init(&self.session).expect("Could not initialize bluetooth adapter"); let mut characs = vec![]; let mut found_macs: HashMap = HashMap::new(); for mac in self.config.mac.keys() { found_macs.insert(mac.to_owned(), false); } for dev_path in adapter.get_device_list().unwrap() { let device = BluetoothDevice::new(&self.session, dev_path); let addr = device.get_address().unwrap(); if self.config.mac.contains_key(&addr) { if !device.is_connected().unwrap() { log::info!("Connecting to {}", addr); match device.connect(5000) { Ok(_) => { let power = device.get_tx_power().expect("Could not request tx power"); log::info!("Connected to {}, tx power={}", addr, power); } Err(e) => { log::error!("Could not connect to {}: {}", addr, e); continue; } } } for srv_path in device.get_gatt_services().unwrap() { let service = BluetoothGATTService::new(&self.session, srv_path); let service_uuid = service.get_uuid().unwrap(); if service_uuid == "a8b3fff0-4834-4051-89d0-3de95cddd318" { for charac_path in service.get_gatt_characteristics().unwrap() { let charac = BluetoothGATTCharacteristic::new( &self.session, charac_path.clone(), ); let charac_uuid = charac.get_uuid().unwrap(); if charac_uuid == "a8b3fff1-4834-4051-89d0-3de95cddd318" { characs.push(charac); found_macs.insert(addr.clone(), true); } } } } } } let macs_not_found: HashMap = found_macs .into_iter() .filter(|(_, v)| *v == false) .collect(); if macs_not_found.len() > 0 { log::warn!( "Could not find configured lightbulbs: {:?}", macs_not_found.keys() ); self.initiate_scan(&adapter); return; } // find out color & pattern to play let mut cfg = LightConfig { color: Some(0xffffff), animation: Some(Animation::Smooth), repeat: Some(1), speed: Some(1.0), }; for (type_glob, type_cfg) in &self.msg_types { if type_glob.is_match(&message.msg_type) { cfg.merge(type_cfg); break; } } if let Some(level_cfg) = self.levels.get(&message.level) { cfg.merge(level_cfg); } let color = cfg.color.unwrap(); let r = ((color >> 16) & 0xff) as f64 / 255.0; let g = ((color >> 8) & 0xff) as f64 / 255.0; let b = ((color >> 0) & 0xff) as f64 / 255.0; let anim = cfg.animation.unwrap(); let speed = cfg.speed.unwrap(); // Play animation log::debug!("Playing {:?} {:?}", anim, color); for _ in 0..cfg.repeat.unwrap() { match anim { Animation::None => { self.set_color(&characs, r, g, b); std::thread::sleep(std::time::Duration::from_millis((2000.0 / speed) as u64)); } Animation::Bounce => { for _ in 0..4 { self.set_color(&characs, r, g, b); std::thread::sleep(std::time::Duration::from_millis( (250.0 / speed) as u64, )); self.set_color(&characs, r * 0.5, g * 0.5, b * 0.5); std::thread::sleep(std::time::Duration::from_millis( (250.0 / speed) as u64, )); } } Animation::Smooth => { for i in 0..10 { let mult = i as f64 / 10.0; self.set_color(&characs, r * mult, g * mult, b * mult); } for i in 0..10 { let mult = 1.0 - (i as f64 / 10.0); self.set_color(&characs, r * mult, g * mult, b * mult); } } Animation::Blink => { for i in 0..4 { let (r, g, b) = match i % 2 { 0 => (r, g, b), _ => (0.0, 0.0, 0.0), }; self.set_color(&characs, r, g, b); std::thread::sleep(std::time::Duration::from_millis( (400.0 / speed) as u64, )); } } Animation::RampUp => { for i in 0..20 { let mult = i as f64 / 20.0; self.set_color(&characs, r * mult, g * mult, b * mult); } std::thread::sleep(std::time::Duration::from_millis(100)); self.set_color(&characs, 0.0, 0.0, 0.0); std::thread::sleep(std::time::Duration::from_millis(100)); } Animation::RampDown => { self.set_color(&characs, r, g, b); std::thread::sleep(std::time::Duration::from_millis(100)); for i in 1..20 { let mult = 1.0 - (i as f64 / 20.0); self.set_color(&characs, r * mult, g * mult, b * mult); } self.set_color(&characs, 0.0, 0.0, 0.0); std::thread::sleep(std::time::Duration::from_millis(100)); } } } self.set_color(&characs, 0.0, 0.0, 0.0); } }