1 // Copyright 2022 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 use std::cmp::max; 16 17 use crate::args::{self, Beacon, BeaconCreate, BeaconPatch, Capture, Command, OnOffState}; 18 use crate::display::Displayer; 19 use netsim_common::util::time_display::TimeDisplay; 20 use netsim_proto::{ 21 common::ChipKind, 22 frontend::{CreateDeviceResponse, ListCaptureResponse, ListDeviceResponse, VersionResponse}, 23 model, 24 }; 25 use protobuf::Message; 26 27 impl args::Command { 28 /// Format and print the response received from the frontend server for the command print_response(&self, response: &[u8], verbose: bool)29 pub fn print_response(&self, response: &[u8], verbose: bool) { 30 match self { 31 Command::Version => { 32 Self::print_version_response(VersionResponse::parse_from_bytes(response).unwrap()); 33 } 34 Command::Radio(cmd) => { 35 if verbose { 36 println!( 37 "Radio {} is {} for {}", 38 cmd.radio_type, 39 cmd.status, 40 cmd.name.to_owned() 41 ); 42 } 43 } 44 Command::Move(cmd) => { 45 if verbose { 46 println!( 47 "Moved device:{} to x: {:.2}, y: {:.2}, z: {:.2}", 48 cmd.name, 49 cmd.x, 50 cmd.y, 51 cmd.z.unwrap_or_default() 52 ) 53 } 54 } 55 Command::Devices(_) => { 56 println!( 57 "{}", 58 Displayer::new( 59 ListDeviceResponse::parse_from_bytes(response).unwrap(), 60 verbose 61 ) 62 ); 63 } 64 Command::Reset => { 65 if verbose { 66 println!("All devices have been reset."); 67 } 68 } 69 Command::Capture(Capture::List(cmd)) => Self::print_list_capture_response( 70 ListCaptureResponse::parse_from_bytes(response).unwrap(), 71 verbose, 72 cmd.patterns.to_owned(), 73 ), 74 Command::Capture(Capture::Patch(cmd)) => { 75 if verbose { 76 println!( 77 "Patched Capture state to {}", 78 Self::on_off_state_to_string(cmd.state), 79 ); 80 } 81 } 82 Command::Capture(Capture::Get(cmd)) => { 83 if verbose { 84 println!("Successfully downloaded file: {}", cmd.current_file); 85 } 86 } 87 Command::Gui => { 88 unimplemented!("No Grpc Response for Gui Command."); 89 } 90 Command::Artifact => { 91 unimplemented!("No Grpc Response for Artifact Command."); 92 } 93 Command::Beacon(action) => match action { 94 Beacon::Create(kind) => match kind { 95 BeaconCreate::Ble(_) => { 96 if !verbose { 97 return; 98 } 99 let device = CreateDeviceResponse::parse_from_bytes(response) 100 .expect("could not read device from response") 101 .device; 102 103 if device.chips.len() == 1 { 104 println!( 105 "Created device '{}' with ble beacon chip '{}'", 106 device.name, device.chips[0].name 107 ); 108 } else { 109 panic!("the gRPC request completed successfully but the response contained an unexpected number of chips"); 110 } 111 } 112 }, 113 Beacon::Patch(kind) => { 114 match kind { 115 BeaconPatch::Ble(args) => { 116 if !verbose { 117 return; 118 } 119 if let Some(advertise_mode) = &args.settings.advertise_mode { 120 match advertise_mode { 121 args::Interval::Mode(mode) => { 122 println!("Set advertise mode to {:#?}", mode) 123 } 124 args::Interval::Milliseconds(ms) => { 125 println!("Set advertise interval to {} ms", ms) 126 } 127 } 128 } 129 if let Some(tx_power_level) = &args.settings.tx_power_level { 130 match tx_power_level { 131 args::TxPower::Level(level) => { 132 println!("Set transmit power level to {:#?}", level) 133 } 134 args::TxPower::Dbm(dbm) => { 135 println!("Set transmit power level to {} dBm", dbm) 136 } 137 } 138 } 139 if args.settings.scannable { 140 println!("Set scannable to true"); 141 } 142 if let Some(timeout) = args.settings.timeout { 143 println!("Set timeout to {} ms", timeout); 144 } 145 if args.advertise_data.include_device_name { 146 println!("Added the device's name to the advertise packet") 147 } 148 if args.advertise_data.include_tx_power_level { 149 println!("Added the beacon's transmit power level to the advertise packet") 150 } 151 if args.advertise_data.manufacturer_data.is_some() { 152 println!("Added manufacturer data to the advertise packet") 153 } 154 if args.settings.scannable { 155 println!("Set scannable to true"); 156 } 157 if let Some(timeout) = args.settings.timeout { 158 println!("Set timeout to {} ms", timeout); 159 } 160 } 161 } 162 } 163 Beacon::Remove(args) => { 164 if !verbose { 165 return; 166 } 167 if let Some(chip_name) = &args.chip_name { 168 println!("Removed chip '{}' from device '{}'", chip_name, args.device_name) 169 } else { 170 println!("Removed device '{}'", args.device_name) 171 } 172 } 173 }, 174 Command::Bumble => { 175 unimplemented!("No Grpc Response for Bumble Command."); 176 } 177 } 178 } 179 capture_state_to_string(state: Option<bool>) -> String180 fn capture_state_to_string(state: Option<bool>) -> String { 181 state.map(|value| if value { "on" } else { "off" }).unwrap_or("unknown").to_string() 182 } 183 on_off_state_to_string(state: OnOffState) -> String184 fn on_off_state_to_string(state: OnOffState) -> String { 185 match state { 186 OnOffState::On => "on".to_string(), 187 OnOffState::Off => "off".to_string(), 188 } 189 } 190 191 /// Helper function to format and print VersionResponse print_version_response(response: VersionResponse)192 fn print_version_response(response: VersionResponse) { 193 println!("Netsim version: {}", response.version); 194 } 195 196 /// Helper function to format and print ListCaptureResponse print_list_capture_response( mut response: ListCaptureResponse, verbose: bool, patterns: Vec<String>, )197 fn print_list_capture_response( 198 mut response: ListCaptureResponse, 199 verbose: bool, 200 patterns: Vec<String>, 201 ) { 202 if response.captures.is_empty() { 203 if verbose { 204 println!("No available Capture found."); 205 } 206 return; 207 } 208 if patterns.is_empty() { 209 println!("List of Captures:"); 210 } else { 211 // Filter out list of captures with matching patterns 212 Self::filter_captures(&mut response.captures, &patterns); 213 if response.captures.is_empty() { 214 if verbose { 215 println!("No available Capture found matching pattern(s) `{:?}`:", patterns); 216 } 217 return; 218 } 219 println!("List of Captures matching pattern(s) `{:?}`:", patterns); 220 } 221 // Create the header row and determine column widths 222 let id_hdr = "ID"; 223 let name_hdr = "Device Name"; 224 let chipkind_hdr = "Chip Kind"; 225 let state_hdr = "State"; 226 let time_hdr = "Timestamp"; 227 let records_hdr = "Records"; 228 let size_hdr = "Size (bytes)"; 229 let id_width = 4; // ID width of 4 since capture id (=chip_id) starts at 1000 230 let state_width = 8; // State width of 8 for 'detached' if device is disconnected 231 let chipkind_width = 11; // ChipKind width 11 for 'UNSPECIFIED' 232 let time_width = 9; // Timestamp width 9 for header (value format set to HH:MM:SS) 233 let name_width = max( 234 (response.captures.iter().max_by_key(|x| x.device_name.len())) 235 .unwrap_or_default() 236 .device_name 237 .len(), 238 name_hdr.len(), 239 ); 240 let records_width = max( 241 (response.captures.iter().max_by_key(|x| x.records)) 242 .unwrap_or_default() 243 .records 244 .to_string() 245 .len(), 246 records_hdr.len(), 247 ); 248 let size_width = max( 249 (response.captures.iter().max_by_key(|x| x.size)) 250 .unwrap_or_default() 251 .size 252 .to_string() 253 .len(), 254 size_hdr.len(), 255 ); 256 // Print header for capture list 257 println!( 258 "{}", 259 if verbose { 260 format!("{:id_width$} | {:name_width$} | {:chipkind_width$} | {:state_width$} | {:time_width$} | {:records_width$} | {:size_width$} |", 261 id_hdr, 262 name_hdr, 263 chipkind_hdr, 264 state_hdr, 265 time_hdr, 266 records_hdr, 267 size_hdr, 268 ) 269 } else { 270 format!( 271 "{:name_width$} | {:chipkind_width$} | {:state_width$} | {:records_width$} |", 272 name_hdr, chipkind_hdr, state_hdr, records_hdr 273 ) 274 } 275 ); 276 // Print information of each Capture 277 for capture in &response.captures { 278 println!( 279 "{}", 280 if verbose { 281 format!("{:id_width$} | {:name_width$} | {:chipkind_width$} | {:state_width$} | {:time_width$} | {:records_width$} | {:size_width$} |", 282 capture.id.to_string(), 283 capture.device_name, 284 Self::chip_kind_to_string(capture.chip_kind.enum_value_or_default()), 285 if capture.valid {Self::capture_state_to_string(capture.state)} else {"detached".to_string()}, 286 TimeDisplay::new( 287 capture.timestamp.get_or_default().seconds, 288 capture.timestamp.get_or_default().nanos as u32, 289 ).utc_display_hms(), 290 capture.records, 291 capture.size, 292 ) 293 } else { 294 format!( 295 "{:name_width$} | {:chipkind_width$} | {:state_width$} | {:records_width$} |", 296 capture.device_name, 297 Self::chip_kind_to_string(capture.chip_kind.enum_value_or_default()), 298 if capture.valid {Self::capture_state_to_string(capture.state)} else {"detached".to_string()}, 299 capture.records, 300 ) 301 } 302 ); 303 } 304 } 305 chip_kind_to_string(chip_kind: ChipKind) -> String306 pub fn chip_kind_to_string(chip_kind: ChipKind) -> String { 307 match chip_kind { 308 ChipKind::UNSPECIFIED => "UNSPECIFIED".to_string(), 309 ChipKind::BLUETOOTH => "BLUETOOTH".to_string(), 310 ChipKind::WIFI => "WIFI".to_string(), 311 ChipKind::UWB => "UWB".to_string(), 312 ChipKind::BLUETOOTH_BEACON => "BLUETOOTH_BEACON".to_string(), 313 } 314 } 315 filter_captures(captures: &mut Vec<model::Capture>, keys: &[String])316 pub fn filter_captures(captures: &mut Vec<model::Capture>, keys: &[String]) { 317 // Filter out list of captures with matching pattern 318 captures.retain(|capture| { 319 keys.iter().map(|key| key.to_uppercase()).all(|key| { 320 capture.id.to_string().contains(&key) 321 || capture.device_name.to_uppercase().contains(&key) 322 || Self::chip_kind_to_string(capture.chip_kind.enum_value_or_default()) 323 .contains(&key) 324 }) 325 }); 326 } 327 } 328 329 #[cfg(test)] 330 mod tests { 331 use super::*; test_filter_captures_helper(patterns: Vec<String>, expected_captures: Vec<model::Capture>)332 fn test_filter_captures_helper(patterns: Vec<String>, expected_captures: Vec<model::Capture>) { 333 let mut captures = all_test_captures(); 334 Command::filter_captures(&mut captures, &patterns); 335 assert_eq!(captures, expected_captures); 336 } 337 capture_1() -> model::Capture338 fn capture_1() -> model::Capture { 339 model::Capture { 340 id: 4001, 341 chip_kind: ChipKind::BLUETOOTH.into(), 342 device_name: "device 1".to_string(), 343 ..Default::default() 344 } 345 } capture_1_wifi() -> model::Capture346 fn capture_1_wifi() -> model::Capture { 347 model::Capture { 348 id: 4002, 349 chip_kind: ChipKind::WIFI.into(), 350 device_name: "device 1".to_string(), 351 ..Default::default() 352 } 353 } capture_2() -> model::Capture354 fn capture_2() -> model::Capture { 355 model::Capture { 356 id: 4003, 357 chip_kind: ChipKind::BLUETOOTH.into(), 358 device_name: "device 2".to_string(), 359 ..Default::default() 360 } 361 } capture_3() -> model::Capture362 fn capture_3() -> model::Capture { 363 model::Capture { 364 id: 4004, 365 chip_kind: ChipKind::WIFI.into(), 366 device_name: "device 3".to_string(), 367 ..Default::default() 368 } 369 } capture_4_uwb() -> model::Capture370 fn capture_4_uwb() -> model::Capture { 371 model::Capture { 372 id: 4005, 373 chip_kind: ChipKind::UWB.into(), 374 device_name: "device 4".to_string(), 375 ..Default::default() 376 } 377 } all_test_captures() -> Vec<model::Capture>378 fn all_test_captures() -> Vec<model::Capture> { 379 vec![capture_1(), capture_1_wifi(), capture_2(), capture_3(), capture_4_uwb()] 380 } 381 382 #[test] test_no_match()383 fn test_no_match() { 384 test_filter_captures_helper(vec!["test".to_string()], vec![]); 385 } 386 387 #[test] test_all_match()388 fn test_all_match() { 389 test_filter_captures_helper(vec!["device".to_string()], all_test_captures()); 390 } 391 392 #[test] test_match_capture_id()393 fn test_match_capture_id() { 394 test_filter_captures_helper(vec!["4001".to_string()], vec![capture_1()]); 395 test_filter_captures_helper(vec!["03".to_string()], vec![capture_2()]); 396 test_filter_captures_helper(vec!["40".to_string()], all_test_captures()); 397 } 398 399 #[test] test_match_device_name()400 fn test_match_device_name() { 401 test_filter_captures_helper( 402 vec!["device 1".to_string()], 403 vec![capture_1(), capture_1_wifi()], 404 ); 405 test_filter_captures_helper(vec![" 2".to_string()], vec![capture_2()]); 406 } 407 408 #[test] test_match_device_name_case_insensitive()409 fn test_match_device_name_case_insensitive() { 410 test_filter_captures_helper( 411 vec!["DEVICE 1".to_string()], 412 vec![capture_1(), capture_1_wifi()], 413 ); 414 } 415 416 #[test] test_match_wifi()417 fn test_match_wifi() { 418 test_filter_captures_helper(vec!["wifi".to_string()], vec![capture_1_wifi(), capture_3()]); 419 test_filter_captures_helper(vec!["WIFI".to_string()], vec![capture_1_wifi(), capture_3()]); 420 } 421 422 #[test] test_match_uwb()423 fn test_match_uwb() { 424 test_filter_captures_helper(vec!["uwb".to_string()], vec![capture_4_uwb()]); 425 test_filter_captures_helper(vec!["UWB".to_string()], vec![capture_4_uwb()]); 426 } 427 428 #[test] test_match_bt()429 fn test_match_bt() { 430 test_filter_captures_helper(vec!["BLUETOOTH".to_string()], vec![capture_1(), capture_2()]); 431 test_filter_captures_helper(vec!["blue".to_string()], vec![capture_1(), capture_2()]); 432 } 433 434 #[test] test_match_name_and_chip()435 fn test_match_name_and_chip() { 436 test_filter_captures_helper( 437 vec!["device 1".to_string(), "wifi".to_string()], 438 vec![capture_1_wifi()], 439 ); 440 } 441 } 442