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 crate::ffi::frontend_client_ffi::{FrontendClient, GrpcMethod};
16 use clap::builder::{PossibleValue, TypedValueParser};
17 use clap::{Args, Parser, Subcommand, ValueEnum};
18 use hex::{decode as hex_to_bytes, FromHexError};
19 use netsim_common::util::time_display::TimeDisplay;
20 use netsim_proto::common::ChipKind;
21 use netsim_proto::frontend;
22 use netsim_proto::frontend::patch_capture_request::PatchCapture as PatchCaptureProto;
23 use netsim_proto::model::chip::ble_beacon::advertise_settings::{
24     AdvertiseMode as AdvertiseModeProto, AdvertiseTxPower as AdvertiseTxPowerProto,
25     Interval as IntervalProto, Tx_power as TxPowerProto,
26 };
27 use netsim_proto::model::chip::ble_beacon::{
28     AdvertiseData as AdvertiseDataProto, AdvertiseSettings as AdvertiseSettingsProto,
29 };
30 use netsim_proto::model::chip::{
31     BleBeacon as Chip_Ble_Beacon, Bluetooth as Chip_Bluetooth, Chip as Chip_Type,
32     Radio as Chip_Radio,
33 };
34 use netsim_proto::model::{
35     self, chip_create, Chip, ChipCreate as ChipCreateProto, Device,
36     DeviceCreate as DeviceCreateProto, Position,
37 };
38 use protobuf::{Message, MessageField};
39 use std::fmt;
40 use std::iter;
41 use std::str::FromStr;
42 use tracing::error;
43 
44 pub type BinaryProtobuf = Vec<u8>;
45 
46 #[derive(Debug, Parser)]
47 pub struct NetsimArgs {
48     #[command(subcommand)]
49     pub command: Command,
50     /// Set verbose mode
51     #[arg(short, long)]
52     pub verbose: bool,
53     /// Set custom grpc port
54     #[arg(short, long)]
55     pub port: Option<i32>,
56     /// Set netsimd instance number to connect
57     #[arg(short, long)]
58     pub instance: Option<u16>,
59     /// Set vsock cid:port pair
60     #[arg(long)]
61     pub vsock: Option<String>,
62 }
63 
64 #[derive(Debug, Subcommand)]
65 #[command(infer_subcommands = true)]
66 pub enum Command {
67     /// Print Netsim version information
68     Version,
69     /// Control the radio state of a device
70     Radio(Radio),
71     /// Set the device location
72     Move(Move),
73     /// Display device(s) information
74     Devices(Devices),
75     /// Reset Netsim device scene
76     Reset,
77     /// Open netsim Web UI
78     Gui,
79     /// Control the packet capture functionalities with commands: list, patch, get
80     #[command(subcommand, visible_alias("pcap"))]
81     Capture(Capture),
82     /// Opens netsim artifacts directory (log, pcaps)
83     Artifact,
84     /// A chip that sends advertisements at a set interval
85     #[command(subcommand)]
86     Beacon(Beacon),
87     /// Open Bumble Hive Web Page
88     Bumble,
89 }
90 
91 impl Command {
92     /// Return the generated request protobuf as a byte vector
93     /// The parsed command parameters are used to construct the request protobuf which is
94     /// returned as a byte vector that can be sent to the server.
get_request_bytes(&self) -> BinaryProtobuf95     pub fn get_request_bytes(&self) -> BinaryProtobuf {
96         match self {
97             Command::Version => Vec::new(),
98             Command::Radio(cmd) => {
99                 let mut chip = Chip { ..Default::default() };
100                 let chip_state = match cmd.status {
101                     UpDownStatus::Up => true,
102                     UpDownStatus::Down => false,
103                 };
104                 if cmd.radio_type == RadioType::Wifi {
105                     let mut wifi_chip = Chip_Radio::new();
106                     wifi_chip.state = chip_state.into();
107                     chip.set_wifi(wifi_chip);
108                     chip.kind = ChipKind::WIFI.into();
109                 } else if cmd.radio_type == RadioType::Uwb {
110                     let mut uwb_chip = Chip_Radio::new();
111                     uwb_chip.state = chip_state.into();
112                     chip.set_uwb(uwb_chip);
113                     chip.kind = ChipKind::UWB.into();
114                 } else {
115                     let mut bt_chip = Chip_Bluetooth::new();
116                     let mut bt_chip_radio = Chip_Radio::new();
117                     bt_chip_radio.state = chip_state.into();
118                     if cmd.radio_type == RadioType::Ble {
119                         bt_chip.low_energy = Some(bt_chip_radio).into();
120                     } else {
121                         bt_chip.classic = Some(bt_chip_radio).into();
122                     }
123                     chip.kind = ChipKind::BLUETOOTH.into();
124                     chip.set_bt(bt_chip);
125                 }
126                 let mut result = frontend::PatchDeviceRequest::new();
127                 let mut device = Device::new();
128                 cmd.name.clone_into(&mut device.name);
129                 device.chips.push(chip);
130                 result.device = Some(device).into();
131                 result.write_to_bytes().unwrap()
132             }
133             Command::Move(cmd) => {
134                 let mut result = frontend::PatchDeviceRequest::new();
135                 let mut device = Device::new();
136                 let position = Position {
137                     x: cmd.x,
138                     y: cmd.y,
139                     z: cmd.z.unwrap_or_default(),
140                     ..Default::default()
141                 };
142                 cmd.name.clone_into(&mut device.name);
143                 device.position = Some(position).into();
144                 result.device = Some(device).into();
145                 result.write_to_bytes().unwrap()
146             }
147             Command::Devices(_) => Vec::new(),
148             Command::Reset => Vec::new(),
149             Command::Gui => {
150                 unimplemented!("get_request_bytes is not implemented for Gui Command.");
151             }
152             Command::Capture(cmd) => match cmd {
153                 Capture::List(_) => Vec::new(),
154                 Capture::Get(_) => {
155                     unimplemented!("get_request_bytes not implemented for Capture Get command. Use get_requests instead.")
156                 }
157                 Capture::Patch(_) => {
158                     unimplemented!("get_request_bytes not implemented for Capture Patch command. Use get_requests instead.")
159                 }
160             },
161             Command::Artifact => {
162                 unimplemented!("get_request_bytes is not implemented for Artifact Command.");
163             }
164             Command::Beacon(action) => match action {
165                 Beacon::Create(kind) => match kind {
166                     BeaconCreate::Ble(args) => {
167                         let device = MessageField::some(DeviceCreateProto {
168                             name: args.device_name.clone().unwrap_or_default(),
169                             chips: vec![ChipCreateProto {
170                                 name: args.chip_name.clone().unwrap_or_default(),
171                                 kind: ChipKind::BLUETOOTH_BEACON.into(),
172                                 chip: Some(chip_create::Chip::BleBeacon(
173                                     chip_create::BleBeaconCreate {
174                                         address: args.address.clone().unwrap_or_default(),
175                                         settings: MessageField::some((&args.settings).into()),
176                                         adv_data: MessageField::some((&args.advertise_data).into()),
177                                         scan_response: MessageField::some(
178                                             (&args.scan_response_data).into(),
179                                         ),
180                                         ..Default::default()
181                                     },
182                                 )),
183                                 ..Default::default()
184                             }],
185                             ..Default::default()
186                         });
187 
188                         let result = frontend::CreateDeviceRequest { device, ..Default::default() };
189                         result.write_to_bytes().unwrap()
190                     }
191                 },
192                 Beacon::Patch(kind) => match kind {
193                     BeaconPatch::Ble(args) => {
194                         let device = MessageField::some(Device {
195                             name: args.device_name.clone(),
196                             chips: vec![Chip {
197                                 name: args.chip_name.clone(),
198                                 kind: ChipKind::BLUETOOTH_BEACON.into(),
199                                 chip: Some(Chip_Type::BleBeacon(Chip_Ble_Beacon {
200                                     bt: MessageField::some(Chip_Bluetooth::new()),
201                                     address: args.address.clone().unwrap_or_default(),
202                                     settings: MessageField::some((&args.settings).into()),
203                                     adv_data: MessageField::some((&args.advertise_data).into()),
204                                     scan_response: MessageField::some(
205                                         (&args.scan_response_data).into(),
206                                     ),
207                                     ..Default::default()
208                                 })),
209                                 ..Default::default()
210                             }],
211                             ..Default::default()
212                         });
213 
214                         let result = frontend::PatchDeviceRequest { device, ..Default::default() };
215                         result.write_to_bytes().unwrap()
216                     }
217                 },
218                 Beacon::Remove(_) => Vec::new(),
219             },
220             Command::Bumble => {
221                 unimplemented!("get_request_bytes is not implemented for Bumble Command.");
222             }
223         }
224     }
225 
226     /// Create and return the request protobuf(s) for the command.
227     /// In the case of a command with pattern argument(s) there may be multiple gRPC requests.
228     /// The parsed command parameters are used to construct the request protobuf.
229     /// The client is used to send gRPC call(s) to retrieve information needed for request protobufs.
get_requests(&mut self, client: &cxx::UniquePtr<FrontendClient>) -> Vec<BinaryProtobuf>230     pub fn get_requests(&mut self, client: &cxx::UniquePtr<FrontendClient>) -> Vec<BinaryProtobuf> {
231         match self {
232             Command::Capture(Capture::Patch(cmd)) => {
233                 let mut reqs = Vec::new();
234                 let filtered_captures = Self::get_filtered_captures(client, &cmd.patterns);
235                 // Create a request for each capture
236                 for capture in &filtered_captures {
237                     let mut result = frontend::PatchCaptureRequest::new();
238                     result.id = capture.id;
239                     let capture_state = match cmd.state {
240                         OnOffState::On => true,
241                         OnOffState::Off => false,
242                     };
243                     let mut patch_capture = PatchCaptureProto::new();
244                     patch_capture.state = capture_state.into();
245                     result.patch = Some(patch_capture).into();
246                     reqs.push(result.write_to_bytes().unwrap())
247                 }
248                 reqs
249             }
250             Command::Capture(Capture::Get(cmd)) => {
251                 let mut reqs = Vec::new();
252                 let filtered_captures = Self::get_filtered_captures(client, &cmd.patterns);
253                 // Create a request for each capture
254                 for capture in &filtered_captures {
255                     let mut result = frontend::GetCaptureRequest::new();
256                     result.id = capture.id;
257                     reqs.push(result.write_to_bytes().unwrap());
258                     let time_display = TimeDisplay::new(
259                         capture.timestamp.get_or_default().seconds,
260                         capture.timestamp.get_or_default().nanos as u32,
261                     );
262                     let file_extension = "pcap";
263                     cmd.filenames.push(format!(
264                         "netsim-{:?}-{}-{}-{}.{}",
265                         capture.id,
266                         capture.device_name.to_owned().replace(' ', "_"),
267                         Self::chip_kind_to_string(capture.chip_kind.enum_value_or_default()),
268                         time_display.utc_display(),
269                         file_extension
270                     ));
271                 }
272                 reqs
273             }
274             _ => {
275                 unimplemented!(
276                     "get_requests not implemented for this command. Use get_request_bytes instead."
277                 )
278             }
279         }
280     }
281 
get_filtered_captures( client: &cxx::UniquePtr<FrontendClient>, patterns: &[String], ) -> Vec<model::Capture>282     fn get_filtered_captures(
283         client: &cxx::UniquePtr<FrontendClient>,
284         patterns: &[String],
285     ) -> Vec<model::Capture> {
286         // Get list of captures
287         let result = client.send_grpc(&GrpcMethod::ListCapture, &Vec::new());
288         if !result.is_ok() {
289             error!("ListCapture Grpc call error: {}", result.err());
290             return Vec::new();
291         }
292         let mut response =
293             frontend::ListCaptureResponse::parse_from_bytes(result.byte_vec().as_slice()).unwrap();
294         if !patterns.is_empty() {
295             // Filter out list of captures with matching patterns
296             Self::filter_captures(&mut response.captures, patterns)
297         }
298         response.captures
299     }
300 }
301 
302 #[derive(Debug, Args)]
303 pub struct Radio {
304     /// Radio type
305     #[arg(value_enum, ignore_case = true)]
306     pub radio_type: RadioType,
307     /// Radio status
308     #[arg(value_enum, ignore_case = true)]
309     pub status: UpDownStatus,
310     /// Device name
311     pub name: String,
312 }
313 
314 #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
315 pub enum RadioType {
316     Ble,
317     Classic,
318     Wifi,
319     Uwb,
320 }
321 
322 impl fmt::Display for RadioType {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result323     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
324         write!(f, "{:?}", self)
325     }
326 }
327 
328 #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
329 pub enum UpDownStatus {
330     Up,
331     Down,
332 }
333 
334 impl fmt::Display for UpDownStatus {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result335     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
336         write!(f, "{:?}", self)
337     }
338 }
339 
340 #[derive(Debug, Args)]
341 pub struct Move {
342     /// Device name
343     pub name: String,
344     /// x position of device
345     pub x: f32,
346     /// y position of device
347     pub y: f32,
348     /// Optional z position of device
349     pub z: Option<f32>,
350 }
351 
352 #[derive(Debug, Args)]
353 pub struct Devices {
354     /// Continuously print device(s) information every second
355     #[arg(short, long)]
356     pub continuous: bool,
357 }
358 
359 #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
360 pub enum OnOffState {
361     On,
362     Off,
363 }
364 
365 #[derive(Debug, Subcommand)]
366 pub enum Beacon {
367     /// Create a beacon chip
368     #[command(subcommand)]
369     Create(BeaconCreate),
370     /// Modify a beacon chip
371     #[command(subcommand)]
372     Patch(BeaconPatch),
373     /// Remove a beacon chip
374     Remove(BeaconRemove),
375 }
376 
377 #[derive(Debug, Subcommand)]
378 pub enum BeaconCreate {
379     /// Create a Bluetooth low-energy beacon chip
380     Ble(BeaconCreateBle),
381 }
382 
383 #[derive(Debug, Args)]
384 pub struct BeaconCreateBle {
385     /// Name of the device to create
386     pub device_name: Option<String>,
387     /// Name of the beacon chip to create within the new device. May only be specified if device_name is specified
388     pub chip_name: Option<String>,
389     /// Bluetooth address of the beacon. Must be a 6-byte hexadecimal string with each byte separated by a colon. Will be generated if not provided
390     #[arg(long)]
391     pub address: Option<String>,
392     #[command(flatten)]
393     pub settings: BeaconBleSettings,
394     #[command(flatten)]
395     pub advertise_data: BeaconBleAdvertiseData,
396     #[command(flatten)]
397     pub scan_response_data: BeaconBleScanResponseData,
398 }
399 
400 #[derive(Debug, Subcommand)]
401 pub enum BeaconPatch {
402     /// Modify a Bluetooth low-energy beacon chip
403     Ble(BeaconPatchBle),
404 }
405 
406 #[derive(Debug, Args)]
407 pub struct BeaconPatchBle {
408     /// Name of the device that contains the chip
409     pub device_name: String,
410     /// Name of the beacon chip to modify
411     pub chip_name: String,
412     /// Bluetooth address of the beacon. Must be a 6-byte hexadecimal string with each byte separated by a colon
413     #[arg(long)]
414     pub address: Option<String>,
415     #[command(flatten)]
416     pub settings: BeaconBleSettings,
417     #[command(flatten)]
418     pub advertise_data: BeaconBleAdvertiseData,
419     #[command(flatten)]
420     pub scan_response_data: BeaconBleScanResponseData,
421 }
422 
423 #[derive(Debug, Args)]
424 pub struct BeaconRemove {
425     /// Name of the device to remove
426     pub device_name: String,
427     /// Name of the beacon chip to remove. Can be omitted if the device has exactly 1 chip
428     pub chip_name: Option<String>,
429 }
430 
431 #[derive(Debug, Args)]
432 pub struct BeaconBleAdvertiseData {
433     /// Whether the device name should be included in the advertise packet
434     #[arg(long, required = false)]
435     pub include_device_name: bool,
436     /// Whether the transmission power level should be included in the advertise packet
437     #[arg(long, required = false)]
438     pub include_tx_power_level: bool,
439     /// Manufacturer-specific data given as bytes in hexadecimal
440     #[arg(long)]
441     pub manufacturer_data: Option<ParsableBytes>,
442 }
443 
444 #[derive(Debug, Clone)]
445 pub struct ParsableBytes(Vec<u8>);
446 
447 impl ParsableBytes {
unwrap(self) -> Vec<u8>448     fn unwrap(self) -> Vec<u8> {
449         self.0
450     }
451 }
452 
453 impl FromStr for ParsableBytes {
454     type Err = FromHexError;
from_str(s: &str) -> Result<Self, Self::Err>455     fn from_str(s: &str) -> Result<Self, Self::Err> {
456         hex_to_bytes(s.strip_prefix("0x").unwrap_or(s)).map(ParsableBytes)
457     }
458 }
459 
460 #[derive(Debug, Args)]
461 pub struct BeaconBleScanResponseData {
462     /// Whether the device name should be included in the scan response packet
463     #[arg(long, required = false)]
464     pub scan_response_include_device_name: bool,
465     /// Whether the transmission power level should be included in the scan response packet
466     #[arg(long, required = false)]
467     pub scan_response_include_tx_power_level: bool,
468     /// Manufacturer-specific data to include in the scan response packet given as bytes in hexadecimal
469     #[arg(long, value_name = "MANUFACTURER_DATA")]
470     pub scan_response_manufacturer_data: Option<ParsableBytes>,
471 }
472 
473 #[derive(Debug, Args)]
474 pub struct BeaconBleSettings {
475     /// Set advertise mode to control the advertising latency
476     #[arg(long, value_parser = IntervalParser)]
477     pub advertise_mode: Option<Interval>,
478     /// Set advertise TX power level to control the beacon's transmission power
479     #[arg(long, value_parser = TxPowerParser, allow_hyphen_values = true)]
480     pub tx_power_level: Option<TxPower>,
481     /// Set whether the beacon will respond to scan requests
482     #[arg(long)]
483     pub scannable: bool,
484     /// Limit advertising to an amount of time given in milliseconds
485     #[arg(long, value_name = "MS")]
486     pub timeout: Option<u64>,
487 }
488 
489 #[derive(Clone, Debug)]
490 pub enum Interval {
491     Mode(AdvertiseMode),
492     Milliseconds(u64),
493 }
494 
495 #[derive(Clone)]
496 struct IntervalParser;
497 
498 impl TypedValueParser for IntervalParser {
499     type Value = Interval;
500 
parse_ref( &self, cmd: &clap::Command, arg: Option<&clap::Arg>, value: &std::ffi::OsStr, ) -> Result<Self::Value, clap::Error>501     fn parse_ref(
502         &self,
503         cmd: &clap::Command,
504         arg: Option<&clap::Arg>,
505         value: &std::ffi::OsStr,
506     ) -> Result<Self::Value, clap::Error> {
507         let millis_parser = clap::value_parser!(u64);
508         let mode_parser = clap::value_parser!(AdvertiseMode);
509 
510         mode_parser
511             .parse_ref(cmd, arg, value)
512             .map(Self::Value::Mode)
513             .or(millis_parser.parse_ref(cmd, arg, value).map(Self::Value::Milliseconds))
514     }
515 
possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>>516     fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
517         Some(Box::new(
518             AdvertiseMode::value_variants().iter().map(|v| v.to_possible_value().unwrap()).chain(
519                 iter::once(
520                     PossibleValue::new("<MS>").help("An exact advertise interval in milliseconds"),
521                 ),
522             ),
523         ))
524     }
525 }
526 
527 #[derive(Clone, Debug)]
528 pub enum TxPower {
529     Level(TxPowerLevel),
530     Dbm(i8),
531 }
532 
533 #[derive(Clone)]
534 struct TxPowerParser;
535 
536 impl TypedValueParser for TxPowerParser {
537     type Value = TxPower;
538 
parse_ref( &self, cmd: &clap::Command, arg: Option<&clap::Arg>, value: &std::ffi::OsStr, ) -> Result<Self::Value, clap::Error>539     fn parse_ref(
540         &self,
541         cmd: &clap::Command,
542         arg: Option<&clap::Arg>,
543         value: &std::ffi::OsStr,
544     ) -> Result<Self::Value, clap::Error> {
545         let dbm_parser = clap::value_parser!(i8);
546         let level_parser = clap::value_parser!(TxPowerLevel);
547 
548         level_parser
549             .parse_ref(cmd, arg, value)
550             .map(Self::Value::Level)
551             .or(dbm_parser.parse_ref(cmd, arg, value).map(Self::Value::Dbm))
552     }
553 
possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>>554     fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
555         Some(Box::new(
556             TxPowerLevel::value_variants().iter().map(|v| v.to_possible_value().unwrap()).chain(
557                 iter::once(
558                     PossibleValue::new("<DBM>").help("An exact transmit power level in dBm"),
559                 ),
560             ),
561         ))
562     }
563 }
564 
565 #[derive(Debug, Clone, ValueEnum)]
566 pub enum AdvertiseMode {
567     /// Lowest power consumption, preferred advertising mode
568     LowPower,
569     /// Balanced between advertising frequency and power consumption
570     Balanced,
571     /// Highest power consumption
572     LowLatency,
573 }
574 
575 #[derive(Debug, Clone, ValueEnum)]
576 pub enum TxPowerLevel {
577     /// Lowest transmission power level
578     UltraLow,
579     /// Low transmission power level
580     Low,
581     /// Medium transmission power level
582     Medium,
583     /// High transmission power level
584     High,
585 }
586 
587 #[derive(Debug, Subcommand)]
588 pub enum Capture {
589     /// List currently available Captures (packet captures)
590     List(ListCapture),
591     /// Patch a Capture source to turn packet capture on/off
592     Patch(PatchCapture),
593     /// Download the packet capture content
594     Get(GetCapture),
595 }
596 
597 #[derive(Debug, Args)]
598 pub struct ListCapture {
599     /// Optional strings of pattern for captures to list. Possible filter fields include Capture ID, Device Name, and Chip Kind
600     pub patterns: Vec<String>,
601     /// Continuously print Capture information every second
602     #[arg(short, long)]
603     pub continuous: bool,
604 }
605 
606 #[derive(Debug, Args)]
607 pub struct PatchCapture {
608     /// Packet capture state
609     #[arg(value_enum, ignore_case = true)]
610     pub state: OnOffState,
611     /// Optional strings of pattern for captures to patch. Possible filter fields include Capture ID, Device Name, and Chip Kind
612     pub patterns: Vec<String>,
613 }
614 
615 #[derive(Debug, Args)]
616 pub struct GetCapture {
617     /// Optional strings of pattern for captures to get. Possible filter fields include Capture ID, Device Name, and Chip Kind
618     pub patterns: Vec<String>,
619     /// Directory to store downloaded capture file(s)
620     #[arg(short = 'o', long)]
621     pub location: Option<String>,
622     #[arg(skip)]
623     pub filenames: Vec<String>,
624     #[arg(skip)]
625     pub current_file: String,
626 }
627 
628 impl From<&BeaconBleSettings> for AdvertiseSettingsProto {
from(value: &BeaconBleSettings) -> Self629     fn from(value: &BeaconBleSettings) -> Self {
630         AdvertiseSettingsProto {
631             interval: value.advertise_mode.as_ref().map(IntervalProto::from),
632             tx_power: value.tx_power_level.as_ref().map(TxPowerProto::from),
633             scannable: value.scannable,
634             timeout: value.timeout.unwrap_or_default(),
635             ..Default::default()
636         }
637     }
638 }
639 
640 impl From<&Interval> for IntervalProto {
from(value: &Interval) -> Self641     fn from(value: &Interval) -> Self {
642         match value {
643             Interval::Mode(mode) => IntervalProto::AdvertiseMode(
644                 match mode {
645                     AdvertiseMode::LowPower => AdvertiseModeProto::LOW_POWER,
646                     AdvertiseMode::Balanced => AdvertiseModeProto::BALANCED,
647                     AdvertiseMode::LowLatency => AdvertiseModeProto::LOW_LATENCY,
648                 }
649                 .into(),
650             ),
651             Interval::Milliseconds(ms) => IntervalProto::Milliseconds(*ms),
652         }
653     }
654 }
655 
656 impl From<&TxPower> for TxPowerProto {
from(value: &TxPower) -> Self657     fn from(value: &TxPower) -> Self {
658         match value {
659             TxPower::Level(level) => TxPowerProto::TxPowerLevel(
660                 match level {
661                     TxPowerLevel::UltraLow => AdvertiseTxPowerProto::ULTRA_LOW,
662                     TxPowerLevel::Low => AdvertiseTxPowerProto::LOW,
663                     TxPowerLevel::Medium => AdvertiseTxPowerProto::MEDIUM,
664                     TxPowerLevel::High => AdvertiseTxPowerProto::HIGH,
665                 }
666                 .into(),
667             ),
668             TxPower::Dbm(dbm) => TxPowerProto::Dbm((*dbm).into()),
669         }
670     }
671 }
672 
673 impl From<&BeaconBleAdvertiseData> for AdvertiseDataProto {
from(value: &BeaconBleAdvertiseData) -> Self674     fn from(value: &BeaconBleAdvertiseData) -> Self {
675         AdvertiseDataProto {
676             include_device_name: value.include_device_name,
677             include_tx_power_level: value.include_tx_power_level,
678             manufacturer_data: value
679                 .manufacturer_data
680                 .clone()
681                 .map(ParsableBytes::unwrap)
682                 .unwrap_or_default(),
683             ..Default::default()
684         }
685     }
686 }
687 
688 impl From<&BeaconBleScanResponseData> for AdvertiseDataProto {
from(value: &BeaconBleScanResponseData) -> Self689     fn from(value: &BeaconBleScanResponseData) -> Self {
690         AdvertiseDataProto {
691             include_device_name: value.scan_response_include_device_name,
692             include_tx_power_level: value.scan_response_include_tx_power_level,
693             manufacturer_data: value
694                 .scan_response_manufacturer_data
695                 .clone()
696                 .map(ParsableBytes::unwrap)
697                 .unwrap_or_default(),
698             ..Default::default()
699         }
700     }
701 }
702 
703 #[cfg(test)]
704 mod tests {
705     use super::*;
706 
707     #[test]
test_hex_parser_succeeds()708     fn test_hex_parser_succeeds() {
709         let hex = ParsableBytes::from_str("beef1234");
710         assert!(hex.is_ok(), "{}", hex.unwrap_err());
711         let hex = hex.unwrap().unwrap();
712 
713         assert_eq!(vec![0xbeu8, 0xef, 0x12, 0x34], hex);
714     }
715 
716     #[test]
test_hex_parser_prefix_succeeds()717     fn test_hex_parser_prefix_succeeds() {
718         let hex = ParsableBytes::from_str("0xabcd");
719         assert!(hex.is_ok(), "{}", hex.unwrap_err());
720         let hex = hex.unwrap().unwrap();
721 
722         assert_eq!(vec![0xabu8, 0xcd], hex);
723     }
724 
725     #[test]
test_hex_parser_empty_str_succeeds()726     fn test_hex_parser_empty_str_succeeds() {
727         let hex = ParsableBytes::from_str("");
728         assert!(hex.is_ok(), "{}", hex.unwrap_err());
729         let hex = hex.unwrap().unwrap();
730 
731         assert_eq!(Vec::<u8>::new(), hex);
732     }
733 
734     #[test]
test_hex_parser_bad_digit_fails()735     fn test_hex_parser_bad_digit_fails() {
736         assert!(ParsableBytes::from_str("0xabcdefg").is_err());
737     }
738 }
739