blob: 8d1bf4ca46d8abe510078dc991fa327977435fb6 [file] [edit]
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use clap::builder::{PossibleValue, TypedValueParser};
use clap::{Args, Parser, Subcommand, ValueEnum};
use hex::{decode as hex_to_bytes, FromHexError};
use netsim_proto::model::chip::ble_beacon::advertise_settings::{
AdvertiseMode as AdvertiseModeProto, AdvertiseTxPower as AdvertiseTxPowerProto,
Interval as IntervalProto, Tx_power as TxPowerProto,
};
use netsim_proto::model::chip::ble_beacon::{
AdvertiseData as AdvertiseDataProto, AdvertiseSettings as AdvertiseSettingsProto,
};
use std::fmt;
use std::iter;
use std::str::FromStr;
#[derive(Debug, Parser)]
pub struct NetsimArgs {
#[command(subcommand)]
pub command: Command,
/// Set verbose mode
#[arg(short, long)]
pub verbose: bool,
/// Set custom grpc port
#[arg(short, long)]
pub port: Option<i32>,
/// Set netsimd instance number to connect
#[arg(short, long)]
pub instance: Option<u16>,
/// Set vsock cid:port pair
#[arg(long)]
pub vsock: Option<String>,
}
#[derive(Debug, Subcommand, PartialEq)]
#[command(infer_subcommands = true)]
pub enum Command {
/// Print Netsim version information
Version,
/// Control the radio state of a device
Radio(Radio),
/// Set the device location
Move(Move),
/// Display device(s) information
Devices(Devices),
/// Reset Netsim device scene
Reset,
/// Open netsim Web UI
Gui,
/// Control the packet capture functionalities with commands: list, patch, get
#[command(subcommand, visible_alias("pcap"))]
Capture(Capture),
/// Opens netsim artifacts directory (log, pcaps)
Artifact,
/// A chip that sends advertisements at a set interval
#[command(subcommand)]
Beacon(Beacon),
/// Open Bumble Hive Web Page
Bumble,
/// Manage Link properties
#[command(subcommand)]
Link(Link),
}
#[derive(Debug, Args, PartialEq)]
pub struct Radio {
/// Radio type
#[arg(value_enum, ignore_case = true)]
pub radio_type: RadioType,
/// Radio status
#[arg(value_enum, ignore_case = true)]
pub status: UpDownStatus,
/// Device name
pub name: String,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
pub enum RadioType {
Ble,
Classic,
Wifi,
Uwb,
}
impl fmt::Display for RadioType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
pub enum UpDownStatus {
Up,
Down,
}
impl fmt::Display for UpDownStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Debug, Args, PartialEq)]
pub struct Move {
/// Device name
pub name: String,
/// x position of device
pub x: f32,
/// y position of device
pub y: f32,
/// Optional z position of device
pub z: Option<f32>,
}
#[derive(Debug, Args, PartialEq)]
pub struct Devices {
/// Continuously print device(s) information every second
#[arg(short, long)]
pub continuous: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
pub enum OnOffState {
#[default]
On,
Off,
}
#[derive(Debug, Subcommand, PartialEq)]
pub enum Beacon {
/// Create a beacon chip
#[command(subcommand)]
Create(BeaconCreate),
/// Modify a beacon chip
#[command(subcommand)]
Patch(BeaconPatch),
/// Remove a beacon chip
Remove(BeaconRemove),
}
#[derive(Debug, Subcommand, PartialEq)]
pub enum BeaconCreate {
/// Create a Bluetooth low-energy beacon chip
Ble(BeaconCreateBle),
}
#[derive(Debug, Args, PartialEq, Default)]
pub struct BeaconCreateBle {
/// Name of the device to create
pub device_name: Option<String>,
/// Name of the beacon chip to create within the new device. May only be specified if device_name is specified
pub chip_name: Option<String>,
/// 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
#[arg(long)]
pub address: Option<String>,
#[command(flatten)]
pub settings: BeaconBleSettings,
#[command(flatten)]
pub advertise_data: BeaconBleAdvertiseData,
#[command(flatten)]
pub scan_response_data: BeaconBleScanResponseData,
}
#[derive(Debug, Subcommand, PartialEq)]
pub enum BeaconPatch {
/// Modify a Bluetooth low-energy beacon chip
Ble(BeaconPatchBle),
}
#[derive(Debug, Args, PartialEq, Default)]
pub struct BeaconPatchBle {
/// Name of the device that contains the chip
pub device_name: String,
/// Name of the beacon chip to modify
pub chip_name: String,
/// Bluetooth address of the beacon. Must be a 6-byte hexadecimal string with each byte separated by a colon
#[arg(long)]
pub address: Option<String>,
#[command(flatten)]
pub settings: BeaconBleSettings,
#[command(flatten)]
pub advertise_data: BeaconBleAdvertiseData,
#[command(flatten)]
pub scan_response_data: BeaconBleScanResponseData,
}
#[derive(Debug, Args, PartialEq)]
pub struct BeaconRemove {
/// Name of the device to remove
pub device_name: String,
/// Name of the beacon chip to remove. Can be omitted if the device has exactly 1 chip
pub chip_name: Option<String>,
}
#[derive(Debug, Args, PartialEq, Default)]
pub struct BeaconBleAdvertiseData {
/// Whether the device name should be included in the advertise packet
#[arg(long, required = false)]
pub include_device_name: bool,
/// Whether the transmission power level should be included in the advertise packet
#[arg(long, required = false)]
pub include_tx_power_level: bool,
/// Manufacturer-specific data given as bytes in hexadecimal
#[arg(long)]
pub manufacturer_data: Option<ParsableBytes>,
}
#[derive(Debug, Subcommand, PartialEq)]
pub enum Link {
/// List all current links and their properties
List,
/// Add or modify link properties
Patch(LinkPatchCommand),
/// Remove link properties
Delete(LinkDeleteCommand),
}
#[derive(Debug, Args, PartialEq)]
pub struct LinkPatchCommand {
#[command(subcommand)]
pub command: LinkPatch,
}
#[derive(Debug, Subcommand, PartialEq)]
pub enum LinkPatch {
/// Patch RSSI (Received Signal Strength Indication) for a link.
Rssi(RssiPatch),
}
#[derive(Debug, Args, PartialEq)]
pub struct LinkDeleteCommand {
#[command(subcommand)]
pub command: LinkDelete,
}
#[derive(Debug, Subcommand, PartialEq)]
pub enum LinkDelete {
/// Delete RSSI (Received Signal Strength Indication) for a link.
Rssi(RssiDelete),
}
#[derive(Debug, Args, PartialEq)]
pub struct RssiPatch {
/// Radio type for the link.
#[arg(value_enum, ignore_case = true)]
pub radio_type: RadioType,
/// RSSI value in dBm (e.g., -60). Must be between -128 and 127.
#[arg(allow_hyphen_values = true)]
pub value: i8,
/// Identifier for the sender chip. Defaults to 0 (ANY_CHIP), affecting all senders to the specified receiver.
pub sender_id: Option<u32>,
/// Identifier for the receiver chip. Defaults to 0 (ANY_CHIP), affecting all receivers from the specified sender.
pub receiver_id: Option<u32>,
}
#[derive(Debug, Args, PartialEq)]
pub struct RssiDelete {
/// Radio type for the link.
#[arg(value_enum, ignore_case = true)]
pub radio_type: RadioType,
/// Identifier for the sender chip. Defaults to 0 (ANY_CHIP).
pub sender_id: Option<u32>,
/// Identifier for the receiver chip. Defaults to 0 (ANY_CHIP).
pub receiver_id: Option<u32>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParsableBytes(pub Vec<u8>);
impl ParsableBytes {
fn unwrap(self) -> Vec<u8> {
self.0
}
}
impl FromStr for ParsableBytes {
type Err = FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
hex_to_bytes(s.strip_prefix("0x").unwrap_or(s)).map(ParsableBytes)
}
}
#[derive(Debug, Args, PartialEq, Default)]
pub struct BeaconBleScanResponseData {
/// Whether the device name should be included in the scan response packet
#[arg(long, required = false)]
pub scan_response_include_device_name: bool,
/// Whether the transmission power level should be included in the scan response packet
#[arg(long, required = false)]
pub scan_response_include_tx_power_level: bool,
/// Manufacturer-specific data to include in the scan response packet given as bytes in hexadecimal
#[arg(long, value_name = "MANUFACTURER_DATA")]
pub scan_response_manufacturer_data: Option<ParsableBytes>,
}
#[derive(Debug, Args, PartialEq, Default)]
pub struct BeaconBleSettings {
/// Set advertise mode to control the advertising latency
#[arg(long, value_parser = IntervalParser)]
pub advertise_mode: Option<Interval>,
/// Set advertise TX power level to control the beacon's transmission power
#[arg(long, value_parser = TxPowerParser, allow_hyphen_values = true)]
pub tx_power_level: Option<TxPower>,
/// Set whether the beacon will respond to scan requests
#[arg(long)]
pub scannable: bool,
/// Limit advertising to an amount of time given in milliseconds
#[arg(long, value_name = "MS")]
pub timeout: Option<u64>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Interval {
Mode(AdvertiseMode),
Milliseconds(u64),
}
#[derive(Clone)]
struct IntervalParser;
impl TypedValueParser for IntervalParser {
type Value = Interval;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let millis_parser = clap::value_parser!(u64);
let mode_parser = clap::value_parser!(AdvertiseMode);
mode_parser
.parse_ref(cmd, arg, value)
.map(Self::Value::Mode)
.or(millis_parser.parse_ref(cmd, arg, value).map(Self::Value::Milliseconds))
}
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
Some(Box::new(
AdvertiseMode::value_variants().iter().map(|v| v.to_possible_value().unwrap()).chain(
iter::once(
PossibleValue::new("<MS>").help("An exact advertise interval in milliseconds"),
),
),
))
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum TxPower {
Level(TxPowerLevel),
Dbm(i8),
}
#[derive(Clone)]
struct TxPowerParser;
impl TypedValueParser for TxPowerParser {
type Value = TxPower;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let dbm_parser = clap::value_parser!(i8);
let level_parser = clap::value_parser!(TxPowerLevel);
level_parser
.parse_ref(cmd, arg, value)
.map(Self::Value::Level)
.or(dbm_parser.parse_ref(cmd, arg, value).map(Self::Value::Dbm))
}
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
Some(Box::new(
TxPowerLevel::value_variants().iter().map(|v| v.to_possible_value().unwrap()).chain(
iter::once(
PossibleValue::new("<DBM>").help("An exact transmit power level in dBm"),
),
),
))
}
}
#[derive(Debug, Clone, ValueEnum, PartialEq)]
pub enum AdvertiseMode {
/// Lowest power consumption, preferred advertising mode
LowPower,
/// Balanced between advertising frequency and power consumption
Balanced,
/// Highest power consumption
LowLatency,
}
#[derive(Debug, Clone, ValueEnum, PartialEq)]
pub enum TxPowerLevel {
/// Lowest transmission power level
UltraLow,
/// Low transmission power level
Low,
/// Medium transmission power level
Medium,
/// High transmission power level
High,
}
#[derive(Debug, Subcommand, PartialEq)]
pub enum Capture {
/// List currently available Captures (packet captures)
List(ListCapture),
/// Patch a Capture source to turn packet capture on/off
Patch(PatchCapture),
/// Download the packet capture content
Get(GetCapture),
}
#[derive(Debug, Args, PartialEq, Default)]
pub struct ListCapture {
/// Optional strings of pattern for captures to list. Possible filter fields include Capture ID, Device Name, and Chip Kind
pub patterns: Vec<String>,
/// Continuously print Capture information every second
#[arg(short, long)]
pub continuous: bool,
}
#[derive(Debug, Args, PartialEq, Default)]
pub struct PatchCapture {
/// Packet capture state
#[arg(value_enum, ignore_case = true)]
pub state: OnOffState,
/// Optional strings of pattern for captures to patch. Possible filter fields include Capture ID, Device Name, and Chip Kind
pub patterns: Vec<String>,
}
#[derive(Debug, Args, PartialEq, Default)]
pub struct GetCapture {
/// Optional strings of pattern for captures to get. Possible filter fields include Capture ID, Device Name, and Chip Kind
pub patterns: Vec<String>,
/// Directory to store downloaded capture file(s)
#[arg(short = 'o', long)]
pub location: Option<String>,
#[arg(skip)]
pub filenames: Vec<String>,
#[arg(skip)]
pub current_file: String,
}
impl From<&BeaconBleSettings> for AdvertiseSettingsProto {
fn from(value: &BeaconBleSettings) -> Self {
AdvertiseSettingsProto {
interval: value.advertise_mode.as_ref().map(IntervalProto::from),
tx_power: value.tx_power_level.as_ref().map(TxPowerProto::from),
scannable: value.scannable,
timeout: value.timeout.unwrap_or_default(),
..Default::default()
}
}
}
impl From<&Interval> for IntervalProto {
fn from(value: &Interval) -> Self {
match value {
Interval::Mode(mode) => IntervalProto::AdvertiseMode(
match mode {
AdvertiseMode::LowPower => AdvertiseModeProto::LOW_POWER,
AdvertiseMode::Balanced => AdvertiseModeProto::BALANCED,
AdvertiseMode::LowLatency => AdvertiseModeProto::LOW_LATENCY,
}
.into(),
),
Interval::Milliseconds(ms) => IntervalProto::Milliseconds(*ms),
}
}
}
impl From<&TxPower> for TxPowerProto {
fn from(value: &TxPower) -> Self {
match value {
TxPower::Level(level) => TxPowerProto::TxPowerLevel(
match level {
TxPowerLevel::UltraLow => AdvertiseTxPowerProto::ULTRA_LOW,
TxPowerLevel::Low => AdvertiseTxPowerProto::LOW,
TxPowerLevel::Medium => AdvertiseTxPowerProto::MEDIUM,
TxPowerLevel::High => AdvertiseTxPowerProto::HIGH,
}
.into(),
),
TxPower::Dbm(dbm) => TxPowerProto::Dbm((*dbm).into()),
}
}
}
impl From<&BeaconBleAdvertiseData> for AdvertiseDataProto {
fn from(value: &BeaconBleAdvertiseData) -> Self {
AdvertiseDataProto {
include_device_name: value.include_device_name,
include_tx_power_level: value.include_tx_power_level,
manufacturer_data: value
.manufacturer_data
.clone()
.map(ParsableBytes::unwrap)
.unwrap_or_default(),
..Default::default()
}
}
}
impl From<&BeaconBleScanResponseData> for AdvertiseDataProto {
fn from(value: &BeaconBleScanResponseData) -> Self {
AdvertiseDataProto {
include_device_name: value.scan_response_include_device_name,
include_tx_power_level: value.scan_response_include_tx_power_level,
manufacturer_data: value
.scan_response_manufacturer_data
.clone()
.map(ParsableBytes::unwrap)
.unwrap_or_default(),
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hex_parser_succeeds() {
let hex = ParsableBytes::from_str("beef1234");
assert!(hex.is_ok(), "{}", hex.unwrap_err());
let hex = hex.unwrap().unwrap();
assert_eq!(vec![0xbeu8, 0xef, 0x12, 0x34], hex);
}
#[test]
fn test_hex_parser_prefix_succeeds() {
let hex = ParsableBytes::from_str("0xabcd");
assert!(hex.is_ok(), "{}", hex.unwrap_err());
let hex = hex.unwrap().unwrap();
assert_eq!(vec![0xabu8, 0xcd], hex);
}
#[test]
fn test_hex_parser_empty_str_succeeds() {
let hex = ParsableBytes::from_str("");
assert!(hex.is_ok(), "{}", hex.unwrap_err());
let hex = hex.unwrap().unwrap();
assert_eq!(Vec::<u8>::new(), hex);
}
#[test]
fn test_hex_parser_bad_digit_fails() {
assert!(ParsableBytes::from_str("0xabcdefg").is_err());
}
}