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