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