#![allow(unused_crate_dependencies)]
#![allow(clippy::print_stdout)]
#![allow(clippy::print_stderr)]
use std::io::BufRead;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use clap::Parser;
use futures_core::future::BoxFuture;
use sscanf::sscanf;
use tracing::info;
use burble::att::Access;
use burble::gap::Appearance;
use burble::gatt::Db;
use burble::hci::AdvEvent;
use burble::hogp::{Input, MouseIn};
use burble::*;
use burble_const::Service;
use burble_crypto::NumCompare;
#[derive(Clone, Copy, Debug, clap::Parser)]
struct Args {
#[arg(short, long, value_parser=hex16)]
vid: Option<u16>,
#[arg(short, long, value_parser=hex16)]
pid: Option<u16>,
#[arg(short, long)]
legacy: bool,
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let args = Args::parse();
let usb = host::Usb::new()?;
let mut ctlr = if let (Some(vid), Some(pid)) = (args.vid, args.pid) {
usb.open_first(vid, pid)?
} else {
println!("Available controllers (pass 'ID <VID>:<PID>' to '--vid' and '--pid' options):");
for ctlr in usb.controllers()? {
println!("{ctlr}");
}
return Ok(());
};
ctlr.init()?;
let host = hci::Host::new(Arc::new(ctlr));
let event_loop = host.event_loop();
host.init().await?;
info!("Local version: {:?}", host.read_local_version().await?);
info!("Device address: {:?}", host.read_bd_addr().await?);
host.le_set_default_phy(Some(hci::PhyMask::LE_2M), Some(hci::PhyMask::LE_2M))
.await?;
let r = serve(args, host).await;
event_loop.stop().await?;
r
}
fn read_input(srv: Arc<hogp::HidService>) -> tokio::task::JoinHandle<()> {
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
std::thread::spawn(move || {
for ln in std::io::BufReader::new(std::io::stdin()).lines() {
tx.blocking_send(ln?)?;
}
Ok::<_, anyhow::Error>(())
});
tokio::task::spawn(async move {
use {Input::*, MouseIn::*};
loop {
let ln: String = tokio::select! {
ln = rx.recv() => match ln {
None => return,
Some(ln) => ln,
},
_ = tokio::signal::ctrl_c() => return,
};
let mut tok = ln.split_ascii_whitespace();
let Some(cmd) = tok.next() else { continue };
let inp = match (cmd, tok.collect::<Vec<&str>>().join(" ")) {
("click" | "c", params) => {
if params.is_empty() {
Mouse(Click(0))
} else {
let Ok(v) = sscanf!(params, "{u8}") else { continue };
Mouse(Click(v))
}
}
("move" | "m", params) => {
let Ok(v) = sscanf!(params, "{i32} {i32}") else { continue };
Mouse(MoveRel { dx: v.0, dy: v.1 })
}
_ => continue,
};
srv.exec(inp).await.unwrap();
}
})
}
async fn serve(args: Args, host: hci::Host) -> Result<()> {
const SEC: Access = Access::READ.authn().encrypt();
let mut db = Db::build();
gatt::Server::define_service(&mut db);
gap::GapService::new("Burble", Appearance::GenericHumanInterfaceDevice).define(&mut db);
dis::DeviceInfoService::new()
.with_manufacturer_name("Blackrock Neurotech")
.with_pnp_id(dis::PnpId::new(dis::VendorId::USB(0x1209), 0x0001, (1, 0, 0)).unwrap())
.define(&mut db, SEC);
bas::BatteryService::new().define(&mut db, SEC);
let hid = hogp::HidService::new();
hid.define(&mut db);
let mut input_task = read_input(hid);
let srv = gatt::Server::new(db, Arc::new(burble_fs::GattServerStore::per_user("burble")));
srv.db().dump();
let key_store: Arc<smp::KeyStore> = Arc::new(burble_fs::KeyStore::per_user("burble"));
let mut secdb = smp::SecDb::new(host.clone(), Arc::clone(&key_store));
tokio::task::spawn(async move { secdb.event_loop().await });
let mut cm = l2cap::ChanManager::new(&host).await?;
let mut adv_task = None;
let mut srv_task = None;
let mut link = None;
loop {
if srv_task.is_none() && adv_task.is_none() {
info!("Enabling advertisements");
adv_task = Some(tokio::task::spawn(advertise(args, host.clone())));
}
tokio::select! {
_ = &mut input_task => return Ok(()),
adv = async { adv_task.as_mut().unwrap().await }, if adv_task.is_some() => {
info!("Advertisement result: {:?}", adv);
adv_task = None;
if matches!(adv??, AdvEvent::Term(_)) {
continue;
}
let link = match link.take() {
Some(link) => link,
None => cm.recv().await?,
};
info!("Serving {link}");
let mut smp = cm.smp_chan(link).unwrap();
let key_store = Arc::clone(&key_store);
tokio::task::spawn(async move {
let mut dev = smp::Device::new().with_display(Box::new(Dev)).with_confirm(Box::new(Dev));
smp.respond(&mut dev, key_store.as_ref()).await
});
let br = cm.att_chan(link).unwrap();
srv_task = Some(tokio::task::spawn(srv.attach(&br).serve(br)));
}
srv = async { srv_task.as_mut().unwrap().await }, if srv_task.is_some() => {
info!("GATT server terminated: {:?}", srv);
srv_task = None;
}
r = cm.recv() => {
assert!(link.is_none());
link = Some(r?);
}
}
}
}
async fn advertise(args: Args, host: hci::Host) -> hci::Result<AdvEvent> {
let mut adv = hci::Advertiser::new(&host).await?;
let params = if args.legacy {
hci::AdvParams {
props: hci::AdvProp::CONNECTABLE | hci::AdvProp::SCANNABLE | hci::AdvProp::LEGACY,
pri_interval: (Duration::from_millis(20), Duration::from_millis(25)),
..hci::AdvParams::default()
}
} else {
hci::AdvParams {
props: hci::AdvProp::CONNECTABLE | hci::AdvProp::INCLUDE_TX_POWER,
pri_interval: (Duration::from_millis(20), Duration::from_millis(25)),
sec_phy: hci::Phy::Le2M,
..hci::AdvParams::default()
}
};
let (h, power) = adv.create(params).await?;
let mut data = gap::ResponseDataMut::new();
data.flags(gap::AdvFlag::LE_GENERAL | gap::AdvFlag::NO_BREDR)
.service(false, [Service::HumanInterfaceDevice])
.local_name(true, "Burble")
.appearance(Appearance::GenericHumanInterfaceDevice)
.tx_power(power);
adv.set_data(h, data.get()).await?;
let enable_params = hci::AdvEnableParams {
handle: h,
duration: Duration::from_secs(20),
max_events: 0,
};
let adv_set = adv.enable(enable_params).await?;
let r = adv_set.await;
adv.remove_all().await?;
r
}
#[derive(Debug)]
struct Dev;
impl smp::Display for Dev {
fn show(&mut self, n: NumCompare) -> BoxFuture<bool> {
println!("Numeric comparison: {n}");
Box::pin(std::future::ready(true))
}
}
impl smp::Confirm for Dev {
fn confirm(&mut self) -> BoxFuture<bool> {
Box::pin(std::future::ready(true))
}
}
pub fn hex16(mut s: &str) -> Result<u16, String> {
if s.starts_with("0x") || s.starts_with("0X") {
s = &s[2..];
}
u16::from_str_radix(s, 16).map_err(|e| format!("{e}"))
}