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