diff --git a/Cargo.toml b/Cargo.toml index 79203af..d801362 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,8 @@ targets = ["x86_64-unknown-linux-gnu"] all-features = true [features] -default = ["webhook"] usb = ["hidapi"] -webhook = ["reqwest"] -command-line = ["pretty_env_logger", "structopt", "usb", "webhook"] +webhook = ["ureq"] [[bin]] name = "lux" @@ -26,10 +24,10 @@ required-features = ["command-line"] [dependencies] log = "0.4.11" -error-chain = "0.12.2" +thiserror = "1.0.56" + #[feature-dependencies] -hidapi = { version = "1.2.3", optional = true } -pretty_env_logger = { version = "0.4.0", optional = true } -reqwest = { version = "0.11", features = ["blocking"], optional = true } -structopt = { version = "0.3.14", optional = true } +hidapi = { version = "2.4.1", optional = true } +ureq = { version = "2.9.1", optional = true } +clap = { version = "4.4.18", optional = true, features = ["derive"] } diff --git a/src/lib.rs b/src/lib.rs index 8dd1e2f..feba437 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,12 +101,7 @@ The following shows the how to set USB connected lights. unused_results, )] -#[macro_use] -extern crate error_chain; - -#[allow(unused_imports)] -#[macro_use] -extern crate log; +use thiserror::Error; use std::fmt::{Display, Formatter}; use std::str::FromStr; @@ -198,17 +193,17 @@ pub trait Device { /// /// Turn the light off. /// - fn turn_off(&self) -> error::Result<()>; + fn turn_off(&self) -> Result<(),()>; /// /// Set the light to a continuous solid color. /// - fn set_solid_color(&self, color: SolidColor) -> error::Result<()>; + fn set_solid_color(&self, color: SolidColor) -> 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<()>; + fn set_fade_to_color(&self, color: SolidColor, fade_duration: u8) -> Result<(),()>; /// /// Strobe the light, this will dim and brighten the same color. @@ -218,7 +213,7 @@ pub trait Device { color: SolidColor, strobe_speed: u8, repeat_count: u8, - ) -> error::Result<()>; + ) -> Result<(),()>; /// /// Set the light to repeat one of a pre-defined set of wave patterns. @@ -229,12 +224,12 @@ pub trait Device { wave_pattern: Wave, wave_speed: u8, repeat_count: u8, - ) -> error::Result<()>; + ) -> 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<()>; + fn set_pattern(&self, pattern: Pattern, repeat_count: u8) -> Result<(),()>; } /// @@ -252,12 +247,16 @@ pub enum SpecificLED { Number(u8), } +#[derive(Debug,Error)] +#[error("invalid led id {0}")] +pub struct InvalidLed(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<()>; + fn set_specific_led(&mut self, led: SpecificLED) -> Result<(),InvalidLed>; } // ------------------------------------------------------------------------------------------------ @@ -287,31 +286,33 @@ impl Display for SolidColor { } } +#[derive(Debug,Error)] +#[error("invalid color string `{0}`")] +struct InvalidColor(String); + impl FromStr for SolidColor { - type Err = error::Error; + type Err = InvalidColor; fn from_str(s: &str) -> Result { 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()) - } - } - } + let solid_color = match &*s { + "red" => SolidColor::Red, + "green" => SolidColor::Green, + "yellow" => SolidColor::Yellow, + "blue" => SolidColor::Blue, + "white" => SolidColor::White, + "cyan" => SolidColor::Cyan, + "magenta" => SolidColor::Magenta, + + hex if hex.len() == 6 && hex.chars().all(|c| c.is_ascii_hexdigit()) => + SolidColor::Custom { + red: u8::from_str_radix(&s[0..1], 16).unwrap(), + green: u8::from_str_radix(&s[2..3], 16).unwrap(), + blue: u8::from_str_radix(&s[4..5], 16).unwrap(), + }, + _ => return Err(InvalidColor(s)) + }; + Ok(solid_color) } } @@ -332,8 +333,12 @@ impl Display for Wave { } } +#[derive(Debug, Error)] +#[error("invalid pattern `{0}`")] +struct InvalidPattern(String); + impl FromStr for Wave { - type Err = error::Error; + type Err = InvalidPattern; fn from_str(s: &str) -> Result { let s = s.to_lowercase(); @@ -342,7 +347,7 @@ impl FromStr for Wave { "long" => Ok(Wave::Long), "overlapping short" => Ok(Wave::OverlappingShort), "overlapping long" => Ok(Wave::OverlappingLong), - _ => Err(error::ErrorKind::InvalidPattern.into()), + _ => Err(InvalidPattern(s)), } } } @@ -372,7 +377,7 @@ impl Display for Pattern { } impl FromStr for Pattern { - type Err = error::Error; + type Err = InvalidPattern; fn from_str(s: &str) -> Result { let s = s.to_lowercase(); @@ -392,7 +397,7 @@ impl FromStr for Pattern { "white wave" => Ok(Pattern::WhiteWave), #[cfg(target_os = "windows")] "synthetic" => Ok(Pattern::Synthetic), - _ => Err(error::ErrorKind::InvalidPattern.into()), + _ => Err(InvalidPattern(s)), } } } @@ -414,8 +419,16 @@ impl Display for SpecificLED { } } +#[derive(Debug,Error)] +pub enum ParseLedError { + #[error("parse error: {0}")] + ParseIntError(#[from] std::num::ParseIntError), + #[error("invalid led: {0}")] + InvalidLed(#[from] InvalidLed), +} + impl FromStr for SpecificLED { - type Err = error::Error; + type Err = ParseLedError; fn from_str(s: &str) -> Result { let s = s.to_lowercase(); @@ -423,13 +436,14 @@ impl FromStr for SpecificLED { "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()), + num => { + match num.parse()? { + n@1..=6 => Ok(SpecificLED::Number(n)), + invalid => Err(ParseLedError::InvalidLed( + InvalidLed(invalid) + )) + } + } } } } @@ -443,56 +457,33 @@ impl FromStr for SpecificLED { /// #[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); - } - } + use thiserror::Error; + + #[derive(Debug, Error)] + #[error("invalid device ID")] + struct InvalidDeviceID {} + + #[derive(Debug, Error)] + #[error("device not found")] + struct DeviceNotFound; + + + //#[derive(Error)] + //#[error("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) + //} + #[derive(Debug, Error)] + #[error("unsupported command")] + struct UnsupportedCommand; } + #[cfg(feature = "usb")] pub mod usb_hid; diff --git a/src/usb_hid.rs b/src/usb_hid.rs index 5314fcd..10badce 100644 --- a/src/usb_hid.rs +++ b/src/usb_hid.rs @@ -104,9 +104,13 @@ The serial number is returned as a pair, (high,low) bytes. */ -use crate::{Device, Pattern, SolidColor, SpecificLED, TargetedDevice, Wave}; +use crate::{Device, Pattern, SolidColor, SpecificLED, TargetedDevice, Wave, InvalidLed}; use hidapi::{HidApi, HidDevice}; +pub use hidapi::HidError; + +use log::{error, info, trace}; + // ------------------------------------------------------------------------------------------------ // Public Types // ------------------------------------------------------------------------------------------------ @@ -184,28 +188,25 @@ impl USBDeviceDiscovery { /// /// Construct a new discovery object, this initializes the USB HID interface and thus can fail. /// - pub fn new() -> crate::error::Result { - match HidApi::new() { - Ok(hid_api) => Ok(Self { hid_api }), - Err(err) => { - error!("Could not connect to USB, error: {:?}", err); - Err(crate::error::ErrorKind::DeviceNotFound.into()) - } - } + pub fn new() -> Result { + HidApi::new() + .map(|hid_api| Self { hid_api }) + .map_err(|err| { + log::error!("could not open HID api: {0}", err); + err + }) } /// /// Return a device, if found, that corresponds to a Luxafor light. /// - pub fn device(&self) -> crate::error::Result { - let result = self.hid_api.open(LUXAFOR_VENDOR_ID, LUXAFOR_PRODUCT_ID); - match result { - Ok(hid_device) => USBDevice::new(hid_device), - Err(err) => { - error!("Could not open HID device: {:?}", err); - Err(crate::error::ErrorKind::DeviceNotFound.into()) - } - } + pub fn device(&self) -> Result { + self.hid_api.open(LUXAFOR_VENDOR_ID, LUXAFOR_PRODUCT_ID) + .map(|dev| USBDevice::new(dev)) + .map_err(|err| { + log::error!("could not open device: {err}"); + err + }) } } @@ -216,18 +217,18 @@ impl Device for USBDevice { self.id.clone() } - fn turn_off(&self) -> crate::error::Result<()> { + fn turn_off(&self) -> Result<(),HidError> { info!("Turning device '{}' off", self.id); self.write(&[HID_REPORT_ID, MODE_SIMPLE, SIMPLE_COLOR_OFF]) } - fn set_solid_color(&self, color: SolidColor) -> crate::error::Result<()> { + fn set_solid_color(&self, color: SolidColor) -> Result<(),HidError> { info!("Setting the color of device '{}' to {}", self.id, color); let (r, g, b) = self.color_to_bytes(color); self.write(&[HID_REPORT_ID, MODE_SOLID, self.target_led, r, g, b]) } - fn set_fade_to_color(&self, color: SolidColor, fade_duration: u8) -> crate::error::Result<()> { + fn set_fade_to_color(&self, color: SolidColor, fade_duration: u8) -> Result<(),()> { info!( "Setting the fade-to color of device '{}' to {}, over {}", self.id, color, fade_duration @@ -249,7 +250,7 @@ impl Device for USBDevice { color: SolidColor, strobe_speed: u8, repeat_count: u8, - ) -> crate::error::Result<()> { + ) -> Result<(),()> { info!( "Setting the device '{}' to strobe {}, at {}, {} times", self.id, color, strobe_speed, repeat_count @@ -274,7 +275,7 @@ impl Device for USBDevice { wave_pattern: Wave, wave_speed: u8, repeat_count: u8, - ) -> crate::error::Result<()> { + ) -> Result<(),()> { info!( "Setting the device '{}' to wave {}, at {}, {} times", self.id, color, wave_speed, repeat_count @@ -299,7 +300,7 @@ impl Device for USBDevice { ]) } - fn set_pattern(&self, pattern: Pattern, repeat_count: u8) -> crate::error::Result<()> { + fn set_pattern(&self, pattern: Pattern, repeat_count: u8) -> Result<(),()> { info!("Setting the pattern of device '{}' to {}", self.id, pattern); let pattern = match pattern { Pattern::Police => PATTERN_POLICE, @@ -325,7 +326,7 @@ impl Device for USBDevice { } impl TargetedDevice for USBDevice { - fn set_specific_led(&mut self, led: SpecificLED) -> crate::error::Result<()> { + fn set_specific_led(&mut self, led: SpecificLED) -> Result<(),InvalidLed> { self.target_led = match led { SpecificLED::All => LED_ALL, SpecificLED::AllFront => LED_FRONT_ALL, @@ -337,7 +338,7 @@ impl TargetedDevice for USBDevice { 4 => LED_BACK_BOTTOM, 5 => LED_BACK_MIDDLE, 6 => LED_BACK_TOP, - _ => return Err(crate::error::ErrorKind::InvalidLED.into()), + other => return Err(InvalidLed(other)), }, }; Ok(()) @@ -345,7 +346,7 @@ impl TargetedDevice for USBDevice { } impl USBDevice { - fn new(hid_device: HidDevice) -> crate::error::Result { + fn new(hid_device: HidDevice) -> USBDevice { let id = format!( "{}::{}::{}", hid_device @@ -361,11 +362,11 @@ impl USBDevice { .unwrap_or(Some("".to_string())) .unwrap_or("".to_string()), ); - Ok(Self { + Self { hid_device, id, target_led: LED_ALL, - }) + } } fn color_to_bytes(&self, color: SolidColor) -> (u8, u8, u8) { @@ -381,7 +382,7 @@ impl USBDevice { } } - fn write(&self, buffer: &[u8]) -> crate::error::Result<()> { + fn write(&self, buffer: &[u8]) -> Result<(),HidError> { trace!( "writing [{:?}]", buffer @@ -390,24 +391,12 @@ impl USBDevice { .collect::>() .join(", ") ); - let result = self.hid_device.write(buffer); - match result { - Ok(bytes_written) => { - if bytes_written == buffer.len() { - Ok(()) - } else { - error!( - "Bytes written, {}, did not match buffer length {}", - bytes_written, - buffer.len() - ); - Err(crate::error::ErrorKind::InvalidRequest.into()) - } - } - Err(err) => { - error!("Could not write to HID device: {:?}", err); - Err(crate::error::ErrorKind::InvalidRequest.into()) - } + let all = buffer.len(); + let sent = self.hid_device.write(buffer)?; + if sent != all { + Err(HidError::IncompleteSendError { sent, all }) + } else { + Ok(()) } } }