| use std::collections::HashMap; |
| use std::fmt::{Display, Formatter}; |
| use std::slice::SliceIndex; |
| use std::sync::{Arc, Mutex}; |
| use std::time::Duration; |
| |
| use crate::bt_adv::AdvSet; |
| use crate::bt_gatt::AuthReq; |
| use crate::callbacks::{BtGattCallback, BtGattServerCallback}; |
| use crate::ClientContext; |
| use crate::{console_red, console_yellow, print_error, print_info}; |
| use bt_topshim::btif::{BtConnectionState, BtDiscMode, BtStatus, BtTransport}; |
| use bt_topshim::profiles::hid_host::BthhReportType; |
| use bt_topshim::profiles::sdp::{BtSdpMpsRecord, BtSdpRecord}; |
| use bt_topshim::profiles::{gatt::LePhy, ProfileConnectionState}; |
| use btstack::bluetooth::{BluetoothDevice, IBluetooth}; |
| use btstack::bluetooth_gatt::{GattWriteType, IBluetoothGatt, ScanSettings, ScanType}; |
| use btstack::bluetooth_media::{IBluetoothMedia, IBluetoothTelephony}; |
| use btstack::bluetooth_qa::IBluetoothQA; |
| use btstack::socket_manager::{IBluetoothSocketManager, SocketResult}; |
| use btstack::uuid::{Profile, UuidHelper, UuidWrapper}; |
| 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"; |
| const GATT_SERVER_APP_UUID: &str = "12345678123456781234567812345679"; |
| |
| enum CommandError { |
| // Command not handled due to invalid arguments. |
| InvalidArgs, |
| // Command handled but failed with the given reason. |
| Failed(String), |
| } |
| |
| impl From<&str> for CommandError { |
| fn from(s: &str) -> CommandError { |
| CommandError::Failed(String::from(s)) |
| } |
| } |
| |
| impl From<String> for CommandError { |
| fn from(s: String) -> CommandError { |
| CommandError::Failed(s) |
| } |
| } |
| |
| type CommandResult = Result<(), CommandError>; |
| |
| type CommandFunction = fn(&mut CommandHandler, &Vec<String>) -> CommandResult; |
| |
| fn _noop(_handler: &mut CommandHandler, _args: &Vec<String>) -> CommandResult { |
| // Used so we can add options with no direct function |
| // e.g. help and quit |
| Ok(()) |
| } |
| |
| pub struct CommandOption { |
| rules: Vec<String>, |
| 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>, |
| } |
| |
| /// Define what to do when a socket connects. Mainly for qualification purposes. |
| /// Specifically, after a socket is connected/accepted, we will do |
| /// (1) send a chunk of data every |send_interval| time until |num_frame| chunks has been sent. |
| /// (2) wait another |disconnect_delay| time. any incoming data will be dumpted during this time. |
| /// (3) disconnect the socket. |
| #[derive(Copy, Clone)] |
| pub struct SocketSchedule { |
| /// Number of times to send data |
| pub num_frame: u32, |
| /// Time interval between each sending |
| pub send_interval: Duration, |
| /// Extra time after the last sending. Any incoming data will be printed during this time. |
| pub disconnect_delay: Duration, |
| } |
| |
| struct DisplayList<T>(Vec<T>); |
| |
| impl<T: Display> Display for DisplayList<T> { |
| fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
| let _ = write!(f, "[\n"); |
| for item in self.0.iter() { |
| let _ = write!(f, " {}\n", item); |
| } |
| |
| write!(f, "]") |
| } |
| } |
| |
| 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 { |
| rules: vec![ |
| String::from("adapter enable"), |
| String::from("adapter disable"), |
| String::from("adapter show"), |
| String::from("adapter discoverable <on|limited|off> <duration>"), |
| String::from("adapter connectable <on|off>"), |
| String::from("adapter set-name <name>"), |
| ], |
| description: String::from( |
| "Enable/Disable/Show default bluetooth adapter. (e.g. adapter enable)\n |
| Discoverable On/Limited/Off (e.g. adapter discoverable on 60)\n |
| Connectable On/Off (e.g. adapter connectable on)", |
| ), |
| function_pointer: CommandHandler::cmd_adapter, |
| }, |
| ); |
| command_options.insert( |
| String::from("bond"), |
| CommandOption { |
| rules: vec![String::from("bond <add|remove|cancel> <address>")], |
| description: String::from("Creates a bond with a device."), |
| function_pointer: CommandHandler::cmd_bond, |
| }, |
| ); |
| command_options.insert( |
| String::from("device"), |
| CommandOption { |
| rules: vec![ |
| String::from("device <connect|disconnect|info> <address>"), |
| String::from("device set-pairing-confirmation <address> <accept|reject>"), |
| String::from("device set-pairing-pin <address> <pin|reject>"), |
| String::from("device set-pairing-passkey <address> <passkey|reject>"), |
| String::from("device set-alias <address> <new-alias>"), |
| ], |
| description: String::from("Take action on a remote device. (i.e. info)"), |
| function_pointer: CommandHandler::cmd_device, |
| }, |
| ); |
| command_options.insert( |
| String::from("discovery"), |
| CommandOption { |
| rules: vec![String::from("discovery <start|stop>")], |
| description: String::from("Start and stop device discovery. (e.g. discovery start)"), |
| function_pointer: CommandHandler::cmd_discovery, |
| }, |
| ); |
| command_options.insert( |
| String::from("floss"), |
| CommandOption { |
| rules: vec![String::from("floss <enable|disable>")], |
| description: String::from("Enable or disable Floss for dogfood."), |
| function_pointer: CommandHandler::cmd_floss, |
| }, |
| ); |
| command_options.insert( |
| String::from("gatt"), |
| CommandOption { |
| rules: vec![ |
| String::from("gatt register-client"), |
| String::from("gatt client-connect <address>"), |
| String::from("gatt client-read-phy <address>"), |
| String::from("gatt client-discover-services <address>"), |
| String::from("gatt client-discover-service-by-uuid-pts <address> <uuid>"), |
| String::from("gatt client-disconnect <address>"), |
| String::from("gatt configure-mtu <address> <mtu>"), |
| String::from("gatt set-direct-connect <true|false>"), |
| String::from("gatt set-connect-transport <Bredr|LE|Auto>"), |
| String::from("gatt set-connect-opportunistic <true|false>"), |
| String::from("gatt set-connect-phy <Phy1m|Phy2m|PhyCoded>"), |
| String::from("gatt set-auth-req <NONE|EncNoMitm|EncMitm|SignedNoMitm|SignedMitm>"), |
| String::from( |
| "gatt write-characteristic <address> <handle> <NoRsp|Write|Prepare> <value>", |
| ), |
| String::from("gatt read-characteristic <address> <handle>"), |
| String::from( |
| "gatt read-characteristic-by-uuid <address> <uuid> <start_handle> <end_handle>", |
| ), |
| String::from("gatt register-notification <address> <handle> <enable|disable>"), |
| String::from("gatt register-server"), |
| ], |
| description: String::from("GATT tools"), |
| function_pointer: CommandHandler::cmd_gatt, |
| }, |
| ); |
| command_options.insert( |
| String::from("le-scan"), |
| CommandOption { |
| rules: vec![ |
| String::from("le-scan register-scanner"), |
| String::from("le-scan unregister-scanner <scanner-id>"), |
| String::from("le-scan start-scan <scanner-id>"), |
| String::from("le-scan stop-scan <scanner-id>"), |
| ], |
| description: String::from("LE scanning utilities."), |
| function_pointer: CommandHandler::cmd_le_scan, |
| }, |
| ); |
| command_options.insert( |
| String::from("advertise"), |
| CommandOption { |
| rules: vec![ |
| String::from("advertise <on|off|ext>"), |
| String::from("advertise set-interval <ms>"), |
| String::from("advertise set-scan-rsp <enable|disable>"), |
| String::from("advertise set-raw-data <raw-adv-data> <adv-id>"), |
| String::from("advertise set-connectable <on|off> <adv-id>"), |
| ], |
| description: String::from("Advertising utilities."), |
| function_pointer: CommandHandler::cmd_advertise, |
| }, |
| ); |
| command_options.insert( |
| String::from("sdp"), |
| CommandOption { |
| rules: vec![String::from("sdp search <address> <uuid>")], |
| description: String::from("Service Discovery Protocol utilities."), |
| function_pointer: CommandHandler::cmd_sdp, |
| }, |
| ); |
| command_options.insert( |
| String::from("socket"), |
| CommandOption { |
| rules: vec![ |
| String::from("socket listen <auth-required> <Bredr|LE>"), |
| String::from("socket listen-rfcomm <scn>"), |
| String::from("socket send-msc <dlci> <address>"), |
| String::from( |
| "socket connect <address> <l2cap|rfcomm> <psm|uuid> <auth-required> <Bredr|LE>", |
| ), |
| String::from("socket disconnect <socket_id>"), |
| String::from("socket set-on-connect-schedule <send|resend|dump>"), |
| ], |
| description: String::from("Socket manager utilities."), |
| function_pointer: CommandHandler::cmd_socket, |
| }, |
| ); |
| command_options.insert( |
| String::from("hid"), |
| CommandOption { |
| rules: vec![ |
| String::from("hid get-report <address> <Input|Output|Feature> <report_id>"), |
| String::from("hid set-report <address> <Input|Output|Feature> <report_value>"), |
| String::from("hid send-data <address> <data>"), |
| ], |
| description: String::from("Socket manager utilities."), |
| function_pointer: CommandHandler::cmd_hid, |
| }, |
| ); |
| command_options.insert( |
| String::from("get-address"), |
| CommandOption { |
| rules: vec![String::from("get-address")], |
| description: String::from("Gets the local device address."), |
| function_pointer: CommandHandler::cmd_get_address, |
| }, |
| ); |
| command_options.insert( |
| String::from("qa"), |
| CommandOption { |
| rules: vec![String::from("qa add-media-player <name> <browsing_supported>")], |
| description: String::from("Methods for testing purposes"), |
| function_pointer: CommandHandler::cmd_qa, |
| }, |
| ); |
| command_options.insert( |
| String::from("help"), |
| CommandOption { |
| rules: vec![String::from("help")], |
| description: String::from("Shows this menu."), |
| function_pointer: CommandHandler::cmd_help, |
| }, |
| ); |
| command_options.insert( |
| String::from("list"), |
| CommandOption { |
| rules: vec![String::from("list <bonded|found|connected>")], |
| description: String::from( |
| "List bonded or found remote devices. Use: list <bonded|found>", |
| ), |
| function_pointer: CommandHandler::cmd_list_devices, |
| }, |
| ); |
| command_options.insert( |
| String::from("telephony"), |
| CommandOption { |
| rules: vec![ |
| String::from("telephony set-network <on|off>"), |
| String::from("telephony set-roaming <on|off>"), |
| String::from("telephony set-signal <strength>"), |
| String::from("telephony set-battery <level>"), |
| String::from("telephony <enable|disable>"), |
| String::from("telephony <incoming-call|dialing-call> <number>"), |
| String::from("telephony <answer-call|hangup-call>"), |
| String::from("telephony <set-memory-call|set-last-call> [<number>]"), |
| String::from( |
| "telephony <release-held|release-active-accept-held|hold-active-accept-held>", |
| ), |
| String::from("telephony <audio-connect|audio-disconnect> <address>"), |
| ], |
| description: String::from("Set device telephony status."), |
| function_pointer: CommandHandler::cmd_telephony, |
| }, |
| ); |
| command_options.insert( |
| String::from("media"), |
| CommandOption { |
| rules: vec![String::from("media log")], |
| description: String::from("Audio tools."), |
| function_pointer: CommandHandler::cmd_media, |
| }, |
| ); |
| command_options.insert( |
| String::from("quit"), |
| CommandOption { |
| rules: vec![String::from("quit")], |
| description: String::from("Quit out of the interactive shell."), |
| function_pointer: _noop, |
| }, |
| ); |
| command_options |
| } |
| |
| // Helper to index a vector safely. The same as `args.get(i)` but converts the None into a |
| // CommandError::InvalidArgs. |
| // |
| // Use this to safely index an argument and conveniently return the error if the argument does not |
| // exist. |
| fn get_arg<I>( |
| args: &Vec<String>, |
| index: I, |
| ) -> Result<&<I as SliceIndex<[String]>>::Output, CommandError> |
| where |
| I: SliceIndex<[String]>, |
| { |
| args.get(index).ok_or(CommandError::InvalidArgs) |
| } |
| |
| 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: &str, args: &Vec<String>) -> bool { |
| // Ignore empty line |
| match command { |
| "" => false, |
| _ => match self.command_options.get(command) { |
| Some(cmd) => { |
| let rules = cmd.rules.clone(); |
| match (cmd.function_pointer)(self, &args) { |
| Ok(()) => true, |
| Err(CommandError::InvalidArgs) => { |
| print_error!("Invalid arguments. Usage:\n{}", rules.join("\n")); |
| false |
| } |
| Err(CommandError::Failed(msg)) => { |
| print_error!("Command failed: {}", msg); |
| false |
| } |
| } |
| } |
| None => { |
| println!("'{}' is an invalid command!", command); |
| self.cmd_help(&args).ok(); |
| false |
| } |
| }, |
| } |
| } |
| |
| fn lock_context(&self) -> std::sync::MutexGuard<ClientContext> { |
| self.context.lock().unwrap() |
| } |
| |
| // Common message for when the adapter isn't ready |
| fn adapter_not_ready(&self) -> CommandError { |
| format!( |
| "Default adapter {} is not enabled. Enable the adapter before using this command.", |
| self.lock_context().default_adapter |
| ) |
| .into() |
| } |
| |
| fn cmd_help(&mut self, args: &Vec<String>) -> CommandResult { |
| if let Some(command) = args.get(0) { |
| match self.command_options.get(command) { |
| Some(cmd) => { |
| println!( |
| "\n{}{}\n{}{}\n", |
| INDENT_CHAR.repeat(4), |
| command, |
| INDENT_CHAR.repeat(8), |
| cmd.description |
| ); |
| } |
| None => { |
| println!("'{}' is an invalid command!", command); |
| self.cmd_help(&vec![]).ok(); |
| } |
| } |
| } 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); |
| } |
| |
| Ok(()) |
| } |
| |
| fn cmd_adapter(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.lock_context().manager_dbus.get_floss_enabled() { |
| return Err("Floss is not enabled. First run, `floss enable`".into()); |
| } |
| |
| let default_adapter = self.lock_context().default_adapter; |
| |
| let command = get_arg(args, 0)?; |
| |
| match &command[..] { |
| "enable" => { |
| if self.lock_context().is_restricted { |
| return Err("You are not allowed to toggle adapter power".into()); |
| } |
| self.lock_context().manager_dbus.start(default_adapter); |
| } |
| "disable" => { |
| if self.lock_context().is_restricted { |
| return Err("You are not allowed to toggle adapter power".into()); |
| } |
| self.lock_context().manager_dbus.stop(default_adapter); |
| } |
| "show" => { |
| if !self.lock_context().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| let enabled = self.lock_context().enabled; |
| let address = match self.lock_context().adapter_address.as_ref() { |
| Some(x) => x.clone(), |
| None => String::from(""), |
| }; |
| let context = self.lock_context(); |
| let adapter_dbus = context.adapter_dbus.as_ref().unwrap(); |
| let qa_dbus = context.qa_dbus.as_ref().unwrap(); |
| let name = adapter_dbus.get_name(); |
| let modalias = qa_dbus.get_modalias(); |
| let uuids = adapter_dbus.get_uuids(); |
| let is_discoverable = adapter_dbus.get_discoverable(); |
| let discoverable_timeout = adapter_dbus.get_discoverable_timeout(); |
| let cod = adapter_dbus.get_bluetooth_class(); |
| let multi_adv_supported = adapter_dbus.is_multi_advertisement_supported(); |
| let le_ext_adv_supported = adapter_dbus.is_le_extended_advertising_supported(); |
| let wbs_supported = adapter_dbus.is_wbs_supported(); |
| let supported_profiles = UuidHelper::get_supported_profiles(); |
| let connected_profiles: Vec<(Profile, ProfileConnectionState)> = supported_profiles |
| .iter() |
| .map(|&prof| { |
| if let Some(uuid) = UuidHelper::get_profile_uuid(&prof) { |
| (prof, adapter_dbus.get_profile_connection_state(uuid.clone())) |
| } else { |
| (prof, ProfileConnectionState::Disconnected) |
| } |
| }) |
| .filter(|(_prof, state)| state != &ProfileConnectionState::Disconnected) |
| .collect(); |
| qa_dbus.fetch_connectable(); |
| qa_dbus.fetch_alias(); |
| qa_dbus.fetch_discoverable_mode(); |
| print_info!("Address: {}", address); |
| print_info!("Name: {}", name); |
| print_info!("Modalias: {}", modalias); |
| print_info!("State: {}", if enabled { "enabled" } else { "disabled" }); |
| print_info!("Discoverable: {}", is_discoverable); |
| print_info!("DiscoverableTimeout: {}s", discoverable_timeout); |
| print_info!("Class: {:#06x}", cod); |
| print_info!("IsMultiAdvertisementSupported: {}", multi_adv_supported); |
| print_info!("IsLeExtendedAdvertisingSupported: {}", le_ext_adv_supported); |
| print_info!("Connected profiles: {:?}", connected_profiles); |
| print_info!("IsWbsSupported: {}", wbs_supported); |
| print_info!( |
| "Uuids: {}", |
| DisplayList( |
| uuids |
| .iter() |
| .map(|&x| UuidHelper::known_uuid_to_string(&x)) |
| .collect::<Vec<String>>() |
| ) |
| ); |
| } |
| "discoverable" => match &get_arg(args, 1)?[..] { |
| "on" => { |
| let duration = String::from(get_arg(args, 2)?) |
| .parse::<u32>() |
| .or(Err("Failed parsing duration."))?; |
| |
| let discoverable = self |
| .lock_context() |
| .adapter_dbus |
| .as_mut() |
| .unwrap() |
| .set_discoverable(BtDiscMode::GeneralDiscoverable, duration); |
| print_info!( |
| "Set discoverable for {} seconds: {}", |
| duration, |
| if discoverable { "succeeded" } else { "failed" } |
| ); |
| } |
| "limited" => { |
| let duration = String::from(get_arg(args, 2)?) |
| .parse::<u32>() |
| .or(Err("Failed parsing duration."))?; |
| |
| let discoverable = self |
| .lock_context() |
| .adapter_dbus |
| .as_mut() |
| .unwrap() |
| .set_discoverable(BtDiscMode::LimitedDiscoverable, duration); |
| print_info!( |
| "Set limited discoverable for {} seconds: {}", |
| duration, |
| if discoverable { "succeeded" } else { "failed" } |
| ); |
| } |
| "off" => { |
| let discoverable = self |
| .lock_context() |
| .adapter_dbus |
| .as_mut() |
| .unwrap() |
| .set_discoverable(BtDiscMode::NonDiscoverable, 0 /*not used*/); |
| print_info!( |
| "Turn discoverable off: {}", |
| if discoverable { "succeeded" } else { "failed" } |
| ); |
| } |
| other => println!("Invalid argument for adapter discoverable '{}'", other), |
| }, |
| "connectable" => match &get_arg(args, 1)?[..] { |
| "on" => { |
| self.lock_context().qa_dbus.as_mut().unwrap().set_connectable(true); |
| } |
| "off" => { |
| self.lock_context().qa_dbus.as_mut().unwrap().set_connectable(false); |
| } |
| other => println!("Invalid argument for adapter connectable '{}'", other), |
| }, |
| "set-name" => { |
| if let Some(name) = args.get(1) { |
| self.lock_context().adapter_dbus.as_ref().unwrap().set_name(name.to_string()); |
| } else { |
| println!("usage: adapter set-name <name>"); |
| } |
| } |
| |
| _ => return Err(CommandError::InvalidArgs), |
| }; |
| |
| Ok(()) |
| } |
| |
| fn cmd_get_address(&mut self, _args: &Vec<String>) -> CommandResult { |
| if !self.lock_context().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| let address = self.lock_context().update_adapter_address(); |
| print_info!("Local address = {}", &address); |
| Ok(()) |
| } |
| |
| fn cmd_discovery(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.lock_context().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| let command = get_arg(args, 0)?; |
| |
| match &command[..] { |
| "start" => { |
| self.lock_context().adapter_dbus.as_mut().unwrap().start_discovery(); |
| } |
| "stop" => { |
| self.lock_context().adapter_dbus.as_mut().unwrap().cancel_discovery(); |
| } |
| _ => return Err(CommandError::InvalidArgs), |
| } |
| |
| Ok(()) |
| } |
| |
| fn cmd_bond(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.lock_context().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| let command = get_arg(args, 0)?; |
| |
| match &command[..] { |
| "add" => { |
| let device = BluetoothDevice { |
| address: String::from(get_arg(args, 1)?), |
| name: String::from("Classic Device"), |
| }; |
| |
| let bonding_attempt = &self.lock_context().bonding_attempt.as_ref().cloned(); |
| |
| if bonding_attempt.is_some() { |
| return Err(format!( |
| "Already bonding [{}]. Cancel bonding first.", |
| bonding_attempt.as_ref().unwrap().address, |
| ) |
| .into()); |
| } |
| |
| let success = self |
| .lock_context() |
| .adapter_dbus |
| .as_mut() |
| .unwrap() |
| .create_bond(device.clone(), BtTransport::Auto); |
| |
| if success { |
| self.lock_context().bonding_attempt = Some(device); |
| } |
| } |
| "remove" => { |
| let device = BluetoothDevice { |
| address: String::from(get_arg(args, 1)?), |
| name: String::from("Classic Device"), |
| }; |
| |
| self.lock_context().adapter_dbus.as_ref().unwrap().remove_bond(device); |
| } |
| "cancel" => { |
| let device = BluetoothDevice { |
| address: String::from(get_arg(args, 1)?), |
| name: String::from("Classic Device"), |
| }; |
| |
| self.lock_context().adapter_dbus.as_ref().unwrap().cancel_bond_process(device); |
| } |
| other => { |
| println!("Invalid argument '{}'", other); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn cmd_device(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.lock_context().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| let command = &get_arg(args, 0)?; |
| |
| match &command[..] { |
| "connect" => { |
| let device = BluetoothDevice { |
| address: String::from(get_arg(args, 1)?), |
| name: String::from("Classic Device"), |
| }; |
| |
| let success = self |
| .lock_context() |
| .adapter_dbus |
| .as_mut() |
| .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(get_arg(args, 1)?), |
| name: String::from("Classic Device"), |
| }; |
| |
| let success = self |
| .lock_context() |
| .adapter_dbus |
| .as_mut() |
| .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(get_arg(args, 1)?), |
| name: String::from("Classic Device"), |
| }; |
| |
| let ( |
| name, |
| alias, |
| device_type, |
| class, |
| appearance, |
| bonded, |
| connection_state, |
| uuids, |
| wake_allowed, |
| ) = { |
| let ctx = self.lock_context(); |
| let adapter = ctx.adapter_dbus.as_ref().unwrap(); |
| |
| let name = adapter.get_remote_name(device.clone()); |
| let device_type = adapter.get_remote_type(device.clone()); |
| let alias = adapter.get_remote_alias(device.clone()); |
| let class = adapter.get_remote_class(device.clone()); |
| let appearance = adapter.get_remote_appearance(device.clone()); |
| let bonded = adapter.get_bond_state(device.clone()); |
| let connection_state = match adapter.get_connection_state(device.clone()) { |
| BtConnectionState::NotConnected => "Not Connected", |
| BtConnectionState::ConnectedOnly => "Connected", |
| _ => "Connected and Paired", |
| }; |
| let uuids = adapter.get_remote_uuids(device.clone()); |
| let wake_allowed = adapter.get_remote_wake_allowed(device.clone()); |
| |
| ( |
| name, |
| alias, |
| device_type, |
| class, |
| appearance, |
| bonded, |
| connection_state, |
| uuids, |
| wake_allowed, |
| ) |
| }; |
| |
| print_info!("Address: {}", &device.address); |
| print_info!("Name: {}", name); |
| print_info!("Alias: {}", alias); |
| print_info!("Type: {:?}", device_type); |
| print_info!("Class: {}", class); |
| print_info!("Appearance: {}", appearance); |
| print_info!("Wake Allowed: {}", wake_allowed); |
| print_info!("Bond State: {:?}", bonded); |
| print_info!("Connection State: {}", connection_state); |
| print_info!( |
| "Uuids: {}", |
| DisplayList( |
| uuids |
| .iter() |
| .map(|&x| UuidHelper::known_uuid_to_string(&x)) |
| .collect::<Vec<String>>() |
| ) |
| ); |
| } |
| "set-alias" => { |
| let new_alias = get_arg(args, 2)?; |
| let device = BluetoothDevice { |
| address: String::from(get_arg(args, 1)?), |
| name: String::from(""), |
| }; |
| let old_alias = self |
| .lock_context() |
| .adapter_dbus |
| .as_ref() |
| .unwrap() |
| .get_remote_alias(device.clone()); |
| println!( |
| "Updating alias for {}: {} -> {}", |
| get_arg(args, 1)?, |
| old_alias, |
| new_alias |
| ); |
| self.lock_context() |
| .adapter_dbus |
| .as_mut() |
| .unwrap() |
| .set_remote_alias(device.clone(), new_alias.clone()); |
| } |
| "set-pairing-confirmation" => { |
| let device = BluetoothDevice { |
| address: String::from(get_arg(args, 1)?), |
| name: String::from(""), |
| }; |
| let accept = match &get_arg(args, 2)?[..] { |
| "accept" => true, |
| "reject" => false, |
| other => { |
| return Err(format!("Failed to parse '{}'", other).into()); |
| } |
| }; |
| |
| self.lock_context() |
| .adapter_dbus |
| .as_mut() |
| .unwrap() |
| .set_pairing_confirmation(device.clone(), accept); |
| } |
| "set-pairing-pin" => { |
| let device = BluetoothDevice { |
| address: String::from(get_arg(args, 1)?), |
| name: String::from(""), |
| }; |
| let pin = get_arg(args, 2)?; |
| |
| let (accept, pin) = match (&pin[..], pin) { |
| ("reject", _) => (false, vec![]), |
| (_, p) => (true, p.as_bytes().iter().cloned().collect::<Vec<u8>>()), |
| }; |
| |
| self.lock_context().adapter_dbus.as_mut().unwrap().set_pin( |
| device.clone(), |
| accept, |
| pin, |
| ); |
| } |
| "set-pairing-passkey" => { |
| let device = BluetoothDevice { |
| address: String::from(get_arg(args, 1)?), |
| name: String::from(""), |
| }; |
| let passkey = get_arg(args, 2)?; |
| let (accept, passkey) = match (&passkey[..], String::from(passkey).parse::<u32>()) { |
| (_, Ok(p)) => (true, Vec::from(p.to_ne_bytes())), |
| ("reject", _) => (false, vec![]), |
| _ => { |
| return Err(format!("Failed to parse '{}'", passkey).into()); |
| } |
| }; |
| |
| self.lock_context().adapter_dbus.as_mut().unwrap().set_passkey( |
| device.clone(), |
| accept, |
| passkey, |
| ); |
| } |
| other => { |
| println!("Invalid argument '{}'", other); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn cmd_floss(&mut self, args: &Vec<String>) -> CommandResult { |
| let command = get_arg(args, 0)?; |
| |
| match &command[..] { |
| "enable" => { |
| self.lock_context().manager_dbus.set_floss_enabled(true); |
| } |
| "disable" => { |
| self.lock_context().manager_dbus.set_floss_enabled(false); |
| } |
| "show" => { |
| print_info!( |
| "Floss enabled: {}", |
| self.lock_context().manager_dbus.get_floss_enabled() |
| ); |
| } |
| _ => return Err(CommandError::InvalidArgs), |
| } |
| |
| Ok(()) |
| } |
| |
| fn cmd_gatt(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.lock_context().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| let command = get_arg(args, 0)?; |
| |
| match &command[..] { |
| "register-client" => { |
| let dbus_connection = self.lock_context().dbus_connection.clone(); |
| let dbus_crossroads = self.lock_context().dbus_crossroads.clone(); |
| |
| self.lock_context().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(), |
| dbus_connection, |
| dbus_crossroads, |
| )), |
| false, |
| ); |
| } |
| "client-connect" => { |
| let client_id = self |
| .lock_context() |
| .gatt_client_context |
| .client_id |
| .ok_or("GATT client is not yet registered.")?; |
| |
| let addr = String::from(get_arg(args, 1)?); |
| let is_direct = self.lock_context().gatt_client_context.is_connect_direct; |
| let transport = self.lock_context().gatt_client_context.connect_transport; |
| let oppurtunistic = self.lock_context().gatt_client_context.connect_opportunistic; |
| let phy = self.lock_context().gatt_client_context.connect_phy; |
| |
| println!("Initiating GATT client connect. client_id: {}, addr: {}, is_direct: {}, transport: {:?}, oppurtunistic: {}, phy: {:?}", client_id, addr, is_direct, transport, oppurtunistic, phy); |
| self.lock_context().gatt_dbus.as_ref().unwrap().client_connect( |
| client_id, |
| addr, |
| is_direct, |
| transport, |
| oppurtunistic, |
| phy, |
| ); |
| } |
| "client-disconnect" => { |
| let client_id = self |
| .lock_context() |
| .gatt_client_context |
| .client_id |
| .ok_or("GATT client is not yet registered.")?; |
| |
| let addr = String::from(get_arg(args, 1)?); |
| self.lock_context().gatt_dbus.as_ref().unwrap().client_disconnect(client_id, addr); |
| } |
| "client-read-phy" => { |
| let client_id = self |
| .lock_context() |
| .gatt_client_context |
| .client_id |
| .ok_or("GATT client is not yet registered.")?; |
| let addr = String::from(get_arg(args, 1)?); |
| self.lock_context().gatt_dbus.as_mut().unwrap().client_read_phy(client_id, addr); |
| } |
| "client-discover-services" => { |
| let client_id = self |
| .lock_context() |
| .gatt_client_context |
| .client_id |
| .ok_or("GATT client is not yet registered.")?; |
| |
| let addr = String::from(get_arg(args, 1)?); |
| self.lock_context().gatt_dbus.as_ref().unwrap().discover_services(client_id, addr); |
| } |
| "client-discover-service-by-uuid-pts" => { |
| let client_id = self |
| .lock_context() |
| .gatt_client_context |
| .client_id |
| .ok_or("GATT client is not yet registered.")?; |
| let addr = String::from(get_arg(args, 1)?); |
| let uuid = String::from(get_arg(args, 2)?); |
| self.lock_context() |
| .gatt_dbus |
| .as_ref() |
| .unwrap() |
| .btif_gattc_discover_service_by_uuid(client_id, addr, uuid); |
| } |
| "configure-mtu" => { |
| let client_id = self |
| .lock_context() |
| .gatt_client_context |
| .client_id |
| .ok_or("GATT client is not yet registered.")?; |
| |
| let addr = String::from(get_arg(args, 1)?); |
| let mtu = |
| String::from(get_arg(args, 2)?).parse::<i32>().or(Err("Failed parsing mtu"))?; |
| |
| self.lock_context().gatt_dbus.as_ref().unwrap().configure_mtu(client_id, addr, mtu) |
| } |
| "set-direct-connect" => { |
| let is_direct = String::from(get_arg(args, 1)?) |
| .parse::<bool>() |
| .or(Err("Failed to parse is_direct"))?; |
| |
| self.lock_context().gatt_client_context.is_connect_direct = is_direct; |
| } |
| "set-connect-transport" => { |
| let transport = match &get_arg(args, 1)?[..] { |
| "Bredr" => BtTransport::Bredr, |
| "LE" => BtTransport::Le, |
| "Auto" => BtTransport::Auto, |
| _ => { |
| return Err("Failed to parse transport".into()); |
| } |
| }; |
| self.lock_context().gatt_client_context.connect_transport = transport; |
| } |
| "set-connect-opportunistic" => { |
| let opportunistic = String::from(get_arg(args, 1)?) |
| .parse::<bool>() |
| .or(Err("Failed to parse opportunistic"))?; |
| |
| self.lock_context().gatt_client_context.connect_opportunistic = opportunistic; |
| } |
| "set-connect-phy" => { |
| let phy = match &get_arg(args, 1)?[..] { |
| "Phy1m" => LePhy::Phy1m, |
| "Phy2m" => LePhy::Phy2m, |
| "PhyCoded" => LePhy::PhyCoded, |
| _ => { |
| return Err("Failed to parse phy".into()); |
| } |
| }; |
| |
| self.lock_context().gatt_client_context.connect_phy = phy; |
| } |
| "set-auth-req" => { |
| let flag = match &get_arg(args, 1)?[..] { |
| "NONE" => AuthReq::NONE, |
| "EncNoMitm" => AuthReq::EncNoMitm, |
| "EncMitm" => AuthReq::EncMitm, |
| "SignedNoMitm" => AuthReq::SignedNoMitm, |
| "SignedMitm" => AuthReq::SignedMitm, |
| _ => { |
| return Err("Failed to parse auth-req".into()); |
| } |
| }; |
| |
| self.lock_context().gatt_client_context.auth_req = flag; |
| println!("AuthReq: {:?}", self.lock_context().gatt_client_context.get_auth_req()); |
| } |
| "write-characteristic" => { |
| let addr = String::from(get_arg(args, 1)?); |
| let handle = String::from(get_arg(args, 2)?) |
| .parse::<i32>() |
| .or(Err("Failed to parse handle"))?; |
| |
| let write_type = match &get_arg(args, 3)?[..] { |
| "NoRsp" => GattWriteType::WriteNoRsp, |
| "Write" => GattWriteType::Write, |
| "Prepare" => GattWriteType::WritePrepare, |
| _ => { |
| return Err("Failed to parse write-type".into()); |
| } |
| }; |
| |
| let value = hex::decode(&get_arg(args, 4)?).or(Err("Failed to parse value"))?; |
| |
| let client_id = self |
| .lock_context() |
| .gatt_client_context |
| .client_id |
| .ok_or("GATT client is not yet registered.")?; |
| |
| let auth_req = self.lock_context().gatt_client_context.get_auth_req().into(); |
| |
| self.lock_context() |
| .gatt_dbus |
| .as_ref() |
| .unwrap() |
| .write_characteristic(client_id, addr, handle, write_type, auth_req, value); |
| } |
| "read-characteristic" => { |
| let addr = String::from(get_arg(args, 1)?); |
| let handle = String::from(get_arg(args, 2)?) |
| .parse::<i32>() |
| .or(Err("Failed to parse handle"))?; |
| let client_id = self |
| .lock_context() |
| .gatt_client_context |
| .client_id |
| .ok_or("GATT client is not yet registered.")?; |
| |
| let auth_req = self.lock_context().gatt_client_context.get_auth_req().into(); |
| |
| self.lock_context() |
| .gatt_dbus |
| .as_ref() |
| .unwrap() |
| .read_characteristic(client_id, addr, handle, auth_req); |
| } |
| "read-characteristic-by-uuid" => { |
| let addr = String::from(get_arg(args, 1)?); |
| let uuid = String::from(get_arg(args, 2)?); |
| let start_handle = String::from(get_arg(args, 3)?) |
| .parse::<i32>() |
| .or(Err("Failed to parse start handle"))?; |
| let end_handle = String::from(get_arg(args, 4)?) |
| .parse::<i32>() |
| .or(Err("Failed to parse end handle"))?; |
| |
| let client_id = self |
| .lock_context() |
| .gatt_client_context |
| .client_id |
| .ok_or("GATT client is not yet registered.")?; |
| |
| let auth_req = self.lock_context().gatt_client_context.get_auth_req().into(); |
| |
| self.lock_context().gatt_dbus.as_ref().unwrap().read_using_characteristic_uuid( |
| client_id, |
| addr, |
| uuid, |
| start_handle, |
| end_handle, |
| auth_req, |
| ); |
| } |
| "register-notification" => { |
| let addr = String::from(get_arg(args, 1)?); |
| let handle = String::from(get_arg(args, 2)?) |
| .parse::<i32>() |
| .or(Err("Failed to parse handle"))?; |
| let enable = match &get_arg(args, 3)?[..] { |
| "enable" => true, |
| "disable" => false, |
| _ => { |
| return Err("Failed to parse enable".into()); |
| } |
| }; |
| |
| let client_id = self |
| .lock_context() |
| .gatt_client_context |
| .client_id |
| .ok_or("GATT client is not yet registered.")?; |
| |
| self.lock_context() |
| .gatt_dbus |
| .as_ref() |
| .unwrap() |
| .register_for_notification(client_id, addr, handle, enable); |
| } |
| "register-server" => { |
| let dbus_connection = self.lock_context().dbus_connection.clone(); |
| let dbus_crossroads = self.lock_context().dbus_crossroads.clone(); |
| |
| self.lock_context().gatt_dbus.as_mut().unwrap().register_server( |
| String::from(GATT_SERVER_APP_UUID), |
| Box::new(BtGattServerCallback::new( |
| String::from( |
| "/org/chromium/bluetooth/client/bluetooth_gatt_server_callback", |
| ), |
| self.context.clone(), |
| dbus_connection, |
| dbus_crossroads, |
| )), |
| false, |
| ); |
| } |
| _ => return Err(CommandError::InvalidArgs), |
| } |
| Ok(()) |
| } |
| |
| fn cmd_le_scan(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.lock_context().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| let command = get_arg(args, 0)?; |
| |
| match &command[..] { |
| "register-scanner" => { |
| let scanner_callback_id = self |
| .lock_context() |
| .scanner_callback_id |
| .ok_or("Cannot register scanner before registering scanner callback")?; |
| |
| let uuid = self |
| .lock_context() |
| .gatt_dbus |
| .as_mut() |
| .unwrap() |
| .register_scanner(scanner_callback_id); |
| |
| print_info!("Scanner to be registered with UUID = {}", UuidWrapper(&uuid)); |
| } |
| "unregister-scanner" => { |
| let scanner_id = String::from(get_arg(args, 1)?) |
| .parse::<u8>() |
| .or(Err("Failed parsing scanner id"))?; |
| |
| self.lock_context().gatt_dbus.as_mut().unwrap().unregister_scanner(scanner_id); |
| } |
| "start-scan" => { |
| let scanner_id = String::from(get_arg(args, 1)?) |
| .parse::<u8>() |
| .or(Err("Failed parsing scanner id"))?; |
| |
| self.lock_context().gatt_dbus.as_mut().unwrap().start_scan( |
| scanner_id, |
| // TODO(b/254870159): Construct real settings and filters depending on |
| // command line options. |
| ScanSettings { interval: 0, window: 0, scan_type: ScanType::Active }, |
| Some(btstack::bluetooth_gatt::ScanFilter { |
| rssi_high_threshold: 0, |
| rssi_low_threshold: 0, |
| rssi_low_timeout: 0, |
| rssi_sampling_period: 0, |
| condition: btstack::bluetooth_gatt::ScanFilterCondition::Patterns(vec![]), |
| }), |
| ); |
| |
| self.lock_context().active_scanner_ids.insert(scanner_id); |
| } |
| "stop-scan" => { |
| let scanner_id = String::from(get_arg(args, 1)?) |
| .parse::<u8>() |
| .or(Err("Failed parsing scanner id"))?; |
| |
| self.lock_context().gatt_dbus.as_mut().unwrap().stop_scan(scanner_id); |
| self.lock_context().active_scanner_ids.remove(&scanner_id); |
| } |
| _ => return Err(CommandError::InvalidArgs), |
| } |
| |
| Ok(()) |
| } |
| |
| // TODO(b/233128828): More options will be implemented to test BLE advertising. |
| // Such as setting advertising parameters, starting multiple advertising sets, etc. |
| fn cmd_advertise(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.lock_context().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| if self.lock_context().advertiser_callback_id == None { |
| return Err("No advertiser callback registered".into()); |
| } |
| |
| let callback_id = self.lock_context().advertiser_callback_id.clone().unwrap(); |
| |
| let command = get_arg(args, 0)?; |
| |
| match &command[..] { |
| "on" => { |
| print_info!("Creating legacy advertising set..."); |
| let s = AdvSet::new(true); // legacy advertising |
| AdvSet::start(self.context.clone(), s, callback_id); |
| } |
| "off" => { |
| AdvSet::stop_all(self.context.clone()); |
| } |
| "ext" => { |
| print_info!("Creating extended advertising set..."); |
| let s = AdvSet::new(false); // extended advertising |
| AdvSet::start(self.context.clone(), s, callback_id); |
| } |
| "set-interval" => { |
| let ms = String::from(get_arg(args, 1)?).parse::<i32>(); |
| if !ms.is_ok() { |
| return Err("Failed parsing interval".into()); |
| } |
| let interval = ms.unwrap() * 8 / 5; // in 0.625 ms. |
| |
| let mut context = self.lock_context(); |
| context.adv_sets.iter_mut().for_each(|(_, s)| s.params.interval = interval); |
| |
| // To avoid borrowing context as mutable from an immutable borrow. |
| // Required information is collected in advance and then passed |
| // to the D-Bus call which requires a mutable borrow. |
| let advs: Vec<(_, _)> = context |
| .adv_sets |
| .iter() |
| .filter_map(|(_, s)| s.adv_id.map(|adv_id| (adv_id.clone(), s.params.clone()))) |
| .collect(); |
| for (adv_id, params) in advs { |
| print_info!("Setting advertising parameters for {}", adv_id); |
| context.gatt_dbus.as_mut().unwrap().set_advertising_parameters(adv_id, params); |
| } |
| } |
| "set-connectable" => { |
| let connectable = match &get_arg(args, 1)?[..] { |
| "on" => true, |
| "off" => false, |
| _ => false, |
| }; |
| |
| let adv_id = String::from(get_arg(args, 2)?) |
| .parse::<i32>() |
| .or(Err("Failed parsing adv_id"))?; |
| |
| let mut context = self.context.lock().unwrap(); |
| |
| let advs: Vec<(_, _)> = context |
| .adv_sets |
| .iter_mut() |
| .filter_map(|(_, s)| { |
| if !(s.adv_id.map_or(false, |id| id == adv_id)) { |
| return None; |
| } |
| s.params.connectable = connectable; |
| Some((s.params.clone(), s.data.clone())) |
| }) |
| .collect(); |
| |
| for (params, data) in advs { |
| print_info!("Setting advertising parameters for {}", adv_id); |
| context.gatt_dbus.as_mut().unwrap().set_advertising_parameters(adv_id, params); |
| |
| // renew the flags |
| print_info!("Setting advertising data for {}", adv_id); |
| context.gatt_dbus.as_mut().unwrap().set_advertising_data(adv_id, data); |
| } |
| } |
| "set-scan-rsp" => { |
| let enable = match &get_arg(args, 1)?[..] { |
| "enable" => true, |
| "disable" => false, |
| _ => false, |
| }; |
| |
| let mut context = self.lock_context(); |
| context.adv_sets.iter_mut().for_each(|(_, s)| s.params.scannable = enable); |
| |
| let advs: Vec<(_, _, _)> = context |
| .adv_sets |
| .iter() |
| .filter_map(|(_, s)| { |
| s.adv_id |
| .map(|adv_id| (adv_id.clone(), s.params.clone(), s.scan_rsp.clone())) |
| }) |
| .collect(); |
| for (adv_id, params, scan_rsp) in advs { |
| print_info!("Setting scan response data for {}", adv_id); |
| context.gatt_dbus.as_mut().unwrap().set_scan_response_data(adv_id, scan_rsp); |
| print_info!("Setting parameters for {}", adv_id); |
| context.gatt_dbus.as_mut().unwrap().set_advertising_parameters(adv_id, params); |
| } |
| } |
| "set-raw-data" => { |
| let data = hex::decode(get_arg(args, 1)?).or(Err("Failed parsing data"))?; |
| |
| let adv_id = String::from(get_arg(args, 2)?) |
| .parse::<i32>() |
| .or(Err("Failed parsing adv_id"))?; |
| |
| let mut context = self.context.lock().unwrap(); |
| if context |
| .adv_sets |
| .iter() |
| .find(|(_, s)| s.adv_id.map_or(false, |id| id == adv_id)) |
| .is_none() |
| { |
| return Err("Failed to find advertising set".into()); |
| } |
| |
| print_info!("Setting advertising data for {}", adv_id); |
| context.gatt_dbus.as_mut().unwrap().set_raw_adv_data(adv_id, data); |
| } |
| _ => return Err(CommandError::InvalidArgs), |
| } |
| |
| Ok(()) |
| } |
| |
| fn cmd_sdp(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.lock_context().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| let command = get_arg(args, 0)?; |
| |
| match &command[..] { |
| "search" => { |
| let device = BluetoothDevice { |
| address: String::from(get_arg(args, 1)?), |
| name: String::from(""), |
| }; |
| let uuid = match UuidHelper::parse_string(get_arg(args, 2)?) { |
| Some(uu) => uu.uu, |
| None => return Err(CommandError::Failed("Invalid UUID".into())), |
| }; |
| let success = |
| self.lock_context().adapter_dbus.as_ref().unwrap().sdp_search(device, uuid); |
| if !success { |
| return Err("Unable to execute SDP search".into()); |
| } |
| } |
| _ => return Err(CommandError::InvalidArgs), |
| } |
| Ok(()) |
| } |
| |
| fn cmd_socket(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.lock_context().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| let callback_id = match self.lock_context().socket_manager_callback_id.clone() { |
| Some(id) => id, |
| None => { |
| return Err("No socket manager callback registered.".into()); |
| } |
| }; |
| |
| let command = get_arg(args, 0)?; |
| |
| match &command[..] { |
| "set-on-connect-schedule" => { |
| let schedule = match &get_arg(args, 1)?[..] { |
| "send" => SocketSchedule { |
| num_frame: 1, |
| send_interval: Duration::from_millis(0), |
| disconnect_delay: Duration::from_secs(30), |
| }, |
| "resend" => SocketSchedule { |
| num_frame: 3, |
| send_interval: Duration::from_millis(100), |
| disconnect_delay: Duration::from_secs(30), |
| }, |
| "dump" => SocketSchedule { |
| num_frame: 0, |
| send_interval: Duration::from_millis(0), |
| disconnect_delay: Duration::from_secs(30), |
| }, |
| _ => { |
| return Err("Failed to parse schedule".into()); |
| } |
| }; |
| |
| self.context.lock().unwrap().socket_test_schedule = Some(schedule); |
| } |
| "send-msc" => { |
| let dlci = |
| String::from(get_arg(args, 1)?).parse::<u8>().or(Err("Failed parsing DLCI"))?; |
| let addr = String::from(get_arg(args, 2)?); |
| self.context.lock().unwrap().qa_dbus.as_mut().unwrap().rfcomm_send_msc(dlci, addr); |
| } |
| "listen-rfcomm" => { |
| let scn = String::from(get_arg(args, 1)?) |
| .parse::<i32>() |
| .or(Err("Failed parsing Service Channel Number"))?; |
| let SocketResult { status, id } = self |
| .context |
| .lock() |
| .unwrap() |
| .socket_manager_dbus |
| .as_mut() |
| .unwrap() |
| .listen_using_rfcomm(callback_id, Some(scn), None, None, None); |
| if status != BtStatus::Success { |
| return Err(format!( |
| "Failed to request for listening using rfcomm, status = {:?}", |
| status, |
| ) |
| .into()); |
| } |
| print_info!("Requested for listening using rfcomm on socket {}", id); |
| } |
| "listen" => { |
| let auth_required = String::from(get_arg(args, 1)?) |
| .parse::<bool>() |
| .or(Err("Failed to parse auth-required"))?; |
| let is_le = match &get_arg(args, 2)?[..] { |
| "LE" => true, |
| "Bredr" => false, |
| _ => { |
| return Err("Failed to parse socket type".into()); |
| } |
| }; |
| |
| let SocketResult { status, id } = { |
| let mut context_proxy = self.context.lock().unwrap(); |
| let proxy = context_proxy.socket_manager_dbus.as_mut().unwrap(); |
| if auth_required { |
| if is_le { |
| proxy.listen_using_l2cap_le_channel(callback_id) |
| } else { |
| proxy.listen_using_l2cap_channel(callback_id) |
| } |
| } else { |
| if is_le { |
| proxy.listen_using_insecure_l2cap_le_channel(callback_id) |
| } else { |
| proxy.listen_using_insecure_l2cap_channel(callback_id) |
| } |
| } |
| }; |
| |
| if status != BtStatus::Success { |
| return Err(format!( |
| "Failed to request for listening using l2cap channel, status = {:?}", |
| status, |
| ) |
| .into()); |
| } |
| print_info!("Requested for listening using l2cap channel on socket {}", id); |
| } |
| "connect" => { |
| let (addr, sock_type, psm_or_uuid) = |
| (&get_arg(args, 1)?, &get_arg(args, 2)?, &get_arg(args, 3)?); |
| let device = BluetoothDevice { |
| address: String::from(*addr), |
| name: String::from("Socket Connect Device"), |
| }; |
| |
| let auth_required = String::from(get_arg(args, 4)?) |
| .parse::<bool>() |
| .or(Err("Failed to parse auth-required"))?; |
| |
| let is_le = match &get_arg(args, 5)?[..] { |
| "LE" => true, |
| "Bredr" => false, |
| _ => { |
| return Err("Failed to parse socket type".into()); |
| } |
| }; |
| |
| let SocketResult { status, id } = { |
| let mut context_proxy = self.context.lock().unwrap(); |
| let proxy = context_proxy.socket_manager_dbus.as_mut().unwrap(); |
| |
| match &sock_type[0..] { |
| "l2cap" => { |
| let psm = match psm_or_uuid.parse::<i32>() { |
| Ok(v) => v, |
| Err(e) => { |
| return Err(CommandError::Failed(format!( |
| "Bad PSM given. Error={}", |
| e |
| ))); |
| } |
| }; |
| |
| if auth_required { |
| if is_le { |
| proxy.create_l2cap_le_channel(callback_id, device, psm) |
| } else { |
| proxy.create_l2cap_channel(callback_id, device, psm) |
| } |
| } else { |
| if is_le { |
| proxy.create_insecure_l2cap_le_channel(callback_id, device, psm) |
| } else { |
| proxy.create_insecure_l2cap_channel(callback_id, device, psm) |
| } |
| } |
| } |
| "rfcomm" => { |
| let uuid = match UuidHelper::parse_string(*psm_or_uuid) { |
| Some(uu) => uu, |
| None => { |
| return Err(CommandError::Failed(format!( |
| "Could not parse given uuid." |
| ))); |
| } |
| }; |
| |
| if auth_required { |
| proxy.create_rfcomm_socket_to_service_record( |
| callback_id, |
| device, |
| uuid, |
| ) |
| } else { |
| proxy.create_insecure_rfcomm_socket_to_service_record( |
| callback_id, |
| device, |
| uuid, |
| ) |
| } |
| } |
| _ => { |
| return Err(CommandError::Failed(format!( |
| "Unknown socket type: {}", |
| sock_type |
| ))); |
| } |
| } |
| }; |
| |
| if status != BtStatus::Success { |
| return Err(CommandError::Failed(format!("Failed to create socket with status={:?} against {}, type {}, with psm/uuid {}", |
| status, addr, sock_type, psm_or_uuid))); |
| } else { |
| print_info!("Called create socket with result ({:?}, {}) against {}, type {}, with psm/uuid {}", |
| status, id, addr, sock_type, psm_or_uuid); |
| } |
| } |
| |
| _ => return Err(CommandError::InvalidArgs), |
| }; |
| |
| Ok(()) |
| } |
| |
| fn cmd_hid(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.context.lock().unwrap().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| let command = get_arg(args, 0)?; |
| |
| match &command[..] { |
| "get-report" => { |
| let addr = String::from(get_arg(args, 1)?); |
| let report_type = match &get_arg(args, 2)?[..] { |
| "Input" => BthhReportType::InputReport, |
| "Output" => BthhReportType::OutputReport, |
| "Feature" => BthhReportType::FeatureReport, |
| _ => { |
| return Err("Failed to parse report type".into()); |
| } |
| }; |
| let report_id = String::from(get_arg(args, 3)?) |
| .parse::<u8>() |
| .or(Err("Failed parsing report_id"))?; |
| |
| self.context.lock().unwrap().qa_dbus.as_mut().unwrap().get_hid_report( |
| addr, |
| report_type, |
| report_id, |
| ); |
| } |
| "set-report" => { |
| let addr = String::from(get_arg(args, 1)?); |
| let report_type = match &get_arg(args, 2)?[..] { |
| "Input" => BthhReportType::InputReport, |
| "Output" => BthhReportType::OutputReport, |
| "Feature" => BthhReportType::FeatureReport, |
| _ => { |
| return Err("Failed to parse report type".into()); |
| } |
| }; |
| let report_value = String::from(get_arg(args, 3)?); |
| |
| self.context.lock().unwrap().qa_dbus.as_mut().unwrap().set_hid_report( |
| addr, |
| report_type, |
| report_value, |
| ); |
| } |
| "send-data" => { |
| let addr = String::from(get_arg(args, 1)?); |
| let data = String::from(get_arg(args, 2)?); |
| |
| self.context.lock().unwrap().qa_dbus.as_mut().unwrap().send_hid_data(addr, data); |
| } |
| _ => return Err(CommandError::InvalidArgs), |
| }; |
| |
| Ok(()) |
| } |
| |
| /// Get the list of rules of supported commands |
| pub fn get_command_rule_list(&self) -> Vec<String> { |
| self.command_options.values().flat_map(|cmd| cmd.rules.clone()).collect() |
| } |
| |
| fn cmd_list_devices(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.lock_context().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| let command = get_arg(args, 0)?; |
| |
| match &command[..] { |
| "bonded" => { |
| print_info!("Known bonded devices:"); |
| let devices = |
| self.lock_context().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.lock_context().found_devices.iter() { |
| print_info!("[{:17}] {}", key, val.name); |
| } |
| } |
| "connected" => { |
| print_info!("Connected devices:"); |
| let devices = |
| self.lock_context().adapter_dbus.as_ref().unwrap().get_connected_devices(); |
| for device in devices.iter() { |
| print_info!("[{:17}] {}", device.address, device.name); |
| } |
| } |
| other => { |
| println!("Invalid argument '{}'", other); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn cmd_telephony(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.context.lock().unwrap().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| match &get_arg(args, 0)?[..] { |
| "set-network" => { |
| self.context |
| .lock() |
| .unwrap() |
| .telephony_dbus |
| .as_mut() |
| .unwrap() |
| .set_network_available(match &get_arg(args, 1)?[..] { |
| "on" => true, |
| "off" => false, |
| other => { |
| return Err(format!("Invalid argument '{}'", other).into()); |
| } |
| }); |
| } |
| "set-roaming" => { |
| self.context.lock().unwrap().telephony_dbus.as_mut().unwrap().set_roaming( |
| match &get_arg(args, 1)?[..] { |
| "on" => true, |
| "off" => false, |
| other => { |
| return Err(format!("Invalid argument '{}'", other).into()); |
| } |
| }, |
| ); |
| } |
| "set-signal" => { |
| let strength = String::from(get_arg(args, 1)?) |
| .parse::<i32>() |
| .or(Err("Failed parsing signal strength"))?; |
| if strength < 0 || strength > 5 { |
| return Err( |
| format!("Invalid signal strength, got {}, want 0 to 5", strength).into() |
| ); |
| } |
| self.context |
| .lock() |
| .unwrap() |
| .telephony_dbus |
| .as_mut() |
| .unwrap() |
| .set_signal_strength(strength); |
| } |
| "set-battery" => { |
| let level = String::from(get_arg(args, 1)?) |
| .parse::<i32>() |
| .or(Err("Failed parsing battery level"))?; |
| if level < 0 || level > 5 { |
| return Err(format!("Invalid battery level, got {}, want 0 to 5", level).into()); |
| } |
| self.context |
| .lock() |
| .unwrap() |
| .telephony_dbus |
| .as_mut() |
| .unwrap() |
| .set_battery_level(level); |
| } |
| "enable" => { |
| let mut context = self.lock_context(); |
| context.telephony_dbus.as_mut().unwrap().set_phone_ops_enabled(true); |
| if context.mps_sdp_handle.is_none() { |
| let success = context |
| .adapter_dbus |
| .as_mut() |
| .unwrap() |
| .create_sdp_record(BtSdpRecord::Mps(BtSdpMpsRecord::default())); |
| if !success { |
| return Err(format!("Failed to create SDP record").into()); |
| } |
| } |
| } |
| "disable" => { |
| let mut context = self.lock_context(); |
| context.telephony_dbus.as_mut().unwrap().set_phone_ops_enabled(false); |
| if let Some(handle) = context.mps_sdp_handle.take() { |
| let success = context.adapter_dbus.as_mut().unwrap().remove_sdp_record(handle); |
| if !success { |
| return Err(format!("Failed to remove SDP record").into()); |
| } |
| } |
| } |
| "incoming-call" => { |
| let success = self |
| .context |
| .lock() |
| .unwrap() |
| .telephony_dbus |
| .as_mut() |
| .unwrap() |
| .incoming_call(String::from(get_arg(args, 1)?)); |
| if !success { |
| return Err("IncomingCall failed".into()); |
| } |
| } |
| "dialing-call" => { |
| let success = self |
| .context |
| .lock() |
| .unwrap() |
| .telephony_dbus |
| .as_mut() |
| .unwrap() |
| .dialing_call(String::from(get_arg(args, 1)?)); |
| if !success { |
| return Err("DialingCall failed".into()); |
| } |
| } |
| "answer-call" => { |
| let success = |
| self.context.lock().unwrap().telephony_dbus.as_mut().unwrap().answer_call(); |
| if !success { |
| return Err("AnswerCall failed".into()); |
| } |
| } |
| "hangup-call" => { |
| let success = |
| self.context.lock().unwrap().telephony_dbus.as_mut().unwrap().hangup_call(); |
| if !success { |
| return Err("HangupCall failed".into()); |
| } |
| } |
| "set-memory-call" => { |
| let success = self |
| .context |
| .lock() |
| .unwrap() |
| .telephony_dbus |
| .as_mut() |
| .unwrap() |
| .set_memory_call(get_arg(args, 1).ok().map(String::from)); |
| if !success { |
| return Err("SetMemoryCall failed".into()); |
| } |
| } |
| "set-last-call" => { |
| let success = self |
| .context |
| .lock() |
| .unwrap() |
| .telephony_dbus |
| .as_mut() |
| .unwrap() |
| .set_last_call(get_arg(args, 1).ok().map(String::from)); |
| if !success { |
| return Err("SetLastCall failed".into()); |
| } |
| } |
| "release-held" => { |
| let success = |
| self.context.lock().unwrap().telephony_dbus.as_mut().unwrap().release_held(); |
| if !success { |
| return Err("ReleaseHeld failed".into()); |
| } |
| } |
| "release-active-accept-held" => { |
| let success = self |
| .context |
| .lock() |
| .unwrap() |
| .telephony_dbus |
| .as_mut() |
| .unwrap() |
| .release_active_accept_held(); |
| if !success { |
| return Err("ReleaseActiveAcceptHeld failed".into()); |
| } |
| } |
| "hold-active-accept-held" => { |
| let success = self |
| .context |
| .lock() |
| .unwrap() |
| .telephony_dbus |
| .as_mut() |
| .unwrap() |
| .hold_active_accept_held(); |
| if !success { |
| return Err("HoldActiveAcceptHeld failed".into()); |
| } |
| } |
| "audio-connect" => { |
| let success = self |
| .context |
| .lock() |
| .unwrap() |
| .telephony_dbus |
| .as_mut() |
| .unwrap() |
| .audio_connect(String::from(get_arg(args, 1)?)); |
| if !success { |
| return Err("ConnectAudio failed".into()); |
| } |
| } |
| "audio-disconnect" => { |
| self.context |
| .lock() |
| .unwrap() |
| .telephony_dbus |
| .as_mut() |
| .unwrap() |
| .audio_disconnect(String::from(get_arg(args, 1)?)); |
| } |
| other => { |
| return Err(format!("Invalid argument '{}'", other).into()); |
| } |
| } |
| Ok(()) |
| } |
| |
| fn cmd_qa(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.context.lock().unwrap().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| let command = get_arg(args, 0)?; |
| |
| match &command[..] { |
| "add-media-player" => { |
| let name = String::from(get_arg(args, 1)?); |
| let browsing_supported = String::from(get_arg(args, 2)?) |
| .parse::<bool>() |
| .or(Err("Failed to parse browsing_supported"))?; |
| self.context |
| .lock() |
| .unwrap() |
| .qa_dbus |
| .as_mut() |
| .unwrap() |
| .add_media_player(name, browsing_supported); |
| } |
| _ => return Err(CommandError::InvalidArgs), |
| }; |
| |
| Ok(()) |
| } |
| |
| fn cmd_media(&mut self, args: &Vec<String>) -> CommandResult { |
| if !self.context.lock().unwrap().adapter_ready { |
| return Err(self.adapter_not_ready()); |
| } |
| |
| match &get_arg(args, 0)?[..] { |
| "log" => { |
| self.context.lock().unwrap().media_dbus.as_mut().unwrap().trigger_debug_dump(); |
| } |
| other => { |
| return Err(format!("Invalid argument '{}'", other).into()); |
| } |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| #[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)); |
| } |
| } |