307 lines
11 KiB
Rust
307 lines
11 KiB
Rust
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<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,
|
|
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<Self, Box<dyn std::error::Error>> {
|
|
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<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(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<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;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|