use clap::{Parser, Subcommand, ValueEnum};
use color_eyre::eyre::Result;
use elk_led_controller::*;
use tokio::time::Duration;
use tracing::{debug, error, info, instrument, trace, warn};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Clone, ValueEnum, Debug)]
enum EffectType {
Rainbow,
Jump,
JumpAll,
CrossfadeRed,
CrossfadeGreen,
CrossfadeBlue,
CrossfadeRgb,
Blink,
BlinkRed,
BlinkGreen,
BlinkBlue,
}
impl std::fmt::Display for EffectType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EffectType::Rainbow => write!(f, "rainbow"),
EffectType::Jump => write!(f, "jump"),
EffectType::JumpAll => write!(f, "jump_all"),
EffectType::CrossfadeRed => write!(f, "crossfade_red"),
EffectType::CrossfadeGreen => write!(f, "crossfade_green"),
EffectType::CrossfadeBlue => write!(f, "crossfade_blue"),
EffectType::CrossfadeRgb => write!(f, "crossfade_rgb"),
EffectType::Blink => write!(f, "blink"),
EffectType::BlinkRed => write!(f, "blink_red"),
EffectType::BlinkGreen => write!(f, "blink_green"),
EffectType::BlinkBlue => write!(f, "blink_blue"),
}
}
}
#[derive(Clone, ValueEnum, Debug)]
enum AudioModeType {
FrequencyColor,
EnergyBrightness,
BeatEffects,
SpectralFlow,
EnhancedFrequencyColor,
BpmSync,
}
impl From<AudioModeType> for VisualizationMode {
fn from(mode: AudioModeType) -> Self {
match mode {
AudioModeType::FrequencyColor => VisualizationMode::FrequencyColor,
AudioModeType::EnergyBrightness => VisualizationMode::EnergyBrightness,
AudioModeType::BeatEffects => VisualizationMode::BeatEffects,
AudioModeType::SpectralFlow => VisualizationMode::SpectralFlow,
AudioModeType::EnhancedFrequencyColor => VisualizationMode::EnhancedFrequencyColor,
AudioModeType::BpmSync => VisualizationMode::BpmSync,
}
}
}
#[derive(Clone, ValueEnum, Debug)]
enum AudioRangeType {
Bass,
Mid,
High,
Full,
}
impl From<AudioRangeType> for FrequencyRange {
fn from(range: AudioRangeType) -> Self {
match range {
AudioRangeType::Bass => FrequencyRange::Bass,
AudioRangeType::Mid => FrequencyRange::Mid,
AudioRangeType::High => FrequencyRange::High,
AudioRangeType::Full => FrequencyRange::Full,
}
}
}
#[derive(Subcommand)]
enum Commands {
Demo {
#[arg(short, long, default_value_t = 5)]
duration: u64,
},
On,
Off,
Red,
Green,
Blue,
White,
Brightness {
#[arg(short, long, default_value_t = 100)]
level: u8,
},
ColorTemp {
#[arg(short, long, default_value_t = 4000)]
kelvin: u32,
},
Color {
#[arg(short, long, default_value_t = 255)]
red: u8,
#[arg(short, long, default_value_t = 255)]
green: u8,
#[arg(short, long, default_value_t = 255)]
blue: u8,
},
Effect {
#[arg(short, long, value_enum, default_value_t = EffectType::Rainbow)]
effect_type: EffectType,
#[arg(short, long, default_value_t = 50)]
speed: u8,
},
ScheduleOn {
#[arg(long, default_value_t = 8)]
hour: u8,
#[arg(short, long, default_value_t = 30)]
minute: u8,
#[arg(short, long, default_value = "weekdays")]
days: String,
},
ScheduleOff {
#[arg(long, default_value_t = 23)]
hour: u8,
#[arg(short, long, default_value_t = 45)]
minute: u8,
#[arg(short, long, default_value = "weekdays")]
days: String,
},
Audio {
#[arg(short, long, value_enum, default_value_t = AudioModeType::FrequencyColor)]
mode: AudioModeType,
#[arg(short, long, value_enum, default_value_t = AudioRangeType::Full)]
range: AudioRangeType,
#[arg(short, long, default_value_t = 70)]
sensitivity: u8,
#[arg(short, long, default_value_t = 50)]
update_ms: u32,
#[arg(short, long, default_value_t = false)]
test: bool,
#[arg(short, long)]
device: Option<String>,
},
}
#[tokio::main]
#[instrument]
async fn main() -> Result<()> {
tracing_subscriber::fmt().compact().init();
color_eyre::install()?;
let cli = Cli::parse();
debug!("Parsed command line arguments");
info!("Starting LED controller");
let mut device = match BleLedDevice::new_without_power().await {
Ok(dev) => dev,
Err(e) => {
error!("Failed to initialize device: {}", e);
return Err(e.into());
}
};
match cli.command.unwrap_or(Commands::Demo { duration: 5 }) {
Commands::Demo { duration } => {
run_demo(&mut device, duration).await?;
}
Commands::On => {
if !device.is_on {
device.power_on().await?;
info!("Device powered on");
}
}
Commands::Off => {
if device.is_on {
device.power_off().await?;
info!("Device powered off");
}
}
Commands::Red => {
if !device.is_on {
device.power_on().await?;
}
device.set_color(255, 0, 0).await?;
info!("Color set to RED");
}
Commands::Green => {
if !device.is_on {
device.power_on().await?;
}
device.set_color(0, 255, 0).await?;
info!("Color set to GREEN");
}
Commands::Blue => {
if !device.is_on {
device.power_on().await?;
}
device.set_color(0, 0, 255).await?;
info!("Color set to BLUE");
}
Commands::White => {
if !device.is_on {
device.power_on().await?;
}
device.set_color(255, 255, 255).await?;
info!("Color set to WHITE");
}
Commands::Brightness { level } => {
if !device.is_on {
device.power_on().await?;
}
device.set_brightness(level).await?;
info!("Brightness set to {}", level);
}
Commands::ColorTemp { kelvin } => {
if !device.is_on {
device.power_on().await?;
}
device.set_color_temp_kelvin(kelvin).await?;
info!("Color temperature set to {}K", kelvin);
}
Commands::Color { red, green, blue } => {
if !device.is_on {
device.power_on().await?;
}
device.set_color(red, green, blue).await?;
info!("Color set to RGB({}, {}, {})", red, green, blue);
}
Commands::Effect { effect_type, speed } => {
if !device.is_on {
device.power_on().await?;
}
let effect_code = match effect_type {
EffectType::Rainbow => EFFECTS.crossfade_red_green_blue_yellow_cyan_magenta_white,
EffectType::Jump => EFFECTS.jump_red_green_blue,
EffectType::JumpAll => EFFECTS.jump_red_green_blue_yellow_cyan_magenta_white,
EffectType::CrossfadeRed => EFFECTS.crossfade_red,
EffectType::CrossfadeGreen => EFFECTS.crossfade_green,
EffectType::CrossfadeBlue => EFFECTS.crossfade_blue,
EffectType::CrossfadeRgb => EFFECTS.crossfade_red_green_blue,
EffectType::Blink => EFFECTS.blink_red_green_blue_yellow_cyan_magenta_white,
EffectType::BlinkRed => EFFECTS.blink_red,
EffectType::BlinkGreen => EFFECTS.blink_green,
EffectType::BlinkBlue => EFFECTS.blink_blue,
};
device.set_effect(effect_code).await?;
device.set_effect_speed(speed).await?;
info!("Effect set to {} with speed {}", effect_type, speed);
}
Commands::ScheduleOn { hour, minute, days } => {
if !device.is_on {
device.power_on().await?;
}
let days_value = parse_days(&days);
device
.set_schedule_on(days_value, hour, minute, true)
.await?;
info!(
"Schedule set to turn on at {:02}:{:02} on {}",
hour, minute, days
);
}
Commands::ScheduleOff { hour, minute, days } => {
if !device.is_on {
device.power_on().await?;
}
let days_value = parse_days(&days);
device
.set_schedule_off(days_value, hour, minute, true)
.await?;
info!(
"Schedule set to turn off at {:02}:{:02} on {}",
hour, minute, days
);
}
Commands::Audio {
mode,
range,
sensitivity,
update_ms,
test,
device: audio_device,
} => {
if !device.is_on {
device.power_on().await?;
}
run_audio_visualization(
&mut device,
mode,
range,
sensitivity,
update_ms,
test,
audio_device,
)
.await?;
}
}
info!("Command completed successfully");
Ok(())
}
#[instrument]
fn parse_days(days: &str) -> u8 {
debug!("Parsing days string: {}", days);
let result = match days.to_lowercase().as_str() {
"mon" | "monday" => WEEK_DAYS.monday,
"tue" | "tuesday" => WEEK_DAYS.tuesday,
"wed" | "wednesday" => WEEK_DAYS.wednesday,
"thu" | "thursday" => WEEK_DAYS.thursday,
"fri" | "friday" => WEEK_DAYS.friday,
"sat" | "saturday" => WEEK_DAYS.saturday,
"sun" | "sunday" => WEEK_DAYS.sunday,
"all" => WEEK_DAYS.all,
"weekdays" => WEEK_DAYS.week_days,
"weekend" => WEEK_DAYS.weekend_days,
_ => {
debug!("Parsing composite days string");
let mut combined = 0;
for day in days.split(',') {
let day_value = parse_days(day);
debug!(" Day '{}' = {:#04x}", day, day_value);
combined |= day_value;
}
combined
}
};
trace!("Days '{}' parsed to bitmask: {:#04x}", days, result);
result
}
#[instrument]
async fn sleep(seconds: u64) {
trace!("Sleeping for {}s", seconds);
tokio::time::sleep(Duration::from_secs(seconds)).await;
trace!("Sleep completed");
}
#[instrument(skip(device))]
async fn run_audio_visualization(
device: &mut BleLedDevice,
mode: AudioModeType,
range: AudioRangeType,
sensitivity: u8,
update_ms: u32,
test: bool,
audio_device: Option<String>,
) -> Result<()> {
info!("Initializing audio monitoring in {:?} mode", mode);
let audio_monitor = match AudioMonitor::new_with_device(audio_device) {
Ok(monitor) => monitor,
Err(e) => {
error!("Failed to initialize audio monitoring: {}", e);
return Err(e.into());
}
};
let mut config = audio_monitor.get_config();
config.mode = mode.clone().into();
config.range = range.into();
config.sensitivity = sensitivity as f32 / 100.0; config.update_interval_ms = update_ms;
audio_monitor.set_config(config);
info!("Starting audio visualization. Press Ctrl+C to exit.");
let ctrl_c = tokio::signal::ctrl_c();
tokio::select! {
result = audio_monitor.start_continuous_monitoring(device) => {
if let Err(e) = result {
error!("Audio monitoring error: {}", e);
return Err(e.into());
}
}
_ = ctrl_c => {
info!("Received Ctrl+C, stopping audio visualization");
}
}
audio_monitor.stop();
device.power_off().await?;
info!("Audio visualization stopped");
Ok(())
}
#[instrument(skip(device))]
async fn run_demo(device: &mut BleLedDevice, duration: u64) -> Result<()> {
info!("Running LED strip demo with {}s intervals", duration);
info!("Turning LEDs on");
device.power_on().await?;
sleep(duration).await;
info!("Setting color to red");
device.set_color(255, 0, 0).await?; sleep(duration).await;
info!("Setting color to green");
device.set_color(0, 255, 0).await?; sleep(duration).await;
info!("Setting color to blue");
device.set_color(0, 0, 255).await?; sleep(duration).await;
info!("Setting brightness to 50%");
device.set_brightness(50).await?;
sleep(duration).await;
info!("Setting brightness to 100%");
device.set_brightness(100).await?;
sleep(duration).await;
info!("Setting warm white (2700K)");
device.set_color_temp_kelvin(2700).await?;
sleep(duration).await;
info!("Setting cool white (6500K)");
device.set_color_temp_kelvin(6500).await?;
sleep(duration).await;
info!("Setting rainbow crossfade effect");
device
.set_effect(EFFECTS.crossfade_red_green_blue_yellow_cyan_magenta_white)
.await?;
sleep(duration).await;
info!("Setting RGB jump effect");
device.set_effect(EFFECTS.jump_red_green_blue).await?;
sleep(duration).await;
info!("Setting RGB blink effect");
device
.set_effect(EFFECTS.blink_red_green_blue_yellow_cyan_magenta_white)
.await?;
sleep(duration).await;
info!("Setting effect speed to slow (20)");
device.set_effect_speed(20).await?;
sleep(duration).await;
info!("Setting effect speed to fast (80)");
device.set_effect_speed(80).await?;
sleep(duration).await;
info!("Back to static white");
device.set_color(255, 255, 255).await?;
sleep(1).await;
info!("Turning LEDs off to end demo");
device.power_off().await?;
info!("Demo completed!");
Ok(())
}