extern crate blurz; use blurz::bluetooth_adapter::BluetoothAdapter; use blurz::bluetooth_device::BluetoothDevice; use blurz::bluetooth_discovery_session::BluetoothDiscoverySession; 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, targets: &Vec<(BluetoothDevice, BluetoothGATTCharacteristic)>, 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 targets { 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!("{}: Searching for new devices...", adapter.get_id()); let disc_session = BluetoothDiscoverySession::create_session(&self.session, adapter.get_id()) .expect("Could not create discovery session"); disc_session .start_discovery() .expect("Could not start discovery session"); } } } impl Output for BluetoothLightbulb { fn new(config: serde_yaml::Value) -> Result> { let config: BluetoothLightbulbConfig = serde_yaml::from_value(config)?; 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)?; let ret = BluetoothLightbulb { config: config, msg_types: msg_types, levels: levels, session: session, }; // Initialize bluetooth adapter let adapter = BluetoothAdapter::init(&ret.session)?; // 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"); } Ok(ret) } fn process_message(&mut self, message: Message) { let adapter = BluetoothAdapter::init(&self.session).expect("Could not initialize bluetooth adapter"); let mut targets = 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(30000) { Ok(_) => { let power = match device.get_tx_power() { Ok(p) => p.to_string(), Err(_) => "?".to_owned(), }; log::info!("Connected to {}, tx power={}", addr, power); // GATT characteristics can take a second to correctly load once connected std::thread::sleep(std::time::Duration::from_secs(1)); } 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" { targets.push((device.clone(), charac)); found_macs.insert(addr.clone(), true); } } } } } } // List devices not found 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; } // Merge LightConfig properties 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); } // Aimation properties 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 {:?} {:X}", anim, color); for _ in 0..cfg.repeat.unwrap() { match anim { Animation::None => { self.set_color(&targets, 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(&targets, r, g, b); std::thread::sleep(std::time::Duration::from_millis( (250.0 / speed) as u64, )); self.set_color(&targets, 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(&targets, r * mult, g * mult, b * mult); } for i in 0..10 { let mult = 1.0 - (i as f64 / 10.0); self.set_color(&targets, 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(&targets, 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(&targets, r * mult, g * mult, b * mult); } std::thread::sleep(std::time::Duration::from_millis(100)); self.set_color(&targets, 0.0, 0.0, 0.0); std::thread::sleep(std::time::Duration::from_millis(100)); } Animation::RampDown => { self.set_color(&targets, 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(&targets, r * mult, g * mult, b * mult); } self.set_color(&targets, 0.0, 0.0, 0.0); std::thread::sleep(std::time::Duration::from_millis(100)); } } } self.set_color(&targets, 0.0, 0.0, 0.0); } }