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, Command, OnOffState, Pcap}; 18 use frontend_proto::{ 19 common::ChipKind, 20 frontend::{GetDevicesResponse, ListCaptureResponse, VersionResponse}, 21 model::{self, chip::Chip as Chip_oneof_chip, State}, 22 }; 23 use protobuf::Message; 24 25 impl args::Command { 26 /// Format and print the response received from the frontend server for the command print_response(&self, response: &[u8], verbose: bool)27 pub fn print_response(&self, response: &[u8], verbose: bool) { 28 match self { 29 Command::Version => { 30 Self::print_version_response(VersionResponse::parse_from_bytes(response).unwrap()); 31 } 32 Command::Radio(cmd) => { 33 if verbose { 34 println!( 35 "Radio {} is {} for {}", 36 cmd.radio_type, 37 cmd.status, 38 cmd.name.to_owned() 39 ); 40 } 41 } 42 Command::Move(cmd) => { 43 if verbose { 44 println!( 45 "Moved device:{} to x: {:.2}, y: {:.2}, z: {:.2}", 46 cmd.name, 47 cmd.x, 48 cmd.y, 49 cmd.z.unwrap_or_default() 50 ) 51 } 52 } 53 Command::Devices(_) => { 54 Self::print_device_response( 55 GetDevicesResponse::parse_from_bytes(response).unwrap(), 56 verbose, 57 ); 58 } 59 Command::Reset => { 60 if verbose { 61 println!("All devices have been reset."); 62 } 63 } 64 Command::Pcap(Pcap::List(cmd)) => Self::print_list_capture_response( 65 ListCaptureResponse::parse_from_bytes(response).unwrap(), 66 verbose, 67 cmd.patterns.to_owned(), 68 ), 69 Command::Pcap(Pcap::Patch(cmd)) => { 70 if verbose { 71 println!( 72 "Patched Capture state to {}", 73 Self::on_off_state_to_string(cmd.state), 74 ); 75 } 76 } 77 Command::Pcap(Pcap::Get(_)) => { 78 if verbose { 79 println!("Successfully downloaded Pcap."); 80 } 81 } 82 Command::Gui => { 83 unimplemented!("No Grpc Response for Gui Command."); 84 } 85 } 86 } 87 88 /// Helper function to format and print GetDevicesResponse print_device_response(response: GetDevicesResponse, verbose: bool)89 fn print_device_response(response: GetDevicesResponse, verbose: bool) { 90 let pos_prec = 2; 91 let name_width = 16; 92 let state_width = 5; 93 let cnt_width = 9; 94 let chip_indent = 2; 95 let radio_width = 9; 96 if verbose { 97 if response.devices.is_empty() { 98 println!("No attached devices found."); 99 } else { 100 println!("List of attached devices:"); 101 } 102 for device in response.devices { 103 let pos = device.position; 104 println!( 105 "{:name_width$} position: {:.pos_prec$}, {:.pos_prec$}, {:.pos_prec$}", 106 device.name, pos.x, pos.y, pos.z 107 ); 108 for chip in &device.chips { 109 match &chip.chip { 110 Some(Chip_oneof_chip::Bt(bt)) => { 111 if bt.low_energy.is_some() { 112 let ble_chip = &bt.low_energy; 113 println!( 114 "{:chip_indent$}{:radio_width$}{:state_width$}| rx_count: {:cnt_width$?} | tx_count: {:cnt_width$?} | capture: {}", 115 "", 116 "ble:", 117 Self::chip_state_to_string(ble_chip.state.enum_value_or_default()), 118 ble_chip.rx_count, 119 ble_chip.tx_count, 120 Self::capture_state_to_string(chip.capture.enum_value_or_default()) 121 ); 122 } 123 if bt.classic.is_some() { 124 let classic_chip = &bt.classic; 125 println!( 126 "{:chip_indent$}{:radio_width$}{:state_width$}| rx_count: {:cnt_width$?} | tx_count: {:cnt_width$?} | capture: {}", 127 "", 128 "classic:", 129 Self::chip_state_to_string(classic_chip.state.enum_value_or_default()), 130 classic_chip.rx_count, 131 classic_chip.tx_count, 132 Self::capture_state_to_string(chip.capture.enum_value_or_default()) 133 ); 134 } 135 } 136 Some(Chip_oneof_chip::Wifi(wifi_chip)) => { 137 println!( 138 "{:chip_indent$}{:radio_width$}{:state_width$}| rx_count: {:cnt_width$?} | tx_count: {:cnt_width$?} | capture: {}", 139 "", 140 "wifi:", 141 Self::chip_state_to_string(wifi_chip.state.enum_value_or_default()), 142 wifi_chip.rx_count, 143 wifi_chip.tx_count, 144 Self::capture_state_to_string(chip.capture.enum_value_or_default()) 145 ); 146 } 147 Some(Chip_oneof_chip::Uwb(uwb_chip)) => { 148 println!( 149 "{:chip_indent$}{:radio_width$}{:state_width$}| rx_count: {:cnt_width$?} | tx_count: {:cnt_width$?} | capture: {}", 150 "", 151 "uwb:", 152 Self::chip_state_to_string(uwb_chip.state.enum_value_or_default()), 153 uwb_chip.rx_count, 154 uwb_chip.tx_count, 155 Self::capture_state_to_string(chip.capture.enum_value_or_default()) 156 ); 157 } 158 _ => println!("{:chip_indent$}Unknown chip: down ", ""), 159 } 160 } 161 } 162 } else { 163 for device in response.devices { 164 let pos = device.position; 165 print!("{:name_width$} ", device.name,); 166 if pos.x != 0.0 || pos.y != 0.0 || pos.z != 0.0 { 167 print!( 168 "position: {:.pos_prec$}, {:.pos_prec$}, {:.pos_prec$}", 169 pos.x, pos.y, pos.z 170 ); 171 } 172 for chip in &device.chips { 173 match &chip.chip { 174 Some(Chip_oneof_chip::Bt(bt)) => { 175 if bt.low_energy.is_some() { 176 let ble_chip = &bt.low_energy; 177 if ble_chip.state.enum_value_or_default() == State::OFF { 178 print!( 179 "{:chip_indent$}{:radio_width$}{:state_width$}", 180 "", 181 "ble:", 182 Self::chip_state_to_string( 183 ble_chip.state.enum_value_or_default() 184 ), 185 ); 186 } 187 } 188 if bt.classic.is_some() { 189 let classic_chip = &bt.classic; 190 if classic_chip.state.enum_value_or_default() == State::OFF { 191 print!( 192 "{:chip_indent$}{:radio_width$}{:state_width$}", 193 "", 194 "classic:", 195 Self::chip_state_to_string( 196 classic_chip.state.enum_value_or_default() 197 ) 198 ); 199 } 200 } 201 } 202 Some(Chip_oneof_chip::Wifi(wifi_chip)) => { 203 if wifi_chip.state.enum_value_or_default() == State::OFF { 204 print!( 205 "{:chip_indent$}{:radio_width$}{:state_width$}", 206 "", 207 "wifi:", 208 Self::chip_state_to_string( 209 wifi_chip.state.enum_value_or_default() 210 ) 211 ); 212 } 213 } 214 Some(Chip_oneof_chip::Uwb(uwb_chip)) => { 215 if uwb_chip.state.enum_value_or_default() == State::OFF { 216 print!( 217 "{:chip_indent$}{:radio_width$}{:state_width$}", 218 "", 219 "uwb:", 220 Self::chip_state_to_string( 221 uwb_chip.state.enum_value_or_default() 222 ) 223 ); 224 } 225 } 226 _ => {} 227 } 228 if chip.capture.enum_value_or_default() == State::ON { 229 print!("{:chip_indent$}capture: on", ""); 230 } 231 } 232 println!(); 233 } 234 } 235 } 236 237 /// Helper function to convert frontend_proto::model::State to string for output chip_state_to_string(state: State) -> String238 fn chip_state_to_string(state: State) -> String { 239 match state { 240 State::ON => "up".to_string(), 241 State::OFF => "down".to_string(), 242 _ => "unknown".to_string(), 243 } 244 } 245 capture_state_to_string(state: State) -> String246 fn capture_state_to_string(state: State) -> String { 247 match state { 248 State::ON => "on".to_string(), 249 State::OFF => "off".to_string(), 250 _ => "unknown".to_string(), 251 } 252 } 253 on_off_state_to_string(state: OnOffState) -> String254 fn on_off_state_to_string(state: OnOffState) -> String { 255 match state { 256 OnOffState::On => "on".to_string(), 257 OnOffState::Off => "off".to_string(), 258 } 259 } 260 261 /// Helper function to format and print VersionResponse print_version_response(response: VersionResponse)262 fn print_version_response(response: VersionResponse) { 263 println!("Netsim version: {}", response.version); 264 } 265 266 /// Helper function to format and print ListCaptureResponse print_list_capture_response( mut response: ListCaptureResponse, verbose: bool, patterns: Vec<String>, )267 fn print_list_capture_response( 268 mut response: ListCaptureResponse, 269 verbose: bool, 270 patterns: Vec<String>, 271 ) { 272 if response.captures.is_empty() { 273 if verbose { 274 println!("No available Capture found."); 275 } 276 return; 277 } 278 if patterns.is_empty() { 279 println!("List of Captures:"); 280 } else { 281 // Filter out list of captures with matching patterns 282 Self::filter_captures(&mut response.captures, &patterns); 283 if response.captures.is_empty() { 284 if verbose { 285 println!("No available Capture found matching pattern(s) `{:?}`:", patterns); 286 } 287 return; 288 } 289 println!("List of Captures matching pattern(s) `{:?}`:", patterns); 290 } 291 // Create the header row and determine column widths 292 let id_hdr = "ID"; 293 let name_hdr = "Device Name"; 294 let chipkind_hdr = "Chip Kind"; 295 let state_hdr = "State"; 296 let size_hdr = "Size"; 297 let id_width = 4; // ID width of 4 since capture id (=chip_id) starts at 1000 298 let state_width = 7; // State width of 7 for 'unknown' 299 let chipkind_width = 11; // ChipKind width 11 for 'UNSPECIFIED' 300 let name_width = max( 301 (response.captures.iter().max_by_key(|x| x.device_name.len())) 302 .unwrap_or_default() 303 .device_name 304 .len(), 305 name_hdr.len(), 306 ); 307 let size_width = max( 308 (response.captures.iter().max_by_key(|x| x.size)) 309 .unwrap_or_default() 310 .size 311 .to_string() 312 .len(), 313 size_hdr.len(), 314 ); 315 // Print header for capture list 316 println!( 317 "{}", 318 if verbose { 319 format!("{:id_width$} | {:name_width$} | {:chipkind_width$} | {:state_width$} | {:size_width$} |", 320 id_hdr, 321 name_hdr, 322 chipkind_hdr, 323 state_hdr, 324 size_hdr, 325 ) 326 } else { 327 format!( 328 "{:name_width$} | {:chipkind_width$} | {:state_width$} | {:size_width$} |", 329 name_hdr, chipkind_hdr, state_hdr, size_hdr, 330 ) 331 } 332 ); 333 // Print information of each Capture 334 for capture in &response.captures { 335 println!( 336 "{}", 337 if verbose { 338 format!("{:id_width$} | {:name_width$} | {:chipkind_width$} | {:state_width$} | {:size_width$} |", 339 capture.id.to_string(), 340 capture.device_name, 341 Self::chip_kind_to_string(capture.chip_kind.enum_value_or_default()), 342 Self::capture_state_to_string(capture.state.enum_value_or_default()), 343 capture.size, 344 ) 345 } else { 346 format!( 347 "{:name_width$} | {:chipkind_width$} | {:state_width$} | {:size_width$} |", 348 capture.device_name, 349 Self::chip_kind_to_string(capture.chip_kind.enum_value_or_default()), 350 Self::capture_state_to_string(capture.state.enum_value_or_default()), 351 capture.size, 352 ) 353 } 354 ); 355 } 356 } 357 chip_kind_to_string(chip_kind: ChipKind) -> String358 pub fn chip_kind_to_string(chip_kind: ChipKind) -> String { 359 match chip_kind { 360 ChipKind::UNSPECIFIED => "UNSPECIFIED".to_string(), 361 ChipKind::BLUETOOTH => "BLUETOOTH".to_string(), 362 ChipKind::WIFI => "WIFI".to_string(), 363 ChipKind::UWB => "UWB".to_string(), 364 } 365 } 366 filter_captures(captures: &mut Vec<model::Capture>, keys: &[String])367 pub fn filter_captures(captures: &mut Vec<model::Capture>, keys: &[String]) { 368 // Filter out list of captures with matching pattern 369 captures.retain(|capture| { 370 keys.iter().map(|key| key.to_uppercase()).all(|key| { 371 capture.id.to_string().contains(&key) 372 || capture.device_name.to_uppercase().contains(&key) 373 || Self::chip_kind_to_string(capture.chip_kind.enum_value_or_default()) 374 .contains(&key) 375 }) 376 }); 377 } 378 } 379 380 #[cfg(test)] 381 mod tests { 382 use super::*; test_filter_captures_helper(patterns: Vec<String>, expected_captures: Vec<model::Capture>)383 fn test_filter_captures_helper(patterns: Vec<String>, expected_captures: Vec<model::Capture>) { 384 let mut captures = all_test_captures(); 385 Command::filter_captures(&mut captures, &patterns); 386 assert_eq!(captures, expected_captures); 387 } 388 capture_1() -> model::Capture389 fn capture_1() -> model::Capture { 390 model::Capture { 391 id: 4001, 392 chip_kind: ChipKind::BLUETOOTH.into(), 393 device_name: "device 1".to_string(), 394 ..Default::default() 395 } 396 } capture_1_wifi() -> model::Capture397 fn capture_1_wifi() -> model::Capture { 398 model::Capture { 399 id: 4002, 400 chip_kind: ChipKind::WIFI.into(), 401 device_name: "device 1".to_string(), 402 ..Default::default() 403 } 404 } capture_2() -> model::Capture405 fn capture_2() -> model::Capture { 406 model::Capture { 407 id: 4003, 408 chip_kind: ChipKind::BLUETOOTH.into(), 409 device_name: "device 2".to_string(), 410 ..Default::default() 411 } 412 } capture_3() -> model::Capture413 fn capture_3() -> model::Capture { 414 model::Capture { 415 id: 4004, 416 chip_kind: ChipKind::WIFI.into(), 417 device_name: "device 3".to_string(), 418 ..Default::default() 419 } 420 } capture_4_uwb() -> model::Capture421 fn capture_4_uwb() -> model::Capture { 422 model::Capture { 423 id: 4005, 424 chip_kind: ChipKind::UWB.into(), 425 device_name: "device 4".to_string(), 426 ..Default::default() 427 } 428 } all_test_captures() -> Vec<model::Capture>429 fn all_test_captures() -> Vec<model::Capture> { 430 vec![capture_1(), capture_1_wifi(), capture_2(), capture_3(), capture_4_uwb()] 431 } 432 433 #[test] test_no_match()434 fn test_no_match() { 435 test_filter_captures_helper(vec!["test".to_string()], vec![]); 436 } 437 438 #[test] test_all_match()439 fn test_all_match() { 440 test_filter_captures_helper(vec!["device".to_string()], all_test_captures()); 441 } 442 443 #[test] test_match_capture_id()444 fn test_match_capture_id() { 445 test_filter_captures_helper(vec!["4001".to_string()], vec![capture_1()]); 446 test_filter_captures_helper(vec!["03".to_string()], vec![capture_2()]); 447 test_filter_captures_helper(vec!["40".to_string()], all_test_captures()); 448 } 449 450 #[test] test_match_device_name()451 fn test_match_device_name() { 452 test_filter_captures_helper( 453 vec!["device 1".to_string()], 454 vec![capture_1(), capture_1_wifi()], 455 ); 456 test_filter_captures_helper(vec![" 2".to_string()], vec![capture_2()]); 457 } 458 459 #[test] test_match_device_name_case_insensitive()460 fn test_match_device_name_case_insensitive() { 461 test_filter_captures_helper( 462 vec!["DEVICE 1".to_string()], 463 vec![capture_1(), capture_1_wifi()], 464 ); 465 } 466 467 #[test] test_match_wifi()468 fn test_match_wifi() { 469 test_filter_captures_helper(vec!["wifi".to_string()], vec![capture_1_wifi(), capture_3()]); 470 test_filter_captures_helper(vec!["WIFI".to_string()], vec![capture_1_wifi(), capture_3()]); 471 } 472 473 #[test] test_match_uwb()474 fn test_match_uwb() { 475 test_filter_captures_helper(vec!["uwb".to_string()], vec![capture_4_uwb()]); 476 test_filter_captures_helper(vec!["UWB".to_string()], vec![capture_4_uwb()]); 477 } 478 479 #[test] test_match_bt()480 fn test_match_bt() { 481 test_filter_captures_helper(vec!["BLUETOOTH".to_string()], vec![capture_1(), capture_2()]); 482 test_filter_captures_helper(vec!["blue".to_string()], vec![capture_1(), capture_2()]); 483 } 484 485 #[test] test_match_name_and_chip()486 fn test_match_name_and_chip() { 487 test_filter_captures_helper( 488 vec!["device 1".to_string(), "wifi".to_string()], 489 vec![capture_1_wifi()], 490 ); 491 } 492 } 493