Loading system/gd/rust/linux/client/src/callbacks.rs +11 −4 Original line number Diff line number Diff line Loading @@ -134,6 +134,8 @@ impl IBluetoothCallback for BtCallback { Some(_) => print_info!("Removed device: {:?}", remote_device), None => (), }; self.context.lock().unwrap().bonded_devices.remove(&remote_device.address); } fn on_discovering_changed(&self, discovering: bool) { Loading Loading @@ -209,12 +211,17 @@ impl IBluetoothCallback for BtCallback { BtBondState::Bonding => (), } let device = BluetoothDevice { address: address.clone(), name: String::from("Classic device") }; // If bonded, we should also automatically connect all enabled profiles if BtBondState::Bonded == state.into() { self.context.lock().unwrap().connect_all_enabled_profiles(BluetoothDevice { address, name: String::from("Classic device"), }); self.context.lock().unwrap().bonded_devices.insert(address.clone(), device.clone()); self.context.lock().unwrap().connect_all_enabled_profiles(device.clone()); } if BtBondState::NotBonded == state.into() { self.context.lock().unwrap().bonded_devices.remove(&address); } } } Loading system/gd/rust/linux/client/src/command_handler.rs +23 −3 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ fn _noop(_handler: &mut CommandHandler, _args: &Vec<String>) { } pub struct CommandOption { rules: Vec<String>, description: String, function_pointer: CommandFunction, } Loading Loading @@ -84,6 +85,7 @@ fn build_commands() -> HashMap<String, CommandOption> { command_options.insert( String::from("adapter"), CommandOption { rules: vec![String::from("adapter <enable|disable|show>")], description: String::from( "Enable/Disable/Show default bluetooth adapter. (e.g. adapter enable)\n Discoverable On/Off (e.g. adapter discoverable on)", Loading @@ -94,6 +96,7 @@ fn build_commands() -> HashMap<String, CommandOption> { 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, }, Loading @@ -101,6 +104,7 @@ fn build_commands() -> HashMap<String, CommandOption> { command_options.insert( String::from("device"), CommandOption { rules: vec![String::from("device <connect|disconnect|info|set-alias> <address>")], description: String::from("Take action on a remote device. (i.e. info)"), function_pointer: CommandHandler::cmd_device, }, Loading @@ -108,6 +112,7 @@ fn build_commands() -> HashMap<String, CommandOption> { 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, }, Loading @@ -115,6 +120,7 @@ fn build_commands() -> HashMap<String, CommandOption> { 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, }, Loading @@ -122,6 +128,12 @@ fn build_commands() -> HashMap<String, CommandOption> { 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>"), ], description: String::from("GATT tools"), function_pointer: CommandHandler::cmd_gatt, }, Loading @@ -129,6 +141,10 @@ fn build_commands() -> HashMap<String, CommandOption> { command_options.insert( String::from("le-scan"), CommandOption { rules: vec![ String::from("le-scan register-scanner"), String::from("le-scan unregister-scanner <scanner-id>"), ], description: String::from("LE scanning utilities."), function_pointer: CommandHandler::cmd_le_scan, }, Loading @@ -136,6 +152,7 @@ fn build_commands() -> HashMap<String, CommandOption> { 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, }, Loading @@ -143,6 +160,7 @@ fn build_commands() -> HashMap<String, CommandOption> { command_options.insert( String::from("help"), CommandOption { rules: vec![String::from("help")], description: String::from("Shows this menu."), function_pointer: CommandHandler::cmd_help, }, Loading @@ -150,6 +168,7 @@ fn build_commands() -> HashMap<String, CommandOption> { command_options.insert( String::from("list"), CommandOption { rules: vec![String::from("list <bonded|found>")], description: String::from( "List bonded or found remote devices. Use: list <bonded|found>", ), Loading @@ -159,6 +178,7 @@ fn build_commands() -> HashMap<String, CommandOption> { command_options.insert( String::from("quit"), CommandOption { rules: vec![String::from("quit")], description: String::from("Quit out of the interactive shell."), function_pointer: _noop, }, Loading Loading @@ -713,9 +733,9 @@ impl CommandHandler { }); } /// 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>>() /// 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>) { Loading system/gd/rust/linux/client/src/editor.rs +94 −12 Original line number Diff line number Diff line Loading @@ -10,22 +10,44 @@ use rustyline::validate::Validator; use rustyline::{CompletionType, Config, Editor}; use rustyline_derive::Helper; use std::collections::HashSet; use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; use crate::console_blue; use crate::ClientContext; #[derive(Helper)] struct BtHelper { commands: Vec<String>, // Command rules must follow below format: // cmd arg1 arg2 arg3 ... // where each argument could have multiple options separated by a single '|' // // It is not required to put an argument in angle brackets. // // "address" in options is a keyword, which will be matched by any of the founded // and bonded devices. // // Example: // list <found|bonded> <address> // This will match // list found any-cached-address // and // list bond any-cached-address command_rules: Vec<String>, client_context: Arc<Mutex<ClientContext>>, } #[derive(Hash, Eq, PartialEq)] struct CommandCandidate { suggest_word: String, matched_len: usize, } impl Completer for BtHelper { type Candidate = String; // Returns completion based on supported commands. // TODO: Add support to autocomplete BT address, command parameters, etc. fn complete( &self, line: &str, Loading @@ -33,13 +55,22 @@ impl Completer for BtHelper { _ctx: &rustyline::Context<'_>, ) -> Result<(usize, Vec<String>), ReadlineError> { let slice = &line[..pos]; let mut completions = vec![]; for cmd in self.commands.iter() { if cmd.starts_with(slice) { completions.push(cmd.clone()); } let candidates = self.get_candidates(slice.to_string().clone()); let mut completions = candidates .iter() .map(|c| { if candidates.len() == 1 { // If only one candidate, Completer will replace the input by // the returned string. Return the complete string here to avoid // input being replaced by the suggested word. slice.to_string() + &c.suggest_word[c.matched_len..] + " " } else { c.suggest_word.clone() } }) .collect::<Vec<String>>(); completions.sort(); Ok((0, completions)) } Loading @@ -53,6 +84,54 @@ impl Highlighter for BtHelper {} impl Validator for BtHelper {} impl BtHelper { fn get_candidates(&self, cmd: String) -> HashSet<CommandCandidate> { let mut result = HashSet::<CommandCandidate>::new(); for rule in self.command_rules.iter() { for (i, (rule_token, cmd_token)) in rule.split(" ").zip(cmd.split(" ")).enumerate() { let mut candidates = Vec::<String>::new(); let mut match_some = false; let n_cmd = cmd.split(" ").count(); for opt in rule_token.replace("<", "").replace(">", "").split("|") { if opt.eq("address") { let devices = self.client_context.lock().unwrap().get_devices(); candidates.extend(devices); } else { candidates.push(opt.to_string()); } } if cmd_token.len() == 0 { candidates.iter().for_each(|s| { result.insert(CommandCandidate { suggest_word: s.clone(), matched_len: 0 }); }); break; } for opt in candidates { if opt.starts_with(cmd_token) { match_some = true; if i == n_cmd - 1 { // we add candidates only if it's the last word result.insert(CommandCandidate { suggest_word: opt.clone(), matched_len: cmd_token.len(), }); } } } if !match_some { break; } } } result } } /// A future that does async readline(). /// /// async readline() is implemented by spawning a thread for the blocking readline(). While this Loading Loading @@ -94,14 +173,17 @@ impl AsyncEditor { /// Creates new async rustyline editor. /// /// * `commands` - List of commands for autocomplete. pub fn new(commands: Vec<String>) -> AsyncEditor { pub(crate) fn new( command_rules: Vec<String>, client_context: Arc<Mutex<ClientContext>>, ) -> AsyncEditor { let builder = Config::builder() .auto_add_history(true) .history_ignore_dups(true) .completion_type(CompletionType::List); let config = builder.build(); let mut rl = rustyline::Editor::with_config(config); let helper = BtHelper { commands }; let helper = BtHelper { command_rules, client_context }; rl.set_helper(Some(helper)); AsyncEditor { rl: Arc::new(Mutex::new(rl)) } } Loading system/gd/rust/linux/client/src/main.rs +35 −2 Original line number Diff line number Diff line Loading @@ -55,6 +55,9 @@ pub(crate) struct ClientContext { /// session starts so that previous results don't pollute current search. pub(crate) found_devices: HashMap<String, BluetoothDevice>, /// List of bonded devices. pub(crate) bonded_devices: HashMap<String, BluetoothDevice>, /// If set, the registered GATT client id. None otherwise. pub(crate) gatt_client_id: Option<i32>, Loading Loading @@ -102,6 +105,7 @@ impl ClientContext { bonding_attempt: None, discovering_state: false, found_devices: HashMap::new(), bonded_devices: HashMap::new(), gatt_client_id: None, manager_dbus, adapter_dbus: None, Loading Loading @@ -163,6 +167,15 @@ impl ClientContext { address } // Foreground-only: Updates bonded devices. fn update_bonded_devices(&mut self) { let bonded_devices = self.adapter_dbus.as_ref().unwrap().get_bonded_devices(); for device in bonded_devices { self.bonded_devices.insert(device.address.clone(), device.clone()); } } fn connect_all_enabled_profiles(&mut self, device: BluetoothDevice) { let fg = self.fg.clone(); tokio::spawn(async move { Loading @@ -176,6 +189,23 @@ impl ClientContext { let _ = fg.send(ForegroundActions::RunCallback(callback)).await; }); } fn get_devices(&self) -> Vec<String> { let mut result: Vec<String> = vec![]; result.extend( self.found_devices.keys().map(|key| String::from(key)).collect::<Vec<String>>(), ); result.extend( self.bonded_devices .keys() .filter(|key| !self.found_devices.contains_key(&String::from(*key))) .map(|key| String::from(key)) .collect::<Vec<String>>(), ); result } } /// Actions to take on the foreground loop. This allows us to queue actions in Loading Loading @@ -282,14 +312,15 @@ async fn start_interactive_shell( mut rx: mpsc::Receiver<ForegroundActions>, context: Arc<Mutex<ClientContext>>, ) { let command_list = handler.get_command_list().clone(); let command_rule_list = handler.get_command_rule_list().clone(); let context_for_closure = context.clone(); let semaphore_fg = Arc::new(tokio::sync::Semaphore::new(1)); // Async task to keep reading new lines from user let semaphore = semaphore_fg.clone(); tokio::spawn(async move { let editor = AsyncEditor::new(command_list); let editor = AsyncEditor::new(command_rule_list, context_for_closure); loop { // Wait until ForegroundAction::Readline finishes its task. Loading Loading @@ -403,6 +434,8 @@ async fn start_interactive_shell( context.lock().unwrap().adapter_ready = true; let adapter_address = context.lock().unwrap().update_adapter_address(); context.lock().unwrap().update_bonded_devices(); print_info!("Adapter {} is ready", adapter_address); } ForegroundActions::Readline(result) => match result { Loading Loading
system/gd/rust/linux/client/src/callbacks.rs +11 −4 Original line number Diff line number Diff line Loading @@ -134,6 +134,8 @@ impl IBluetoothCallback for BtCallback { Some(_) => print_info!("Removed device: {:?}", remote_device), None => (), }; self.context.lock().unwrap().bonded_devices.remove(&remote_device.address); } fn on_discovering_changed(&self, discovering: bool) { Loading Loading @@ -209,12 +211,17 @@ impl IBluetoothCallback for BtCallback { BtBondState::Bonding => (), } let device = BluetoothDevice { address: address.clone(), name: String::from("Classic device") }; // If bonded, we should also automatically connect all enabled profiles if BtBondState::Bonded == state.into() { self.context.lock().unwrap().connect_all_enabled_profiles(BluetoothDevice { address, name: String::from("Classic device"), }); self.context.lock().unwrap().bonded_devices.insert(address.clone(), device.clone()); self.context.lock().unwrap().connect_all_enabled_profiles(device.clone()); } if BtBondState::NotBonded == state.into() { self.context.lock().unwrap().bonded_devices.remove(&address); } } } Loading
system/gd/rust/linux/client/src/command_handler.rs +23 −3 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ fn _noop(_handler: &mut CommandHandler, _args: &Vec<String>) { } pub struct CommandOption { rules: Vec<String>, description: String, function_pointer: CommandFunction, } Loading Loading @@ -84,6 +85,7 @@ fn build_commands() -> HashMap<String, CommandOption> { command_options.insert( String::from("adapter"), CommandOption { rules: vec![String::from("adapter <enable|disable|show>")], description: String::from( "Enable/Disable/Show default bluetooth adapter. (e.g. adapter enable)\n Discoverable On/Off (e.g. adapter discoverable on)", Loading @@ -94,6 +96,7 @@ fn build_commands() -> HashMap<String, CommandOption> { 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, }, Loading @@ -101,6 +104,7 @@ fn build_commands() -> HashMap<String, CommandOption> { command_options.insert( String::from("device"), CommandOption { rules: vec![String::from("device <connect|disconnect|info|set-alias> <address>")], description: String::from("Take action on a remote device. (i.e. info)"), function_pointer: CommandHandler::cmd_device, }, Loading @@ -108,6 +112,7 @@ fn build_commands() -> HashMap<String, CommandOption> { 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, }, Loading @@ -115,6 +120,7 @@ fn build_commands() -> HashMap<String, CommandOption> { 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, }, Loading @@ -122,6 +128,12 @@ fn build_commands() -> HashMap<String, CommandOption> { 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>"), ], description: String::from("GATT tools"), function_pointer: CommandHandler::cmd_gatt, }, Loading @@ -129,6 +141,10 @@ fn build_commands() -> HashMap<String, CommandOption> { command_options.insert( String::from("le-scan"), CommandOption { rules: vec![ String::from("le-scan register-scanner"), String::from("le-scan unregister-scanner <scanner-id>"), ], description: String::from("LE scanning utilities."), function_pointer: CommandHandler::cmd_le_scan, }, Loading @@ -136,6 +152,7 @@ fn build_commands() -> HashMap<String, CommandOption> { 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, }, Loading @@ -143,6 +160,7 @@ fn build_commands() -> HashMap<String, CommandOption> { command_options.insert( String::from("help"), CommandOption { rules: vec![String::from("help")], description: String::from("Shows this menu."), function_pointer: CommandHandler::cmd_help, }, Loading @@ -150,6 +168,7 @@ fn build_commands() -> HashMap<String, CommandOption> { command_options.insert( String::from("list"), CommandOption { rules: vec![String::from("list <bonded|found>")], description: String::from( "List bonded or found remote devices. Use: list <bonded|found>", ), Loading @@ -159,6 +178,7 @@ fn build_commands() -> HashMap<String, CommandOption> { command_options.insert( String::from("quit"), CommandOption { rules: vec![String::from("quit")], description: String::from("Quit out of the interactive shell."), function_pointer: _noop, }, Loading Loading @@ -713,9 +733,9 @@ impl CommandHandler { }); } /// 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>>() /// 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>) { Loading
system/gd/rust/linux/client/src/editor.rs +94 −12 Original line number Diff line number Diff line Loading @@ -10,22 +10,44 @@ use rustyline::validate::Validator; use rustyline::{CompletionType, Config, Editor}; use rustyline_derive::Helper; use std::collections::HashSet; use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; use crate::console_blue; use crate::ClientContext; #[derive(Helper)] struct BtHelper { commands: Vec<String>, // Command rules must follow below format: // cmd arg1 arg2 arg3 ... // where each argument could have multiple options separated by a single '|' // // It is not required to put an argument in angle brackets. // // "address" in options is a keyword, which will be matched by any of the founded // and bonded devices. // // Example: // list <found|bonded> <address> // This will match // list found any-cached-address // and // list bond any-cached-address command_rules: Vec<String>, client_context: Arc<Mutex<ClientContext>>, } #[derive(Hash, Eq, PartialEq)] struct CommandCandidate { suggest_word: String, matched_len: usize, } impl Completer for BtHelper { type Candidate = String; // Returns completion based on supported commands. // TODO: Add support to autocomplete BT address, command parameters, etc. fn complete( &self, line: &str, Loading @@ -33,13 +55,22 @@ impl Completer for BtHelper { _ctx: &rustyline::Context<'_>, ) -> Result<(usize, Vec<String>), ReadlineError> { let slice = &line[..pos]; let mut completions = vec![]; for cmd in self.commands.iter() { if cmd.starts_with(slice) { completions.push(cmd.clone()); } let candidates = self.get_candidates(slice.to_string().clone()); let mut completions = candidates .iter() .map(|c| { if candidates.len() == 1 { // If only one candidate, Completer will replace the input by // the returned string. Return the complete string here to avoid // input being replaced by the suggested word. slice.to_string() + &c.suggest_word[c.matched_len..] + " " } else { c.suggest_word.clone() } }) .collect::<Vec<String>>(); completions.sort(); Ok((0, completions)) } Loading @@ -53,6 +84,54 @@ impl Highlighter for BtHelper {} impl Validator for BtHelper {} impl BtHelper { fn get_candidates(&self, cmd: String) -> HashSet<CommandCandidate> { let mut result = HashSet::<CommandCandidate>::new(); for rule in self.command_rules.iter() { for (i, (rule_token, cmd_token)) in rule.split(" ").zip(cmd.split(" ")).enumerate() { let mut candidates = Vec::<String>::new(); let mut match_some = false; let n_cmd = cmd.split(" ").count(); for opt in rule_token.replace("<", "").replace(">", "").split("|") { if opt.eq("address") { let devices = self.client_context.lock().unwrap().get_devices(); candidates.extend(devices); } else { candidates.push(opt.to_string()); } } if cmd_token.len() == 0 { candidates.iter().for_each(|s| { result.insert(CommandCandidate { suggest_word: s.clone(), matched_len: 0 }); }); break; } for opt in candidates { if opt.starts_with(cmd_token) { match_some = true; if i == n_cmd - 1 { // we add candidates only if it's the last word result.insert(CommandCandidate { suggest_word: opt.clone(), matched_len: cmd_token.len(), }); } } } if !match_some { break; } } } result } } /// A future that does async readline(). /// /// async readline() is implemented by spawning a thread for the blocking readline(). While this Loading Loading @@ -94,14 +173,17 @@ impl AsyncEditor { /// Creates new async rustyline editor. /// /// * `commands` - List of commands for autocomplete. pub fn new(commands: Vec<String>) -> AsyncEditor { pub(crate) fn new( command_rules: Vec<String>, client_context: Arc<Mutex<ClientContext>>, ) -> AsyncEditor { let builder = Config::builder() .auto_add_history(true) .history_ignore_dups(true) .completion_type(CompletionType::List); let config = builder.build(); let mut rl = rustyline::Editor::with_config(config); let helper = BtHelper { commands }; let helper = BtHelper { command_rules, client_context }; rl.set_helper(Some(helper)); AsyncEditor { rl: Arc::new(Mutex::new(rl)) } } Loading
system/gd/rust/linux/client/src/main.rs +35 −2 Original line number Diff line number Diff line Loading @@ -55,6 +55,9 @@ pub(crate) struct ClientContext { /// session starts so that previous results don't pollute current search. pub(crate) found_devices: HashMap<String, BluetoothDevice>, /// List of bonded devices. pub(crate) bonded_devices: HashMap<String, BluetoothDevice>, /// If set, the registered GATT client id. None otherwise. pub(crate) gatt_client_id: Option<i32>, Loading Loading @@ -102,6 +105,7 @@ impl ClientContext { bonding_attempt: None, discovering_state: false, found_devices: HashMap::new(), bonded_devices: HashMap::new(), gatt_client_id: None, manager_dbus, adapter_dbus: None, Loading Loading @@ -163,6 +167,15 @@ impl ClientContext { address } // Foreground-only: Updates bonded devices. fn update_bonded_devices(&mut self) { let bonded_devices = self.adapter_dbus.as_ref().unwrap().get_bonded_devices(); for device in bonded_devices { self.bonded_devices.insert(device.address.clone(), device.clone()); } } fn connect_all_enabled_profiles(&mut self, device: BluetoothDevice) { let fg = self.fg.clone(); tokio::spawn(async move { Loading @@ -176,6 +189,23 @@ impl ClientContext { let _ = fg.send(ForegroundActions::RunCallback(callback)).await; }); } fn get_devices(&self) -> Vec<String> { let mut result: Vec<String> = vec![]; result.extend( self.found_devices.keys().map(|key| String::from(key)).collect::<Vec<String>>(), ); result.extend( self.bonded_devices .keys() .filter(|key| !self.found_devices.contains_key(&String::from(*key))) .map(|key| String::from(key)) .collect::<Vec<String>>(), ); result } } /// Actions to take on the foreground loop. This allows us to queue actions in Loading Loading @@ -282,14 +312,15 @@ async fn start_interactive_shell( mut rx: mpsc::Receiver<ForegroundActions>, context: Arc<Mutex<ClientContext>>, ) { let command_list = handler.get_command_list().clone(); let command_rule_list = handler.get_command_rule_list().clone(); let context_for_closure = context.clone(); let semaphore_fg = Arc::new(tokio::sync::Semaphore::new(1)); // Async task to keep reading new lines from user let semaphore = semaphore_fg.clone(); tokio::spawn(async move { let editor = AsyncEditor::new(command_list); let editor = AsyncEditor::new(command_rule_list, context_for_closure); loop { // Wait until ForegroundAction::Readline finishes its task. Loading Loading @@ -403,6 +434,8 @@ async fn start_interactive_shell( context.lock().unwrap().adapter_ready = true; let adapter_address = context.lock().unwrap().update_adapter_address(); context.lock().unwrap().update_bonded_devices(); print_info!("Adapter {} is ready", adapter_address); } ForegroundActions::Readline(result) => match result { Loading