• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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