blob: 4fc72625485d511502b1e4248b0ba17cd80905bb [file] [log] [blame]
use std::collections::HashMap;
use std::fmt::{Display, Formatter, Result};
use std::sync::{Arc, Mutex};
use crate::callbacks::BtGattCallback;
use crate::ClientContext;
use crate::{console_red, console_yellow, print_error, print_info};
use bt_topshim::btif::BtTransport;
use btstack::bluetooth::{BluetoothDevice, IBluetooth};
use btstack::bluetooth_gatt::IBluetoothGatt;
use btstack::uuid::UuidHelper;
use manager_service::iface_bluetooth_manager::IBluetoothManager;
const INDENT_CHAR: &str = " ";
const BAR1_CHAR: &str = "=";
const BAR2_CHAR: &str = "-";
const MAX_MENU_CHAR_WIDTH: usize = 72;
const GATT_CLIENT_APP_UUID: &str = "12345678123456781234567812345678";
type CommandFunction = fn(&mut CommandHandler, &Vec<String>);
fn _noop(_handler: &mut CommandHandler, _args: &Vec<String>) {
// Used so we can add options with no direct function
// e.g. help and quit
}
pub struct CommandOption {
description: String,
function_pointer: CommandFunction,
}
/// Handles string command entered from command line.
pub(crate) struct CommandHandler {
context: Arc<Mutex<ClientContext>>,
command_options: HashMap<String, CommandOption>,
}
struct DisplayList<T>(Vec<T>);
impl<T: Display> Display for DisplayList<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let _ = write!(f, "[\n");
for item in self.0.iter() {
let _ = write!(f, " {}\n", item);
}
write!(f, "]")
}
}
fn enforce_arg_len<F>(args: &Vec<String>, min_len: usize, msg: &str, mut action: F)
where
F: FnMut(),
{
if args.len() < min_len {
println!("Usage: {}", msg);
} else {
action();
}
}
fn wrap_help_text(text: &str, max: usize, indent: usize) -> String {
let remaining_count = std::cmp::max(
// real_max
std::cmp::max(max, text.chars().count())
// take away char count
- text.chars().count()
// take away real_indent
- (
if std::cmp::max(max, text.chars().count())- text.chars().count() > indent {
indent
} else {
0
}),
0,
);
format!("|{}{}{}|", INDENT_CHAR.repeat(indent), text, INDENT_CHAR.repeat(remaining_count))
}
// This should be called during the constructor in order to populate the command option map
fn build_commands() -> HashMap<String, CommandOption> {
let mut command_options = HashMap::<String, CommandOption>::new();
command_options.insert(
String::from("adapter"),
CommandOption {
description: String::from(
"Enable/Disable/Show default bluetooth adapter. (e.g. adapter enable)",
),
function_pointer: CommandHandler::cmd_adapter,
},
);
command_options.insert(
String::from("bond"),
CommandOption {
description: String::from("Creates a bond with a device."),
function_pointer: CommandHandler::cmd_bond,
},
);
command_options.insert(
String::from("device"),
CommandOption {
description: String::from("Take action on a remote device. (i.e. info)"),
function_pointer: CommandHandler::cmd_device,
},
);
command_options.insert(
String::from("discovery"),
CommandOption {
description: String::from("Start and stop device discovery. (e.g. discovery start)"),
function_pointer: CommandHandler::cmd_discovery,
},
);
command_options.insert(
String::from("floss"),
CommandOption {
description: String::from("Enable or disable Floss for dogfood."),
function_pointer: CommandHandler::cmd_floss,
},
);
command_options.insert(
String::from("gatt"),
CommandOption {
description: String::from("GATT tools"),
function_pointer: CommandHandler::cmd_gatt,
},
);
command_options.insert(
String::from("get-address"),
CommandOption {
description: String::from("Gets the local device address."),
function_pointer: CommandHandler::cmd_get_address,
},
);
command_options.insert(
String::from("help"),
CommandOption {
description: String::from("Shows this menu."),
function_pointer: CommandHandler::cmd_help,
},
);
command_options.insert(
String::from("list"),
CommandOption {
description: String::from(
"List bonded or found remote devices. Use: list <bonded|found>",
),
function_pointer: CommandHandler::cmd_list_devices,
},
);
command_options.insert(
String::from("quit"),
CommandOption {
description: String::from("Quit out of the interactive shell."),
function_pointer: _noop,
},
);
command_options
}
impl CommandHandler {
/// Creates a new CommandHandler.
pub fn new(context: Arc<Mutex<ClientContext>>) -> CommandHandler {
CommandHandler { context, command_options: build_commands() }
}
/// Entry point for command and arguments
pub fn process_cmd_line(&mut self, command: &String, args: &Vec<String>) {
// Ignore empty line
match &command[0..] {
"" => {}
_ => match self.command_options.get(command) {
Some(cmd) => (cmd.function_pointer)(self, &args),
None => {
println!("'{}' is an invalid command!", command);
self.cmd_help(&args);
}
},
};
}
// Common message for when the adapter isn't ready
fn adapter_not_ready(&self) {
let adapter_idx = self.context.lock().unwrap().default_adapter;
print_error!(
"Default adapter {} is not enabled. Enable the adapter before using this command.",
adapter_idx
);
}
fn cmd_help(&mut self, args: &Vec<String>) {
if args.len() > 0 {
match self.command_options.get(&args[0]) {
Some(cmd) => {
println!(
"\n{}{}\n{}{}\n",
INDENT_CHAR.repeat(4),
args[0],
INDENT_CHAR.repeat(8),
cmd.description
);
}
None => {
println!("'{}' is an invalid command!", args[0]);
self.cmd_help(&vec![]);
}
}
} else {
// Build equals bar and Shave off sides
let equal_bar = format!(" {} ", BAR1_CHAR.repeat(MAX_MENU_CHAR_WIDTH));
// Build empty bar and Shave off sides
let empty_bar = format!("|{}|", INDENT_CHAR.repeat(MAX_MENU_CHAR_WIDTH));
// Header
println!(
"\n{}\n{}\n{}\n{}",
equal_bar,
wrap_help_text("Help Menu", MAX_MENU_CHAR_WIDTH, 2),
// Minus bar
format!("+{}+", BAR2_CHAR.repeat(MAX_MENU_CHAR_WIDTH)),
empty_bar
);
// Print commands
for (key, val) in self.command_options.iter() {
println!(
"{}\n{}\n{}",
wrap_help_text(&key, MAX_MENU_CHAR_WIDTH, 4),
wrap_help_text(&val.description, MAX_MENU_CHAR_WIDTH, 8),
empty_bar
);
}
// Footer
println!("{}\n{}", empty_bar, equal_bar);
}
}
fn cmd_adapter(&mut self, args: &Vec<String>) {
let default_adapter = self.context.lock().unwrap().default_adapter;
enforce_arg_len(args, 1, "adapter <enable|disable|show>", || match &args[0][0..] {
"enable" => {
self.context.lock().unwrap().manager_dbus.start(default_adapter);
}
"disable" => {
self.context.lock().unwrap().manager_dbus.stop(default_adapter);
}
"show" => {
if !self.context.lock().unwrap().manager_dbus.get_floss_enabled() {
println!("Floss is not enabled. First run, `floss enable`");
return;
}
let enabled = self.context.lock().unwrap().enabled;
let address = match self.context.lock().unwrap().adapter_address.as_ref() {
Some(x) => x.clone(),
None => String::from(""),
};
let name = self.context.lock().unwrap().adapter_dbus.as_ref().unwrap().get_name();
let uuids = self.context.lock().unwrap().adapter_dbus.as_ref().unwrap().get_uuids();
print_info!("Address: {}", address);
print_info!("Name: {}", name);
print_info!("State: {}", if enabled { "enabled" } else { "disabled" });
print_info!(
"Uuids: {}",
DisplayList(
uuids.iter().map(|&x| UuidHelper::to_string(&x)).collect::<Vec<String>>()
)
);
}
_ => {
println!("Invalid argument '{}'", args[0]);
}
});
}
fn cmd_get_address(&mut self, _args: &Vec<String>) {
if !self.context.lock().unwrap().adapter_ready {
self.adapter_not_ready();
return;
}
let address = self.context.lock().unwrap().update_adapter_address();
print_info!("Local address = {}", &address);
}
fn cmd_discovery(&mut self, args: &Vec<String>) {
if !self.context.lock().unwrap().adapter_ready {
self.adapter_not_ready();
return;
}
enforce_arg_len(args, 1, "discovery <start|stop>", || match &args[0][0..] {
"start" => {
self.context.lock().unwrap().adapter_dbus.as_ref().unwrap().start_discovery();
}
"stop" => {
self.context.lock().unwrap().adapter_dbus.as_ref().unwrap().cancel_discovery();
}
_ => {
println!("Invalid argument '{}'", args[0]);
}
});
}
fn cmd_bond(&mut self, args: &Vec<String>) {
if !self.context.lock().unwrap().adapter_ready {
self.adapter_not_ready();
return;
}
enforce_arg_len(args, 2, "bond <add|remove|cancel> <address>", || match &args[0][0..] {
"add" => {
let device = BluetoothDevice {
address: String::from(&args[1]),
name: String::from("Classic Device"),
};
let bonding_attempt =
&self.context.lock().unwrap().bonding_attempt.as_ref().cloned();
if bonding_attempt.is_some() {
print_info!(
"Already bonding [{}]. Cancel bonding first.",
bonding_attempt.as_ref().unwrap().address,
);
return;
}
let success = self
.context
.lock()
.unwrap()
.adapter_dbus
.as_ref()
.unwrap()
.create_bond(device.clone(), BtTransport::Auto);
if success {
self.context.lock().unwrap().bonding_attempt = Some(device);
}
}
"remove" => {
let device = BluetoothDevice {
address: String::from(&args[1]),
name: String::from("Classic Device"),
};
self.context.lock().unwrap().adapter_dbus.as_ref().unwrap().remove_bond(device);
}
"cancel" => {
let device = BluetoothDevice {
address: String::from(&args[1]),
name: String::from("Classic Device"),
};
self.context
.lock()
.unwrap()
.adapter_dbus
.as_ref()
.unwrap()
.cancel_bond_process(device);
}
_ => {
println!("Invalid argument '{}'", args[0]);
}
});
}
fn cmd_device(&mut self, args: &Vec<String>) {
if !self.context.lock().unwrap().adapter_ready {
self.adapter_not_ready();
return;
}
enforce_arg_len(args, 2, "device <connect|disconnect|info> <address>", || {
match &args[0][0..] {
"connect" => {
let device = BluetoothDevice {
address: String::from(&args[1]),
name: String::from("Classic Device"),
};
let success = self
.context
.lock()
.unwrap()
.adapter_dbus
.as_ref()
.unwrap()
.connect_all_enabled_profiles(device.clone());
if success {
println!("Connecting to {}", &device.address);
} else {
println!("Can't connect to {}", &device.address);
}
}
"disconnect" => {
let device = BluetoothDevice {
address: String::from(&args[1]),
name: String::from("Classic Device"),
};
let success = self
.context
.lock()
.unwrap()
.adapter_dbus
.as_ref()
.unwrap()
.disconnect_all_enabled_profiles(device.clone());
if success {
println!("Disconnecting from {}", &device.address);
} else {
println!("Can't disconnect from {}", &device.address);
}
}
"info" => {
let device = BluetoothDevice {
address: String::from(&args[1]),
name: String::from("Classic Device"),
};
let (bonded, connected, uuids) = {
let ctx = self.context.lock().unwrap();
let adapter = ctx.adapter_dbus.as_ref().unwrap();
let bonded = adapter.get_bond_state(device.clone());
let connected = adapter.get_connection_state(device.clone());
let uuids = adapter.get_remote_uuids(device.clone());
(bonded, connected, uuids)
};
print_info!("Address: {}", &device.address);
print_info!("Bonded: {}", bonded);
print_info!("Connected: {}", connected);
print_info!(
"Uuids: {}",
DisplayList(
uuids
.iter()
.map(|&x| UuidHelper::to_string(&x))
.collect::<Vec<String>>()
)
);
}
_ => {
println!("Invalid argument '{}'", args[0]);
}
}
});
}
fn cmd_floss(&mut self, args: &Vec<String>) {
enforce_arg_len(args, 1, "floss <enable|disable>", || match &args[0][0..] {
"enable" => {
self.context.lock().unwrap().manager_dbus.set_floss_enabled(true);
}
"disable" => {
self.context.lock().unwrap().manager_dbus.set_floss_enabled(false);
}
"show" => {
print_info!(
"Floss enabled: {}",
self.context.lock().unwrap().manager_dbus.get_floss_enabled()
);
}
_ => {
println!("Invalid argument '{}'", args[0]);
}
});
}
fn cmd_gatt(&mut self, args: &Vec<String>) {
if !self.context.lock().unwrap().adapter_ready {
self.adapter_not_ready();
return;
}
enforce_arg_len(args, 1, "gatt <commands>", || match &args[0][0..] {
"register-client" => {
self.context.lock().unwrap().gatt_dbus.as_mut().unwrap().register_client(
String::from(GATT_CLIENT_APP_UUID),
Box::new(BtGattCallback::new(
String::from("/org/chromium/bluetooth/client/bluetooth_gatt_callback"),
self.context.clone(),
)),
false,
);
}
"client-connect" => {
if args.len() < 2 {
println!("usage: gatt client-connect <addr>");
return;
}
let client_id = self.context.lock().unwrap().gatt_client_id;
if client_id.is_none() {
println!("GATT client is not yet registered.");
return;
}
let addr = String::from(&args[1]);
self.context.lock().unwrap().gatt_dbus.as_ref().unwrap().client_connect(
client_id.unwrap(),
addr,
false,
2,
false,
1,
);
}
"client-read-phy" => {
if args.len() < 2 {
println!("usage: gatt client-read-phy <addr>");
return;
}
let client_id = self.context.lock().unwrap().gatt_client_id;
if client_id.is_none() {
println!("GATT client is not yet registered.");
return;
}
let addr = String::from(&args[1]);
self.context
.lock()
.unwrap()
.gatt_dbus
.as_mut()
.unwrap()
.client_read_phy(client_id.unwrap(), addr);
}
"client-discover-services" => {
if args.len() < 2 {
println!("usage: gatt client-discover-services <addr>");
return;
}
let client_id = self.context.lock().unwrap().gatt_client_id;
if client_id.is_none() {
println!("GATT client is not yet registered.");
return;
}
let addr = String::from(&args[1]);
self.context
.lock()
.unwrap()
.gatt_dbus
.as_ref()
.unwrap()
.discover_services(client_id.unwrap(), addr);
}
_ => {
println!("Invalid argument '{}'", args[0]);
}
});
}
/// Get the list of currently supported commands
pub fn get_command_list(&self) -> Vec<String> {
self.command_options.keys().map(|key| String::from(key)).collect::<Vec<String>>()
}
fn cmd_list_devices(&mut self, args: &Vec<String>) {
if !self.context.lock().unwrap().adapter_ready {
self.adapter_not_ready();
return;
}
enforce_arg_len(args, 1, "list <bonded|found>", || match &args[0][0..] {
"bonded" => {
print_info!("Known bonded devices:");
let devices = self
.context
.lock()
.unwrap()
.adapter_dbus
.as_ref()
.unwrap()
.get_bonded_devices();
for device in devices.iter() {
print_info!("[{:17}] {}", device.address, device.name);
}
}
"found" => {
print_info!("Devices found in most recent discovery session:");
for (key, val) in self.context.lock().unwrap().found_devices.iter() {
print_info!("[{:17}] {}", key, val.name);
}
}
_ => {
println!("Invalid argument '{}'", args[0]);
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wrap_help_text() {
let text = "hello";
let text_len = text.chars().count();
// ensure no overflow
assert_eq!(format!("|{}|", text), wrap_help_text(text, 4, 0));
assert_eq!(format!("|{}|", text), wrap_help_text(text, 5, 0));
assert_eq!(format!("|{}{}|", text, " "), wrap_help_text(text, 6, 0));
assert_eq!(format!("|{}{}|", text, " ".repeat(2)), wrap_help_text(text, 7, 0));
assert_eq!(
format!("|{}{}|", text, " ".repeat(100 - text_len)),
wrap_help_text(text, 100, 0)
);
assert_eq!(format!("|{}{}|", " ", text), wrap_help_text(text, 4, 1));
assert_eq!(format!("|{}{}|", " ".repeat(2), text), wrap_help_text(text, 5, 2));
assert_eq!(format!("|{}{}{}|", " ".repeat(3), text, " "), wrap_help_text(text, 6, 3));
assert_eq!(
format!("|{}{}{}|", " ".repeat(4), text, " ".repeat(7 - text_len)),
wrap_help_text(text, 7, 4)
);
assert_eq!(format!("|{}{}|", " ".repeat(9), text), wrap_help_text(text, 4, 9));
assert_eq!(format!("|{}{}|", " ".repeat(10), text), wrap_help_text(text, 3, 10));
assert_eq!(format!("|{}{}|", " ".repeat(11), text), wrap_help_text(text, 2, 11));
assert_eq!(format!("|{}{}|", " ".repeat(12), text), wrap_help_text(text, 1, 12));
assert_eq!("||", wrap_help_text("", 0, 0));
assert_eq!("| |", wrap_help_text("", 1, 0));
assert_eq!("| |", wrap_help_text("", 1, 1));
assert_eq!("| |", wrap_help_text("", 0, 1));
}
#[test]
fn test_enforce_arg_len() {
// With min arg set and min arg supplied
let args: &Vec<String> = &vec![String::from("arg")];
let mut i: usize = 0;
enforce_arg_len(args, 1, "help text", || {
i = 1;
});
assert_eq!(1, i);
// With no min arg set and with arg supplied
i = 0;
enforce_arg_len(args, 0, "help text", || {
i = 1;
});
assert_eq!(1, i);
// With min arg set and no min arg supplied
let args: &Vec<String> = &vec![];
i = 0;
enforce_arg_len(args, 1, "help text", || {
i = 1;
});
assert_eq!(0, i);
// With no min arg set and no arg supplied
i = 0;
enforce_arg_len(args, 0, "help text", || {
i = 1;
});
assert_eq!(1, i);
}
}