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 anyhow::Result; 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_proto::model::chip::ble_beacon::advertise_settings::{ 20 AdvertiseMode as AdvertiseModeProto, AdvertiseTxPower as AdvertiseTxPowerProto, 21 Interval as IntervalProto, Tx_power as TxPowerProto, 22 }; 23 use netsim_proto::model::chip::ble_beacon::{ 24 AdvertiseData as AdvertiseDataProto, AdvertiseSettings as AdvertiseSettingsProto, 25 }; 26 27 use std::fmt; 28 use std::iter; 29 use std::str::FromStr; 30 31 #[derive(Debug, Parser)] 32 pub struct NetsimArgs { 33 #[command(subcommand)] 34 pub command: Command, 35 /// Set verbose mode 36 #[arg(short, long)] 37 pub verbose: bool, 38 /// Set custom grpc port 39 #[arg(short, long)] 40 pub port: Option<i32>, 41 /// Set netsimd instance number to connect 42 #[arg(short, long)] 43 pub instance: Option<u16>, 44 /// Set vsock cid:port pair 45 #[arg(long)] 46 pub vsock: Option<String>, 47 } 48 49 #[derive(Debug, Subcommand, PartialEq)] 50 #[command(infer_subcommands = true)] 51 pub enum Command { 52 /// Print Netsim version information 53 Version, 54 /// Control the radio state of a device 55 Radio(Radio), 56 /// Set the device location 57 Move(Move), 58 /// Display device(s) information 59 Devices(Devices), 60 /// Reset Netsim device scene 61 Reset, 62 /// Open netsim Web UI 63 Gui, 64 /// Control the packet capture functionalities with commands: list, patch, get 65 #[command(subcommand, visible_alias("pcap"))] 66 Capture(Capture), 67 /// Opens netsim artifacts directory (log, pcaps) 68 Artifact, 69 /// A chip that sends advertisements at a set interval 70 #[command(subcommand)] 71 Beacon(Beacon), 72 /// Open Bumble Hive Web Page 73 Bumble, 74 } 75 76 #[derive(Debug, Args, PartialEq)] 77 pub struct Radio { 78 /// Radio type 79 #[arg(value_enum, ignore_case = true)] 80 pub radio_type: RadioType, 81 /// Radio status 82 #[arg(value_enum, ignore_case = true)] 83 pub status: UpDownStatus, 84 /// Device name 85 pub name: String, 86 } 87 88 #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] 89 pub enum RadioType { 90 Ble, 91 Classic, 92 Wifi, 93 Uwb, 94 } 95 96 impl fmt::Display for RadioType { fmt(&self, f: &mut fmt::Formatter) -> fmt::Result97 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 98 write!(f, "{:?}", self) 99 } 100 } 101 102 #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] 103 pub enum UpDownStatus { 104 Up, 105 Down, 106 } 107 108 impl fmt::Display for UpDownStatus { fmt(&self, f: &mut fmt::Formatter) -> fmt::Result109 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 110 write!(f, "{:?}", self) 111 } 112 } 113 114 #[derive(Debug, Args, PartialEq)] 115 pub struct Move { 116 /// Device name 117 pub name: String, 118 /// x position of device 119 pub x: f32, 120 /// y position of device 121 pub y: f32, 122 /// Optional z position of device 123 pub z: Option<f32>, 124 } 125 126 #[derive(Debug, Args, PartialEq)] 127 pub struct Devices { 128 /// Continuously print device(s) information every second 129 #[arg(short, long)] 130 pub continuous: bool, 131 } 132 133 #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)] 134 pub enum OnOffState { 135 #[default] 136 On, 137 Off, 138 } 139 140 #[derive(Debug, Subcommand, PartialEq)] 141 pub enum Beacon { 142 /// Create a beacon chip 143 #[command(subcommand)] 144 Create(BeaconCreate), 145 /// Modify a beacon chip 146 #[command(subcommand)] 147 Patch(BeaconPatch), 148 /// Remove a beacon chip 149 Remove(BeaconRemove), 150 } 151 152 #[derive(Debug, Subcommand, PartialEq)] 153 pub enum BeaconCreate { 154 /// Create a Bluetooth low-energy beacon chip 155 Ble(BeaconCreateBle), 156 } 157 158 #[derive(Debug, Args, PartialEq, Default)] 159 pub struct BeaconCreateBle { 160 /// Name of the device to create 161 pub device_name: Option<String>, 162 /// Name of the beacon chip to create within the new device. May only be specified if device_name is specified 163 pub chip_name: Option<String>, 164 /// 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 165 #[arg(long)] 166 pub address: Option<String>, 167 #[command(flatten)] 168 pub settings: BeaconBleSettings, 169 #[command(flatten)] 170 pub advertise_data: BeaconBleAdvertiseData, 171 #[command(flatten)] 172 pub scan_response_data: BeaconBleScanResponseData, 173 } 174 175 #[derive(Debug, Subcommand, PartialEq)] 176 pub enum BeaconPatch { 177 /// Modify a Bluetooth low-energy beacon chip 178 Ble(BeaconPatchBle), 179 } 180 181 #[derive(Debug, Args, PartialEq, Default)] 182 pub struct BeaconPatchBle { 183 /// Name of the device that contains the chip 184 pub device_name: String, 185 /// Name of the beacon chip to modify 186 pub chip_name: String, 187 /// Bluetooth address of the beacon. Must be a 6-byte hexadecimal string with each byte separated by a colon 188 #[arg(long)] 189 pub address: Option<String>, 190 #[command(flatten)] 191 pub settings: BeaconBleSettings, 192 #[command(flatten)] 193 pub advertise_data: BeaconBleAdvertiseData, 194 #[command(flatten)] 195 pub scan_response_data: BeaconBleScanResponseData, 196 } 197 198 #[derive(Debug, Args, PartialEq)] 199 pub struct BeaconRemove { 200 /// Name of the device to remove 201 pub device_name: String, 202 /// Name of the beacon chip to remove. Can be omitted if the device has exactly 1 chip 203 pub chip_name: Option<String>, 204 } 205 206 #[derive(Debug, Args, PartialEq, Default)] 207 pub struct BeaconBleAdvertiseData { 208 /// Whether the device name should be included in the advertise packet 209 #[arg(long, required = false)] 210 pub include_device_name: bool, 211 /// Whether the transmission power level should be included in the advertise packet 212 #[arg(long, required = false)] 213 pub include_tx_power_level: bool, 214 /// Manufacturer-specific data given as bytes in hexadecimal 215 #[arg(long)] 216 pub manufacturer_data: Option<ParsableBytes>, 217 } 218 219 #[derive(Debug, Clone, PartialEq)] 220 pub struct ParsableBytes(pub Vec<u8>); 221 222 impl ParsableBytes { unwrap(self) -> Vec<u8>223 fn unwrap(self) -> Vec<u8> { 224 self.0 225 } 226 } 227 228 impl FromStr for ParsableBytes { 229 type Err = FromHexError; from_str(s: &str) -> Result<Self, Self::Err>230 fn from_str(s: &str) -> Result<Self, Self::Err> { 231 hex_to_bytes(s.strip_prefix("0x").unwrap_or(s)).map(ParsableBytes) 232 } 233 } 234 235 #[derive(Debug, Args, PartialEq, Default)] 236 pub struct BeaconBleScanResponseData { 237 /// Whether the device name should be included in the scan response packet 238 #[arg(long, required = false)] 239 pub scan_response_include_device_name: bool, 240 /// Whether the transmission power level should be included in the scan response packet 241 #[arg(long, required = false)] 242 pub scan_response_include_tx_power_level: bool, 243 /// Manufacturer-specific data to include in the scan response packet given as bytes in hexadecimal 244 #[arg(long, value_name = "MANUFACTURER_DATA")] 245 pub scan_response_manufacturer_data: Option<ParsableBytes>, 246 } 247 248 #[derive(Debug, Args, PartialEq, Default)] 249 pub struct BeaconBleSettings { 250 /// Set advertise mode to control the advertising latency 251 #[arg(long, value_parser = IntervalParser)] 252 pub advertise_mode: Option<Interval>, 253 /// Set advertise TX power level to control the beacon's transmission power 254 #[arg(long, value_parser = TxPowerParser, allow_hyphen_values = true)] 255 pub tx_power_level: Option<TxPower>, 256 /// Set whether the beacon will respond to scan requests 257 #[arg(long)] 258 pub scannable: bool, 259 /// Limit advertising to an amount of time given in milliseconds 260 #[arg(long, value_name = "MS")] 261 pub timeout: Option<u64>, 262 } 263 264 #[derive(Clone, Debug, PartialEq)] 265 pub enum Interval { 266 Mode(AdvertiseMode), 267 Milliseconds(u64), 268 } 269 270 #[derive(Clone)] 271 struct IntervalParser; 272 273 impl TypedValueParser for IntervalParser { 274 type Value = Interval; 275 parse_ref( &self, cmd: &clap::Command, arg: Option<&clap::Arg>, value: &std::ffi::OsStr, ) -> Result<Self::Value, clap::Error>276 fn parse_ref( 277 &self, 278 cmd: &clap::Command, 279 arg: Option<&clap::Arg>, 280 value: &std::ffi::OsStr, 281 ) -> Result<Self::Value, clap::Error> { 282 let millis_parser = clap::value_parser!(u64); 283 let mode_parser = clap::value_parser!(AdvertiseMode); 284 285 mode_parser 286 .parse_ref(cmd, arg, value) 287 .map(Self::Value::Mode) 288 .or(millis_parser.parse_ref(cmd, arg, value).map(Self::Value::Milliseconds)) 289 } 290 possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>>291 fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> { 292 Some(Box::new( 293 AdvertiseMode::value_variants().iter().map(|v| v.to_possible_value().unwrap()).chain( 294 iter::once( 295 PossibleValue::new("<MS>").help("An exact advertise interval in milliseconds"), 296 ), 297 ), 298 )) 299 } 300 } 301 302 #[derive(Clone, Debug, PartialEq)] 303 pub enum TxPower { 304 Level(TxPowerLevel), 305 Dbm(i8), 306 } 307 308 #[derive(Clone)] 309 struct TxPowerParser; 310 311 impl TypedValueParser for TxPowerParser { 312 type Value = TxPower; 313 parse_ref( &self, cmd: &clap::Command, arg: Option<&clap::Arg>, value: &std::ffi::OsStr, ) -> Result<Self::Value, clap::Error>314 fn parse_ref( 315 &self, 316 cmd: &clap::Command, 317 arg: Option<&clap::Arg>, 318 value: &std::ffi::OsStr, 319 ) -> Result<Self::Value, clap::Error> { 320 let dbm_parser = clap::value_parser!(i8); 321 let level_parser = clap::value_parser!(TxPowerLevel); 322 323 level_parser 324 .parse_ref(cmd, arg, value) 325 .map(Self::Value::Level) 326 .or(dbm_parser.parse_ref(cmd, arg, value).map(Self::Value::Dbm)) 327 } 328 possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>>329 fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> { 330 Some(Box::new( 331 TxPowerLevel::value_variants().iter().map(|v| v.to_possible_value().unwrap()).chain( 332 iter::once( 333 PossibleValue::new("<DBM>").help("An exact transmit power level in dBm"), 334 ), 335 ), 336 )) 337 } 338 } 339 340 #[derive(Debug, Clone, ValueEnum, PartialEq)] 341 pub enum AdvertiseMode { 342 /// Lowest power consumption, preferred advertising mode 343 LowPower, 344 /// Balanced between advertising frequency and power consumption 345 Balanced, 346 /// Highest power consumption 347 LowLatency, 348 } 349 350 #[derive(Debug, Clone, ValueEnum, PartialEq)] 351 pub enum TxPowerLevel { 352 /// Lowest transmission power level 353 UltraLow, 354 /// Low transmission power level 355 Low, 356 /// Medium transmission power level 357 Medium, 358 /// High transmission power level 359 High, 360 } 361 362 #[derive(Debug, Subcommand, PartialEq)] 363 pub enum Capture { 364 /// List currently available Captures (packet captures) 365 List(ListCapture), 366 /// Patch a Capture source to turn packet capture on/off 367 Patch(PatchCapture), 368 /// Download the packet capture content 369 Get(GetCapture), 370 } 371 372 #[derive(Debug, Args, PartialEq, Default)] 373 pub struct ListCapture { 374 /// Optional strings of pattern for captures to list. Possible filter fields include Capture ID, Device Name, and Chip Kind 375 pub patterns: Vec<String>, 376 /// Continuously print Capture information every second 377 #[arg(short, long)] 378 pub continuous: bool, 379 } 380 381 #[derive(Debug, Args, PartialEq, Default)] 382 pub struct PatchCapture { 383 /// Packet capture state 384 #[arg(value_enum, ignore_case = true)] 385 pub state: OnOffState, 386 /// Optional strings of pattern for captures to patch. Possible filter fields include Capture ID, Device Name, and Chip Kind 387 pub patterns: Vec<String>, 388 } 389 390 #[derive(Debug, Args, PartialEq, Default)] 391 pub struct GetCapture { 392 /// Optional strings of pattern for captures to get. Possible filter fields include Capture ID, Device Name, and Chip Kind 393 pub patterns: Vec<String>, 394 /// Directory to store downloaded capture file(s) 395 #[arg(short = 'o', long)] 396 pub location: Option<String>, 397 #[arg(skip)] 398 pub filenames: Vec<String>, 399 #[arg(skip)] 400 pub current_file: String, 401 } 402 403 impl From<&BeaconBleSettings> for AdvertiseSettingsProto { from(value: &BeaconBleSettings) -> Self404 fn from(value: &BeaconBleSettings) -> Self { 405 AdvertiseSettingsProto { 406 interval: value.advertise_mode.as_ref().map(IntervalProto::from), 407 tx_power: value.tx_power_level.as_ref().map(TxPowerProto::from), 408 scannable: value.scannable, 409 timeout: value.timeout.unwrap_or_default(), 410 ..Default::default() 411 } 412 } 413 } 414 415 impl From<&Interval> for IntervalProto { from(value: &Interval) -> Self416 fn from(value: &Interval) -> Self { 417 match value { 418 Interval::Mode(mode) => IntervalProto::AdvertiseMode( 419 match mode { 420 AdvertiseMode::LowPower => AdvertiseModeProto::LOW_POWER, 421 AdvertiseMode::Balanced => AdvertiseModeProto::BALANCED, 422 AdvertiseMode::LowLatency => AdvertiseModeProto::LOW_LATENCY, 423 } 424 .into(), 425 ), 426 Interval::Milliseconds(ms) => IntervalProto::Milliseconds(*ms), 427 } 428 } 429 } 430 431 impl From<&TxPower> for TxPowerProto { from(value: &TxPower) -> Self432 fn from(value: &TxPower) -> Self { 433 match value { 434 TxPower::Level(level) => TxPowerProto::TxPowerLevel( 435 match level { 436 TxPowerLevel::UltraLow => AdvertiseTxPowerProto::ULTRA_LOW, 437 TxPowerLevel::Low => AdvertiseTxPowerProto::LOW, 438 TxPowerLevel::Medium => AdvertiseTxPowerProto::MEDIUM, 439 TxPowerLevel::High => AdvertiseTxPowerProto::HIGH, 440 } 441 .into(), 442 ), 443 TxPower::Dbm(dbm) => TxPowerProto::Dbm((*dbm).into()), 444 } 445 } 446 } 447 448 impl From<&BeaconBleAdvertiseData> for AdvertiseDataProto { from(value: &BeaconBleAdvertiseData) -> Self449 fn from(value: &BeaconBleAdvertiseData) -> Self { 450 AdvertiseDataProto { 451 include_device_name: value.include_device_name, 452 include_tx_power_level: value.include_tx_power_level, 453 manufacturer_data: value 454 .manufacturer_data 455 .clone() 456 .map(ParsableBytes::unwrap) 457 .unwrap_or_default(), 458 ..Default::default() 459 } 460 } 461 } 462 463 impl From<&BeaconBleScanResponseData> for AdvertiseDataProto { from(value: &BeaconBleScanResponseData) -> Self464 fn from(value: &BeaconBleScanResponseData) -> Self { 465 AdvertiseDataProto { 466 include_device_name: value.scan_response_include_device_name, 467 include_tx_power_level: value.scan_response_include_tx_power_level, 468 manufacturer_data: value 469 .scan_response_manufacturer_data 470 .clone() 471 .map(ParsableBytes::unwrap) 472 .unwrap_or_default(), 473 ..Default::default() 474 } 475 } 476 } 477 478 #[cfg(test)] 479 mod tests { 480 use super::*; 481 482 #[test] test_hex_parser_succeeds()483 fn test_hex_parser_succeeds() { 484 let hex = ParsableBytes::from_str("beef1234"); 485 assert!(hex.is_ok(), "{}", hex.unwrap_err()); 486 let hex = hex.unwrap().unwrap(); 487 488 assert_eq!(vec![0xbeu8, 0xef, 0x12, 0x34], hex); 489 } 490 491 #[test] test_hex_parser_prefix_succeeds()492 fn test_hex_parser_prefix_succeeds() { 493 let hex = ParsableBytes::from_str("0xabcd"); 494 assert!(hex.is_ok(), "{}", hex.unwrap_err()); 495 let hex = hex.unwrap().unwrap(); 496 497 assert_eq!(vec![0xabu8, 0xcd], hex); 498 } 499 500 #[test] test_hex_parser_empty_str_succeeds()501 fn test_hex_parser_empty_str_succeeds() { 502 let hex = ParsableBytes::from_str(""); 503 assert!(hex.is_ok(), "{}", hex.unwrap_err()); 504 let hex = hex.unwrap().unwrap(); 505 506 assert_eq!(Vec::<u8>::new(), hex); 507 } 508 509 #[test] test_hex_parser_bad_digit_fails()510 fn test_hex_parser_bad_digit_fails() { 511 assert!(ParsableBytes::from_str("0xabcdefg").is_err()); 512 } 513 } 514