299 lines
10 KiB
Rust
299 lines
10 KiB
Rust
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<Level, LightConfig>,
|
|
msg_types: Vec<(GlobMatcher, LightConfig)>,
|
|
}
|
|
#[derive(Debug, Deserialize)]
|
|
struct BluetoothLightbulbConfig {
|
|
#[serde(default)]
|
|
mac: HashMap<String, ()>,
|
|
#[serde(default)]
|
|
levels: HashMap<Level, LightConfig>,
|
|
#[serde(default)]
|
|
msg_types: HashMap<String, LightConfig>,
|
|
}
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
enum Animation {
|
|
None,
|
|
Smooth,
|
|
Bounce,
|
|
Blink,
|
|
RampUp,
|
|
RampDown,
|
|
}
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
struct LightConfig {
|
|
#[serde(default)]
|
|
color: Option<u32>,
|
|
#[serde(default)]
|
|
animation: Option<Animation>,
|
|
#[serde(default)]
|
|
repeat: Option<u64>,
|
|
#[serde(default)]
|
|
speed: Option<f64>,
|
|
}
|
|
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<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 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<String, serde_yaml::Value>,
|
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
|
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<String, bool> = 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<String, bool> = 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);
|
|
}
|
|
}
|