extern crate btleplug;
extern crate failure;
#[macro_use]
extern crate failure_derive;
pub use btleplug::api::Peripheral as Device;
use btleplug::api::{BDAddr, Central, Characteristic, ParseBDAddrError, UUID};
#[cfg(target_os = "linux")]
use btleplug::bluez::{adapter::Adapter, manager::Manager};
#[cfg(target_os = "macos")]
use btleplug::corebluetooth::{adapter::Adapter, manager::Manager};
#[cfg(target_os = "windows")]
use btleplug::winrtble::{adapter::Adapter, manager::Manager};
use indicatif::{ProgressBar, ProgressStyle};
use std::thread;
use std::time::Duration;
use std::{
cmp::{max, Ordering},
time::Instant,
};
const CONTROL_UUID: UUID = UUID::B128([
0x8a, 0xf7, 0x15, 0x02, 0x9c, 0x00, 0x49, 0x8a, 0x24, 0x10, 0x8a, 0x33, 0x02, 0x00, 0xfa, 0x99,
]);
const POSITION_UUID: UUID = UUID::B128([
0x8a, 0xf7, 0x15, 0x02, 0x9c, 0x00, 0x49, 0x8a, 0x24, 0x10, 0x8a, 0x33, 0x21, 0x00, 0xfa, 0x99,
]);
const UP: [u8; 2] = [0x47, 0x00];
const DOWN: [u8; 2] = [0x46, 0x00];
const STOP: [u8; 2] = [0xFF, 0x00];
pub const MIN_HEIGHT: u16 = 6200;
pub const MAX_HEIGHT: u16 = 12700;
pub fn bytes_to_tenth_millimeters(bytes: &[u8]) -> u16 {
let as_int = ((bytes[1] as u16) << 8) + bytes[0] as u16;
as_int + MIN_HEIGHT
}
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "Cannot find the device.")]
CannotFindDevice,
#[fail(display = "Cannot connect to the device.")]
ConnectionFailed,
#[fail(display = "Cannot scan for devices.")]
ScanFailed,
#[fail(display = "Permission denied.")]
PermissionDenied,
#[fail(display = "Cannot discover Bluetooth characteristics.")]
CharacteristicsDiscoveryFailed,
#[fail(display = "Bluetooth characteristics not found: '{}'.", _0)]
CharacteristicsNotFound(String),
#[fail(display = "Desired position has to be between MIN_HEIGHT and MAX_HEIGHT.")]
PositionNotInRange,
#[fail(display = "Cannot subscribe to read position.")]
CannotSubscribePosition,
#[fail(display = "Cannot read position.")]
CannotReadPosition,
#[fail(display = "Failed to parse mac address.")]
MacAddrParseFailed(ParseBDAddrError),
}
fn get_desk(mac: Option<BDAddr>) -> Result<impl Device, Error> {
let manager = Manager::new().unwrap();
let adapters = manager.adapters().unwrap();
let central = adapters.into_iter().next().unwrap();
if let Err(err) = central.start_scan() {
return Err(match err {
btleplug::Error::PermissionDenied => Error::PermissionDenied,
_ => Error::ScanFailed,
});
};
let desk = find_desk(central, mac);
if desk.is_none() {
return Err(Error::CannotFindDevice);
}
let desk = desk.unwrap();
if desk.connect().is_err() {
return Err(Error::ConnectionFailed);
}
Ok(desk)
}
fn find_desk(central: Adapter, mac: Option<BDAddr>) -> Option<impl Device> {
let mut attempt = 0;
while attempt < 240 {
let desk = central.peripherals().into_iter().find(|p| match mac {
Some(mac) => p.properties().address == mac,
None => p
.properties()
.local_name
.iter()
.any(|name| name.contains("Desk")),
});
if desk.is_some() {
return desk;
}
attempt += 1;
thread::sleep(Duration::from_millis(50));
}
None
}
pub fn get_instance() -> Result<Idasen<impl Device>, Error> {
let desk = get_desk(None)?;
Ok(Idasen::new(desk)?)
}
pub fn get_instance_by_mac(mac: &str) -> Result<Idasen<impl Device>, Error> {
let addr = mac.parse::<BDAddr>();
match addr {
Ok(addr) => {
let desk = get_desk(Some(addr))?;
Ok(Idasen::new(desk)?)
}
Err(err) => Err(Error::MacAddrParseFailed(err)),
}
}
pub struct Idasen<T>
where
T: Device,
{
pub mac_addr: BDAddr,
desk: T,
control_characteristic: Characteristic,
position_characteristic: Characteristic,
}
impl<T: Device> Idasen<T> {
pub fn new(desk: T) -> Result<Self, Error> {
let mac_addr = desk.address();
let characteristics = desk.discover_characteristics();
if characteristics.is_err() {
return Err(Error::CharacteristicsDiscoveryFailed);
};
let characteristics = characteristics.unwrap();
let control_characteristic = characteristics
.iter()
.find(|characteristic| characteristic.uuid == CONTROL_UUID);
if control_characteristic.is_none() {
return Err(Error::CharacteristicsNotFound("Control".to_string()));
}
let control_characteristic = control_characteristic.unwrap().clone();
let position_characteristic = characteristics
.iter()
.find(|characteristics| characteristics.uuid == POSITION_UUID);
if position_characteristic.is_none() {
return Err(Error::CharacteristicsNotFound("Position".to_string()));
}
let position_characteristic = position_characteristic.unwrap().clone();
if desk.subscribe(&position_characteristic).is_err() {
return Err(Error::CannotSubscribePosition)
};
Ok(Self {
desk,
mac_addr,
control_characteristic,
position_characteristic,
})
}
pub fn up(&self) -> btleplug::Result<()> {
self.desk.command(&self.control_characteristic, &UP)
}
pub fn down(&self) -> btleplug::Result<()> {
self.desk.command(&self.control_characteristic, &DOWN)
}
pub fn stop(&self) -> btleplug::Result<()> {
self.desk.command(&self.control_characteristic, &STOP)
}
pub fn move_to(&self, target_position: u16) -> Result<(), Error> {
self.move_to_target(target_position, None)
}
pub fn move_to_with_progress(&self, target_position: u16) -> Result<(), Error> {
let initial_position = (target_position as i16 - self.position()? as i16).abs();
let progress = ProgressBar::new(initial_position as u64);
progress.set_style(ProgressStyle::default_bar().template("{spinner} {wide_bar} [{msg}cm]"));
self.move_to_target(target_position, Some(progress))
}
fn move_to_target(
&self,
target_position: u16,
progress: Option<ProgressBar>,
) -> Result<(), Error> {
if !(MIN_HEIGHT..=MAX_HEIGHT).contains(&target_position) {
return Err(Error::PositionNotInRange);
}
let mut position_reached = false;
let mut last_position = self.position()? as i16;
let mut last_position_read_at = Instant::now();
let target_position = target_position as i16;
while !position_reached {
let current_position = self.position()? as i16;
let going_up = match target_position.cmp(¤t_position) {
Ordering::Greater => true,
Ordering::Less => false,
Ordering::Equal => return Ok(()),
};
let remaining_distance = (target_position - current_position).abs();
let elapsed_millis = last_position_read_at.elapsed().as_millis();
let moved_height = (last_position - current_position).abs();
let speed = ((moved_height as f64 / elapsed_millis as f64) * 1000f64) as i16;
if let Some(ref progress) = progress {
progress.inc(speed as u64);
let position_cm = current_position as f32 / 100.0;
progress.set_message(format!("{}", position_cm).as_str());
}
if remaining_distance <= 10 {
position_reached = true;
let _ = self.stop();
} else if going_up {
let _ = self.up();
} else if !going_up {
let _ = self.down();
}
if remaining_distance < max(speed / 2, 50) {
let _ = self.stop();
}
last_position = self.position()? as i16;
last_position_read_at = Instant::now();
}
if let Some(progress) = progress {
progress.finish();
}
Ok(())
}
pub fn position(&self) -> Result<u16, Error> {
let response = self.desk.read(&self.position_characteristic);
match response {
Ok(value) => Ok(bytes_to_tenth_millimeters(&value)),
Err(_) => Err(Error::CannotReadPosition),
}
}
}