rust-patlite-beacon 0.1.0

A Rust library and CLI tool for controlling USB PATLITE beacon devices
Documentation
use anyhow::Result;
use clap::{Parser, Subcommand};
use rust_patlite_beacon::{Beacon, BuzzerCount, BuzzerPattern, BuzzerVolume, LedColor, LedPattern, Setting};

#[derive(Parser)]
#[command(name = "beacon")]
#[command(about = "Control USB beacon device", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
    
    #[arg(long, value_name = "COLOR")]
    color: Option<String>,
    
    #[arg(long, value_name = "PATTERN")]
    pattern: Option<String>,
    
    #[arg(long, value_name = "BUZZER")]
    buzzer: Option<String>,
    
    #[arg(long, value_name = "VOLUME")]
    volume: Option<u8>,
    
    #[arg(long, value_name = "COUNT")]
    count: Option<u8>,
}

#[derive(Subcommand)]
enum Commands {
    Scan,
    
    Light {
        #[arg(value_name = "COLOR")]
        color: String,
        
        #[arg(value_name = "PATTERN", default_value = "on")]
        pattern: String,
    },
    
    Buzzer {
        #[arg(value_name = "PATTERN")]
        pattern: String,
        
        #[arg(value_name = "COUNT", default_value = "0")]
        count: u8,
    },
    
    Volume {
        #[arg(value_name = "LEVEL")]
        level: u8,
    },
    
    BuzzerEx {
        #[arg(value_name = "PATTERN")]
        pattern: String,
        
        #[arg(value_name = "COUNT")]
        count: u8,
        
        #[arg(value_name = "VOLUME")]
        volume: u8,
    },
    
    Setting {
        #[arg(value_name = "STATE")]
        state: String,
    },
    
    TouchSensor,
    
    Reset,
}

fn main() -> Result<()> {
    let cli = Cli::parse();
    
    // Handle scanning without opening device
    if let Some(Commands::Scan) = &cli.command {
        let devices = Beacon::scan()?;
        if devices.is_empty() {
            println!("No beacon devices found");
        } else {
            println!("Found {} beacon device(s):", devices.len());
            for (bus, addr, vid, pid) in devices {
                println!("  Bus {:03} Device {:03}: ID {:04x}:{:04x}", bus, addr, vid, pid);
            }
        }
        return Ok(());
    }
    
    // Open the beacon device for all other commands
    let beacon = Beacon::open()?;
    
    // Handle commands
    match cli.command {
        Some(Commands::Light { color, pattern }) => {
            let led_color = LedColor::from_str(&color)
                .ok_or_else(|| anyhow::anyhow!("Invalid color: {}", color))?;
            let led_pattern = LedPattern::from_str(&pattern)
                .ok_or_else(|| anyhow::anyhow!("Invalid pattern: {}", pattern))?;
            beacon.set_light(led_color, led_pattern)?;
            println!("Set light to {} with pattern {}", color, pattern);
        }
        
        Some(Commands::Buzzer { pattern, count }) => {
            let buzzer_pattern = BuzzerPattern::from_str(&pattern)
                .ok_or_else(|| anyhow::anyhow!("Invalid buzzer pattern: {}", pattern))?;
            let buzzer_count = if count == 0 {
                BuzzerCount::CONTINUOUS
            } else {
                BuzzerCount::times(count)
                    .ok_or_else(|| anyhow::anyhow!("Invalid count: {} (must be 0 or 1-14)", count))?
            };
            beacon.set_buzzer(buzzer_pattern, buzzer_count)?;
            println!("Set buzzer to {} {} times", pattern, if count == 0 { "continuous".to_string() } else { count.to_string() });
        }
        
        Some(Commands::Volume { level }) => {
            let volume = BuzzerVolume::level(level)
                .ok_or_else(|| anyhow::anyhow!("Invalid volume: {} (must be 0-10)", level))?;
            beacon.set_volume(volume)?;
            println!("Set volume to {}", level);
        }
        
        Some(Commands::BuzzerEx { pattern, count, volume }) => {
            let buzzer_pattern = BuzzerPattern::from_str(&pattern)
                .ok_or_else(|| anyhow::anyhow!("Invalid buzzer pattern: {}", pattern))?;
            let buzzer_count = if count == 0 {
                BuzzerCount::CONTINUOUS
            } else {
                BuzzerCount::times(count)
                    .ok_or_else(|| anyhow::anyhow!("Invalid count: {} (must be 0 or 1-14)", count))?
            };
            let buzzer_volume = BuzzerVolume::level(volume)
                .ok_or_else(|| anyhow::anyhow!("Invalid volume: {} (must be 0-10)", volume))?;
            beacon.set_buzzer_ex(buzzer_pattern, buzzer_count, buzzer_volume)?;
            println!("Set buzzer to {} {} times at volume {}", pattern, 
                if count == 0 { "continuous".to_string() } else { count.to_string() }, volume);
        }
        
        Some(Commands::Setting { state }) => {
            let setting = match state.to_lowercase().as_str() {
                "on" | "1" => Setting::On,
                "off" | "0" => Setting::Off,
                _ => return Err(anyhow::anyhow!("Invalid setting state: {} (use 'on' or 'off')", state)),
            };
            beacon.set_setting(setting)?;
            println!("Set connection display to {}", state);
        }
        
        Some(Commands::TouchSensor) => {
            let state = beacon.get_touch_sensor_state()?;
            println!("Touch sensor input = {}", if state { "ON" } else { "OFF" });
        }
        
        Some(Commands::Reset) => {
            beacon.reset()?;
            println!("Device reset: LED off, buzzer stopped");
        }
        
        Some(Commands::Scan) => unreachable!(), // Already handled above
        
        None => {
            // Handle the shorthand syntax like --color red
            let mut action_taken = false;
            
            if let Some(ref color) = cli.color {
                let led_color = LedColor::from_str(color)
                    .ok_or_else(|| anyhow::anyhow!("Invalid color: {}", color))?;
                let led_pattern = if let Some(ref pattern) = cli.pattern {
                    LedPattern::from_str(pattern)
                        .ok_or_else(|| anyhow::anyhow!("Invalid pattern: {}", pattern))?
                } else {
                    LedPattern::On
                };
                beacon.set_light(led_color, led_pattern)?;
                println!("Set light to {} with pattern {:?}", color, led_pattern);
                action_taken = true;
            }
            
            if let Some(ref buzzer) = cli.buzzer {
                let buzzer_pattern = BuzzerPattern::from_str(buzzer)
                    .ok_or_else(|| anyhow::anyhow!("Invalid buzzer pattern: {}", buzzer))?;
                let buzzer_count = if let Some(count) = cli.count {
                    if count == 0 {
                        BuzzerCount::CONTINUOUS
                    } else {
                        BuzzerCount::times(count)
                            .ok_or_else(|| anyhow::anyhow!("Invalid count: {} (must be 0 or 1-14)", count))?
                    }
                } else {
                    BuzzerCount::CONTINUOUS
                };
                
                if let Some(volume) = cli.volume {
                    let buzzer_volume = BuzzerVolume::level(volume)
                        .ok_or_else(|| anyhow::anyhow!("Invalid volume: {} (must be 0-10)", volume))?;
                    beacon.set_buzzer_ex(buzzer_pattern, buzzer_count, buzzer_volume)?;
                    println!("Set buzzer to {} at volume {}", buzzer, volume);
                } else {
                    beacon.set_buzzer(buzzer_pattern, buzzer_count)?;
                    println!("Set buzzer to {}", buzzer);
                }
                action_taken = true;
            }
            
            if !action_taken && cli.volume.is_none() {
                println!("No action specified. Use --help for usage information.");
            }
        }
    }
    
    Ok(())
}