bcms 0.1.12

Bluetooth Client Manager Service for IoT device management
Documentation
use bcms::config::Config;
use bcms::devices::WorkingMode;
use bcms::rpc::RpcClient;
use bcms::Result;
use clap::{Parser, Subcommand};
use std::io::Write;
use std::net::SocketAddr;

#[derive(Parser, Debug)]
#[command(name = "bcms")]
#[command(about = "Bluetooth Client Manager Service CLI")]
struct Args {
    #[arg(long, default_value = "127.0.0.1:4567")]
    server: String,
    
    #[arg(long, short)]
    verbose: bool,
    
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// List discovered devices
    List {
        #[arg(long)]
        only_approved: bool,
    },
    /// Approve a device for BLE data collection (BCMS internal)
    Approve {
        #[arg(long)]
        address: String,
    },
    /// Remove a device from BCMS memory
    Remove {
        #[arg(long)]
        address: String,
    },
    /// Get current working mode
    Mode,
    /// Set working mode
    SetMode {
        #[arg(value_enum)]
        mode: CliWorkingMode,
    },
    /// Perform actual Bluetooth pairing/bonding with a device
    Pair {
        #[arg(long)]
        address: String,
    },
    /// Remove Bluetooth bond/pairing with a device
    Unpair {
        #[arg(long)]
        address: String,
    },
    /// Check if device is paired
    IsPaired {
        #[arg(long)]
        address: String,
    },
}

#[derive(clap::ValueEnum, Clone, Debug)]
enum CliWorkingMode {
    Any,
    Approved,
}

impl From<CliWorkingMode> for WorkingMode {
    fn from(mode: CliWorkingMode) -> Self {
        match mode {
            CliWorkingMode::Any => WorkingMode::Any,
            CliWorkingMode::Approved => WorkingMode::Approved,
        }
    }
}

impl From<WorkingMode> for CliWorkingMode {
    fn from(mode: WorkingMode) -> Self {
        match mode {
            WorkingMode::Any => CliWorkingMode::Any,
            WorkingMode::Approved => CliWorkingMode::Approved,
        }
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let args = Args::parse();
    
    // Initialize logging for debug output only if verbose flag is set or RUST_LOG is set
    if args.verbose || std::env::var("RUST_LOG").is_ok() {
        tracing_subscriber::fmt()
            .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()
                .add_directive("bcms=debug".parse().unwrap()))
            .init();
    }
    
    // Parse server address
    let server_addr: SocketAddr = args.server.parse()
        .map_err(|e| bcms::Error::Config(format!("Invalid server address: {}", e)))?;
    
    let client = RpcClient::new(server_addr);
    
    match args.command {
        Commands::List { only_approved } => {
            let (devices, errors) = client.list_devices(only_approved).await?;
            
            if !errors.is_empty() {
                eprintln!("Errors:");
                for error in errors {
                    eprintln!("  {}", error);
                }
            }
            
            if devices.is_empty() {
                println!("No devices found.");
            } else {
                println!("Found {} device{}:", devices.len(), if devices.len() == 1 { "" } else { "s" });
                println!();
                
                // Calculate column widths
                let max_name_width = devices.iter().map(|d| d.name.len()).max().unwrap_or(4).max(4);
                let max_address_width = devices.iter().map(|d| d.address.len()).max().unwrap_or(7).max(7);
                let approved_width = 8; // "Approved"
                let paired_width = 6; // "Paired"
                
                // Print header
                println!("โ”Œโ”€{}โ”€โ”ฌโ”€{}โ”€โ”ฌโ”€{}โ”€โ”ฌโ”€{}โ”€โ”", 
                    "โ”€".repeat(max_name_width), 
                    "โ”€".repeat(max_address_width), 
                    "โ”€".repeat(approved_width), 
                    "โ”€".repeat(paired_width)
                );
                println!("โ”‚ {:width_name$} โ”‚ {:width_addr$} โ”‚ {:width_approved$} โ”‚ {:width_paired$} โ”‚", 
                    "Name", "Address", "Approved", "Paired",
                    width_name = max_name_width,
                    width_addr = max_address_width,
                    width_approved = approved_width,
                    width_paired = paired_width
                );
                println!("โ”œโ”€{}โ”€โ”ผโ”€{}โ”€โ”ผโ”€{}โ”€โ”ผโ”€{}โ”€โ”ค", 
                    "โ”€".repeat(max_name_width), 
                    "โ”€".repeat(max_address_width), 
                    "โ”€".repeat(approved_width), 
                    "โ”€".repeat(paired_width)
                );
                
                // Print devices
                for device in devices {
                    let approved_symbol = if device.approved { "โœ“" } else { "โœ—" };
                    let paired_symbol = if device.paired { "โœ“" } else { "โœ—" };
                    
                    println!("โ”‚ {:width_name$} โ”‚ {:width_addr$} โ”‚ {:^width_approved$} โ”‚ {:^width_paired$} โ”‚", 
                        device.name, 
                        device.address, 
                        approved_symbol,
                        paired_symbol,
                        width_name = max_name_width,
                        width_addr = max_address_width,
                        width_approved = approved_width,
                        width_paired = paired_width
                    );
                }
                
                // Print footer
                println!("โ””โ”€{}โ”€โ”ดโ”€{}โ”€โ”ดโ”€{}โ”€โ”ดโ”€{}โ”€โ”˜", 
                    "โ”€".repeat(max_name_width), 
                    "โ”€".repeat(max_address_width), 
                    "โ”€".repeat(approved_width), 
                    "โ”€".repeat(paired_width)
                );
                
                println!();
                println!("Legend: โœ“ Yes  โœ— No");
            }
        }
        
        Commands::Approve { address } => {
            let (success, errors) = client.approve_device(&address).await?;
            
            if !errors.is_empty() {
                eprintln!("Errors:");
                for error in errors {
                    eprintln!("  {}", error);
                }
            }
            
            if success {
                println!("โœ“ Device approved: {}", address);
            } else {
                println!("โœ— Failed to approve device: {}", address);
            }
        }
        
        Commands::Remove { address } => {
            let (success, errors) = client.remove_device(&address).await?;
            
            if !errors.is_empty() {
                eprintln!("Errors:");
                for error in errors {
                    eprintln!("  {}", error);
                }
            }
            
            if success {
                println!("โœ“ Device removed: {}", address);
            } else {
                println!("โœ— Failed to remove device: {}", address);
            }
        }
        
        Commands::Mode => {
            let mode = client.get_mode().await?;
            let (symbol, mode_str, description) = match mode {
                WorkingMode::Any => ("๐Ÿ”“", "any", "connects to all discovered devices"),
                WorkingMode::Approved => ("๐Ÿ”’", "approved", "connects only to approved devices"),
            };
            println!("Current working mode: {} {} ({})", symbol, mode_str, description);
        }
        
        Commands::SetMode { mode } => {
            let working_mode = WorkingMode::from(mode.clone());
            let (success, errors) = client.set_mode(working_mode).await?;
            
            if !errors.is_empty() {
                eprintln!("Errors:");
                for error in errors {
                    eprintln!("  {}", error);
                }
            }
            
            if success {
                let (symbol, mode_str) = match mode {
                    CliWorkingMode::Any => ("๐Ÿ”“", "any"),
                    CliWorkingMode::Approved => ("๐Ÿ”’", "approved"),
                };
                println!("โœ“ Working mode set to: {} {}", symbol, mode_str);
            } else {
                println!("โœ— Failed to set working mode");
            }
        }
        
        Commands::Pair { address } => {
            println!("๐Ÿ”— Attempting to pair with device: {}", address);
            
            // Start the pairing process
            let (initial_success, errors) = client.pair_device(&address).await?;
            
            if !errors.is_empty() {
                eprintln!("Errors:");
                for error in errors {
                    eprintln!("  {}", error);
                }
            }
            
            if !initial_success {
                println!("โœ— Failed to initiate pairing with device: {}", address);
                return Ok(());
            }
            
            // Show loading animation while waiting for pairing to complete
            println!("โณ Waiting for device to pair...");
            
            let spinner_chars = ['โ ‹', 'โ ™', 'โ น', 'โ ธ', 'โ ผ', 'โ ด', 'โ ฆ', 'โ ง', 'โ ‡', 'โ '];
            let mut spinner_index = 0;
            let mut attempts = 0;
            const MAX_ATTEMPTS: u32 = 60; // 60 seconds max for pairing
            
            loop {
                // Check if device is now paired
                let (is_paired, _) = client.is_device_paired(&address).await?;
                
                if is_paired {
                    print!("\rโœ“ Device successfully paired: {}          \n", address);
                    break;
                }
                
                attempts += 1;
                if attempts >= MAX_ATTEMPTS {
                    print!("\rโš  Pairing timeout after {}s - device may still be pairing in background          \n", MAX_ATTEMPTS);
                    println!("  ๐Ÿ’ก Try running 'bcms list' to check if the device paired successfully");
                    println!("  ๐Ÿ’ก Some devices require manual confirmation or take longer to pair");
                    break;
                }
                
                // Show spinner with more helpful messaging
                let progress_bar = "=".repeat((attempts * 20 / MAX_ATTEMPTS) as usize);
                let remaining_bar = " ".repeat(20 - (attempts * 20 / MAX_ATTEMPTS) as usize);
                print!("\r{} Pairing in progress... [{}{}] {}s", 
                    spinner_chars[spinner_index], progress_bar, remaining_bar, attempts);
                std::io::stdout().flush().unwrap();
                
                spinner_index = (spinner_index + 1) % spinner_chars.len();
                tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
            }
        }
        
        Commands::Unpair { address } => {
            let (success, errors) = client.unpair_device(&address).await?;
            
            if !errors.is_empty() {
                eprintln!("Errors:");
                for error in errors {
                    eprintln!("  {}", error);
                }
            }
            
            if success {
                println!("โœ“ Device unpaired: {}", address);
            } else {
                println!("โœ— Failed to unpair device: {}", address);
            }
        }
        
        Commands::IsPaired { address } => {
            let (is_paired, errors) = client.is_device_paired(&address).await?;
            
            if !errors.is_empty() {
                eprintln!("Errors:");
                for error in errors {
                    eprintln!("  {}", error);
                }
            }
            
            if is_paired {
                println!("โœ“ Device {} is paired", address);
            } else {
                println!("ยท Device {} is not paired", address);
            }
        }
    }
    
    Ok(())
}