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