use anyhow::{bail, Context, Result};
use blink1rs::{Blink1, Color, Led, OnTimeout};
use clap::{Parser, Subcommand};
use std::time::Duration;
#[derive(Parser)]
#[command(name = "blink1rs", version, about = "Control a blink(1) USB RGB LED")]
struct Cli {
#[arg(short, long, global = true, conflicts_with = "serial")]
device: Option<usize>,
#[arg(short, long, global = true)]
serial: Option<String>,
#[arg(long, global = true)]
gamma: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
List,
Info,
Set {
color: String,
#[arg(short, long)]
fade: Option<String>,
#[arg(short, long, default_value_t = 0)]
led: u8,
},
Off,
Blink {
color: String,
#[arg(short, long, default_value_t = 3)]
count: u32,
#[arg(short, long, default_value = "200ms")]
rate: String,
},
Play,
Stop,
Watchdog {
#[arg(short, long, default_value = "60s")]
timeout: String,
#[arg(short, long)]
color: Option<String>,
#[arg(long)]
off: bool,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
if let Command::List = cli.command {
let found = Blink1::list().context("enumerating USB HID devices")?;
if found.is_empty() {
println!("no blink(1) found");
#[cfg(target_os = "linux")]
println!("on Linux this usually means the udev rule is missing; see the README");
}
for (i, d) in found.iter().enumerate() {
println!("{i}: {} ({})", d.serial, d.kind);
}
return Ok(());
}
let mut b = open(&cli)?;
b.set_gamma(cli.gamma);
match cli.command {
Command::List => unreachable!("handled above"),
Command::Info => {
println!("serial: {}", b.serial());
println!("hardware: {}", b.kind());
println!("patterns: {} lines", b.pattern_max());
match b.firmware_version() {
Ok(v) => println!("firmware: {}.{}", v / 100, v % 100),
Err(e) => println!("firmware: unavailable ({e})"),
}
if b.kind().can_read_rgb() {
match b.read_rgb(Led::All) {
Ok(c) => println!("color: {c}"),
Err(e) => println!("color: unavailable ({e})"),
}
}
}
Command::Set { color, fade, led } => {
let c = parse_color(&color)?;
let led = if led == 0 { Led::All } else { Led::N(led) };
match fade {
Some(f) => b.fade_led(c, parse_duration(&f)?, led)?,
None if led == Led::All => b.set(c)?,
None => b.fade_led(c, Duration::ZERO, led)?,
}
}
Command::Off => b.off()?,
Command::Blink { color, count, rate } => {
let c = parse_color(&color)?;
let rate = parse_duration(&rate)?;
for _ in 0..count {
b.set(c)?;
std::thread::sleep(rate);
b.off()?;
std::thread::sleep(rate);
}
}
Command::Play => b.play()?,
Command::Stop => b.stop()?,
Command::Watchdog {
timeout,
color,
off,
} => {
if off {
b.watchdog_disable()?;
println!("watchdog disarmed");
} else {
let timeout = parse_duration(&timeout)?;
let action = match &color {
Some(s) => {
b.write_pattern_line(0, parse_color(s)?, Duration::ZERO, Led::All)?;
OnTimeout::PlayPattern { start: 0, end: 0 }
}
None => OnTimeout::Off,
};
b.watchdog_enable(timeout, action)?;
let what = color.as_deref().unwrap_or("off");
println!("watchdog armed: {what} in {timeout:.1?} unless re-armed");
}
}
}
Ok(())
}
fn open(cli: &Cli) -> Result<Blink1> {
let b = match (&cli.serial, cli.device) {
(Some(s), _) => Blink1::open_serial(s).with_context(|| format!("opening serial {s}")),
(None, Some(n)) => Blink1::open_index(n).with_context(|| format!("opening device {n}")),
(None, None) => Blink1::open().context("opening the first blink(1)"),
};
b.map_err(|e| {
if cfg!(target_os = "linux") {
e.context("on Linux a missing udev rule hides the device; see the README")
} else {
e
}
})
}
fn parse_color(s: &str) -> Result<Color> {
s.parse().map_err(anyhow::Error::from)
}
fn parse_duration(s: &str) -> Result<Duration> {
let s = s.trim();
let (num, scale) = if let Some(n) = s.strip_suffix("ms") {
(n, 1.0)
} else if let Some(n) = s.strip_suffix('s') {
(n, 1000.0)
} else {
(s, 1.0)
};
let v: f64 = num
.trim()
.parse()
.with_context(|| format!("not a duration: {s:?}"))?;
if !v.is_finite() || v < 0.0 {
bail!("not a duration: {s:?}");
}
Ok(Duration::from_millis((v * scale) as u64))
}