502 lines
16 KiB
Rust
502 lines
16 KiB
Rust
/*!
|
||
Library, and CLI, for [Luxafor](https://luxafor.com/products/) lights via either USB or webhooks.
|
||
|
||
The main entry point for clients is the trait [Device](trait.Device.html) that has implementations
|
||
for USB connected devices such as the [flag](https://luxafor.com/flag-usb-busylight-availability-indicator/)
|
||
as well as webhooks for both the flag and [bluetooth](https://luxafor.com/bluetooth-busy-light-availability-indicator/)
|
||
lights.
|
||
|
||
Each connection has its own discovery or connection methods but will provide a `Device` implementation
|
||
for the manipulation of the light state.
|
||
|
||
# API Examples
|
||
|
||
The following example shows a function that sets the light to a solid red color. It demonstrates
|
||
the use of a USB connected device.
|
||
|
||
```rust,ignore
|
||
use luxafor::usb_hid::USBDeviceDiscovery;
|
||
use luxafor::{Device, SolidColor, TargetedDevice};
|
||
use luxafor::error::Result;
|
||
|
||
fn set_do_not_disturb() -> Result<()> {
|
||
let discovery = USBDeviceDiscovery::new()?;
|
||
let device = discovery.device()?;
|
||
println!("USB device: '{}'", device.id());
|
||
device.set_specific_led(SpecificLED::AllFront);
|
||
device.set_solid_color(SolidColor::Red, false)
|
||
}
|
||
```
|
||
|
||
The following shows the same function but using the webhook connection. Note that the webhook API
|
||
is more limited in the features it exposes; it does not support `set_specific_led` for a start.
|
||
|
||
```rust,ignore
|
||
use luxafor::webhook::new_device_for;
|
||
use luxafor::{Device, SolidColor};
|
||
use luxafor::error::Result;
|
||
|
||
fn set_do_not_disturb(device_id: &str) -> Result<()> {
|
||
let device = new_device_for(device_id)?;
|
||
println!("Webhook device: '{}'", device.id());
|
||
device.set_solid_color(SolidColor::Red, false)
|
||
}
|
||
```
|
||
|
||
# CLI Examples
|
||
|
||
The following shows the command line tool setting the color to red.
|
||
|
||
```bash
|
||
❯ lux -d 2a0f2c73b72 solid red
|
||
```
|
||
|
||
The following shows the command line tool setting the color to a blinking green. This example uses the environment
|
||
variable `LUX_DEVICE` to save repeating the device identifier on each call.
|
||
|
||
```bash
|
||
❯ export LUX_DEVICE=2a0f2c73b72
|
||
❯ lux blink green
|
||
```
|
||
|
||
The following shows the command line tool turning the light off.
|
||
|
||
```bash
|
||
❯ lux -vvv -d 2a0f2c73b72 off
|
||
INFO luxafor > Setting the color of device '2a0f2c73b72e' to 000000
|
||
INFO luxafor > call successful
|
||
```
|
||
|
||
The following shows the how to set USB connected lights.
|
||
|
||
```bash
|
||
❯ lux -d usb solid red
|
||
```
|
||
|
||
# Features
|
||
|
||
* **command-line**; provides the command line tool `lux`, it is not on by default for library clients.
|
||
* **usb**; provides access to USB connected devices.
|
||
* **webhook** (default); provides access to USB, or Bluetooth, devices via webhooks.
|
||
|
||
*/
|
||
|
||
#![warn(
|
||
// ---------- Stylistic
|
||
future_incompatible,
|
||
nonstandard_style,
|
||
rust_2018_idioms,
|
||
trivial_casts,
|
||
trivial_numeric_casts,
|
||
// ---------- Public
|
||
missing_debug_implementations,
|
||
missing_docs,
|
||
unreachable_pub,
|
||
// ---------- Unsafe
|
||
unsafe_code,
|
||
// ---------- Unused
|
||
unused_extern_crates,
|
||
unused_import_braces,
|
||
unused_qualifications,
|
||
unused_results,
|
||
)]
|
||
|
||
#[macro_use]
|
||
extern crate error_chain;
|
||
|
||
#[allow(unused_imports)]
|
||
#[macro_use]
|
||
extern crate log;
|
||
|
||
use std::fmt::{Display, Formatter};
|
||
use std::str::FromStr;
|
||
|
||
// ------------------------------------------------------------------------------------------------
|
||
// Public Types
|
||
// ------------------------------------------------------------------------------------------------
|
||
|
||
///
|
||
/// A color that the light can be set to.
|
||
///
|
||
#[derive(Clone, Debug)]
|
||
pub enum SolidColor {
|
||
/// A preset color
|
||
Red,
|
||
/// A preset color
|
||
Green,
|
||
/// A preset color
|
||
Yellow,
|
||
/// A preset color
|
||
Blue,
|
||
/// A preset color
|
||
White,
|
||
/// A preset color
|
||
Cyan,
|
||
/// A preset color
|
||
Magenta,
|
||
/// A custom color using standard RGB values
|
||
Custom {
|
||
/// The _red_ channel
|
||
red: u8,
|
||
/// The _green_ channel
|
||
green: u8,
|
||
/// The _blue_ channel
|
||
blue: u8,
|
||
},
|
||
}
|
||
|
||
///
|
||
/// Waves produce a pattern that starts at the bottom of the light, fills the light and then
|
||
/// fades out at the top.
|
||
///
|
||
#[derive(Clone, Debug)]
|
||
pub enum Wave {
|
||
/// A short transition, completed before the next wave starts.
|
||
Short,
|
||
/// A long transition, completed before the next wave starts.
|
||
Long,
|
||
/// A short transition, which _does not_ complete before the next wave starts.
|
||
OverlappingShort,
|
||
/// A long transition, which _does not_ complete before the next wave starts.
|
||
OverlappingLong,
|
||
}
|
||
|
||
///
|
||
/// A pattern the light can be set to show.
|
||
///
|
||
#[derive(Clone, Debug)]
|
||
pub enum Pattern {
|
||
/// A preset pattern that cycles between red and blue.
|
||
Police,
|
||
/// A preset pattern that cycles between green,. yellow, and red.
|
||
TrafficLights,
|
||
/// Preset random patterns
|
||
Random(u8),
|
||
/// A preset pattern
|
||
#[cfg(target_os = "windows")]
|
||
Rainbow,
|
||
/// A preset pattern
|
||
#[cfg(target_os = "windows")]
|
||
Sea,
|
||
/// A preset pattern
|
||
#[cfg(target_os = "windows")]
|
||
WhiteWave,
|
||
/// A preset pattern
|
||
#[cfg(target_os = "windows")]
|
||
Synthetic,
|
||
}
|
||
|
||
///
|
||
/// A trait implemented by different access methods to control a light.
|
||
///
|
||
pub trait Device {
|
||
///
|
||
/// Return the identifier for the device.
|
||
///
|
||
fn id(&self) -> String;
|
||
|
||
///
|
||
/// Turn the light off.
|
||
///
|
||
fn turn_off(&self) -> error::Result<()>;
|
||
|
||
///
|
||
/// Set the light to a continuous solid color.
|
||
///
|
||
fn set_solid_color(&self, color: SolidColor) -> error::Result<()>;
|
||
|
||
///
|
||
/// Set the light to fade from its current color to a new one.
|
||
///
|
||
fn set_fade_to_color(&self, color: SolidColor, fade_duration: u8) -> error::Result<()>;
|
||
|
||
///
|
||
/// Strobe the light, this will dim and brighten the same color.
|
||
///
|
||
fn set_color_strobe(
|
||
&self,
|
||
color: SolidColor,
|
||
strobe_speed: u8,
|
||
repeat_count: u8,
|
||
) -> error::Result<()>;
|
||
|
||
///
|
||
/// Set the light to repeat one of a pre-defined set of wave patterns.
|
||
///
|
||
fn set_color_wave(
|
||
&self,
|
||
color: SolidColor,
|
||
wave_pattern: Wave,
|
||
wave_speed: u8,
|
||
repeat_count: u8,
|
||
) -> error::Result<()>;
|
||
|
||
///
|
||
/// Set the light to repeat one of a pre-defined set of patterns.
|
||
///
|
||
fn set_pattern(&self, pattern: Pattern, repeat_count: u8) -> error::Result<()>;
|
||
}
|
||
|
||
///
|
||
/// Denotes which LED in the light should be the target of any device operations.
|
||
///
|
||
#[derive(Clone, Debug)]
|
||
pub enum SpecificLED {
|
||
/// All supported LEDs
|
||
All,
|
||
/// Only the LEDs on the front (tab) of the light
|
||
AllFront,
|
||
/// Only the LEDs on the back of the light
|
||
AllBack,
|
||
/// Only one specific LED (value: 1..6)
|
||
Number(u8),
|
||
}
|
||
|
||
///
|
||
/// Extension trait to allow targeting specific LEDs on the device.
|
||
///
|
||
pub trait TargetedDevice: Device {
|
||
/// Set the LED to be used for future operations.
|
||
fn set_specific_led(&mut self, led: SpecificLED) -> error::Result<()>;
|
||
}
|
||
|
||
// ------------------------------------------------------------------------------------------------
|
||
// Implementations
|
||
// ------------------------------------------------------------------------------------------------
|
||
|
||
impl Display for SolidColor {
|
||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||
fn to_hex(v: &u8) -> String {
|
||
format!("{:#04x}", v)[2..].to_string()
|
||
}
|
||
write!(
|
||
f,
|
||
"{}",
|
||
match self {
|
||
SolidColor::Red => "red".to_string(),
|
||
SolidColor::Green => "green".to_string(),
|
||
SolidColor::Yellow => "yellow".to_string(),
|
||
SolidColor::Blue => "blue".to_string(),
|
||
SolidColor::White => "white".to_string(),
|
||
SolidColor::Cyan => "cyan".to_string(),
|
||
SolidColor::Magenta => "magenta".to_string(),
|
||
SolidColor::Custom { red, green, blue } =>
|
||
format!("{}{}{}", to_hex(red), to_hex(green), to_hex(blue)),
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
impl FromStr for SolidColor {
|
||
type Err = error::Error;
|
||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
let s = s.to_lowercase();
|
||
match s.as_str() {
|
||
"red" => Ok(SolidColor::Red),
|
||
"green" => Ok(SolidColor::Green),
|
||
"yellow" => Ok(SolidColor::Yellow),
|
||
"blue" => Ok(SolidColor::Blue),
|
||
"white" => Ok(SolidColor::White),
|
||
"cyan" => Ok(SolidColor::Cyan),
|
||
"magenta" => Ok(SolidColor::Magenta),
|
||
_ => {
|
||
if s.len() == 6 && s.chars().all(|c| c.is_ascii_hexdigit()) {
|
||
Ok(SolidColor::Custom {
|
||
red: u8::from_str_radix(&s[0..1], 16)?,
|
||
green: u8::from_str_radix(&s[2..3], 16)?,
|
||
blue: u8::from_str_radix(&s[4..5], 16)?,
|
||
})
|
||
} else {
|
||
Err(error::ErrorKind::InvalidColor.into())
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------------------------------------------
|
||
|
||
impl Display for Wave {
|
||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||
write!(
|
||
f,
|
||
"{}",
|
||
match self {
|
||
Wave::Short => "short",
|
||
Wave::Long => "long",
|
||
Wave::OverlappingShort => "overlapping short",
|
||
Wave::OverlappingLong => "overlapping long",
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
impl FromStr for Wave {
|
||
type Err = error::Error;
|
||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
let s = s.to_lowercase();
|
||
match s.as_str() {
|
||
"short" => Ok(Wave::Short),
|
||
"long" => Ok(Wave::Long),
|
||
"overlapping short" => Ok(Wave::OverlappingShort),
|
||
"overlapping long" => Ok(Wave::OverlappingLong),
|
||
_ => Err(error::ErrorKind::InvalidPattern.into()),
|
||
}
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------------------------------------------
|
||
|
||
impl Display for Pattern {
|
||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||
write!(
|
||
f,
|
||
"{}",
|
||
match self {
|
||
Pattern::Police => "police".to_string(),
|
||
Pattern::TrafficLights => "traffic lights".to_string(),
|
||
Pattern::Random(n) => format!("random {}", n),
|
||
#[cfg(target_os = "windows")]
|
||
Pattern::Rainbow => "rainbow".to_string(),
|
||
#[cfg(target_os = "windows")]
|
||
Pattern::Sea => "sea".to_string(),
|
||
#[cfg(target_os = "windows")]
|
||
Pattern::WhiteWave => "white wave".to_string(),
|
||
#[cfg(target_os = "windows")]
|
||
Pattern::Synthetic => "synthetic".to_string(),
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
impl FromStr for Pattern {
|
||
type Err = error::Error;
|
||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
let s = s.to_lowercase();
|
||
match s.as_str() {
|
||
"police" => Ok(Pattern::Police),
|
||
"traffic lights" => Ok(Pattern::TrafficLights),
|
||
"random 1" => Ok(Pattern::Random(1)),
|
||
"random 2" => Ok(Pattern::Random(2)),
|
||
"random 3" => Ok(Pattern::Random(3)),
|
||
"random 4" => Ok(Pattern::Random(4)),
|
||
"random 5" => Ok(Pattern::Random(5)),
|
||
#[cfg(target_os = "windows")]
|
||
"rainbow" => Ok(Pattern::Rainbow),
|
||
#[cfg(target_os = "windows")]
|
||
"sea" => Ok(Pattern::Sea),
|
||
#[cfg(target_os = "windows")]
|
||
"white wave" => Ok(Pattern::WhiteWave),
|
||
#[cfg(target_os = "windows")]
|
||
"synthetic" => Ok(Pattern::Synthetic),
|
||
_ => Err(error::ErrorKind::InvalidPattern.into()),
|
||
}
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------------------------------------------
|
||
|
||
impl Display for SpecificLED {
|
||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||
write!(
|
||
f,
|
||
"{}",
|
||
match self {
|
||
SpecificLED::All => "all".to_string(),
|
||
SpecificLED::AllFront => "front".to_string(),
|
||
SpecificLED::AllBack => "back".to_string(),
|
||
SpecificLED::Number(n) => n.to_string(),
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
impl FromStr for SpecificLED {
|
||
type Err = error::Error;
|
||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
let s = s.to_lowercase();
|
||
match s.as_str() {
|
||
"all" => Ok(SpecificLED::All),
|
||
"front" => Ok(SpecificLED::AllFront),
|
||
"back" => Ok(SpecificLED::AllBack),
|
||
"1" => Ok(SpecificLED::Number(1)),
|
||
"2" => Ok(SpecificLED::Number(2)),
|
||
"3" => Ok(SpecificLED::Number(3)),
|
||
"4" => Ok(SpecificLED::Number(4)),
|
||
"5" => Ok(SpecificLED::Number(5)),
|
||
"6" => Ok(SpecificLED::Number(6)),
|
||
_ => Err(error::ErrorKind::InvalidLED.into()),
|
||
}
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------------------------------------------
|
||
// Modules
|
||
// ------------------------------------------------------------------------------------------------
|
||
|
||
///
|
||
/// Error handling types.
|
||
///
|
||
#[allow(missing_docs)]
|
||
pub mod error {
|
||
error_chain! {
|
||
errors {
|
||
#[doc("The color value supplied was not recognized")]
|
||
InvalidColor {
|
||
description("The color value supplied was not recognized")
|
||
display("The color value supplied was not recognized")
|
||
}
|
||
#[doc("The pattern value supplied was not recognized")]
|
||
InvalidPattern {
|
||
description("The pattern value supplied was not recognized")
|
||
display("The pattern value supplied was not recognized")
|
||
}
|
||
#[doc("The LED number is either invalid or not supported by the connected device")]
|
||
InvalidLED {
|
||
description("The LED number is either invalid or not supported by the connected device")
|
||
display("The LED number is either invalid or not supported by the connected device")
|
||
}
|
||
#[doc("The provided device ID was incorrectly formatted")]
|
||
InvalidDeviceID {
|
||
description("The provided device ID was incorrectly formatted")
|
||
display("The provided device ID was incorrectly formatted")
|
||
}
|
||
#[doc("No device was discovered, or the ID did not resolve to a device")]
|
||
DeviceNotFound {
|
||
description("No device was discovered, or the ID did not resolve to a device")
|
||
display("No device was discovered, or the ID did not resolve to a device")
|
||
}
|
||
#[doc("The server indicated an invalid request")]
|
||
InvalidRequest {
|
||
description("The server indicated an invalid request")
|
||
display("The server indicated an invalid request")
|
||
}
|
||
#[doc("An unexpected HTTP error was returned")]
|
||
UnexpectedError(sc: u16) {
|
||
description("An unexpected HTTP error was returned")
|
||
display("An unexpected HTTP error was returned: {}", sc)
|
||
}
|
||
#[doc("The command is not supported by the current device, or connection to the device")]
|
||
UnsupportedCommand {
|
||
description("The command is not supported by the current device, or connection to the device")
|
||
display("The command is not supported by the current device, or connection to the device")
|
||
}
|
||
}
|
||
foreign_links {
|
||
CustomFmt(::std::num::ParseIntError);
|
||
Request(::reqwest::Error);
|
||
Fmt(::std::fmt::Error);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(feature = "usb")]
|
||
pub mod usb_hid;
|
||
|
||
#[cfg(feature = "webhook")]
|
||
pub mod webhook;
|