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