Skip to content

Commit

Permalink
Add auto-toggle command that turns light on/off when using a webcam (
Browse files Browse the repository at this point in the history
…#18)

* Add `auto-toggle` command that turns light on/off when using a webcam

This adds a new `auto-toggle` command to the command line interface.
When using that command, `litra` will keep running and monitor the video
device files (`/dev/video*`) using inotify. Opening or closing such a
device will be tracked using a counter to determine if at least one
device is currently opened. If so, the light is turned on, otherwise
it's turned off.

* build: Automatically build `auto-toggle` CLI command on Linux

* Update src/main.rs

---------

Co-authored-by: Tim Rogers <[email protected]>
  • Loading branch information
Holzhaus and timrogers authored Feb 24, 2024
1 parent 362311f commit 56cb6cb
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 1 deletion.
148 changes: 148 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ serde_json = { version = "1.0.113", optional = true }

[features]
default = ["cli"]
cli = ["dep:clap", "dep:serde", "dep:serde_json"]
cli = ["dep:clap", "dep:serde", "dep:serde_json", "dep:inotify"]

[target.'cfg(target_os = "linux")'.dependencies]
inotify = { version = "0.10.2", optional = true }

[[bin]]
name = "litra"
Expand Down
86 changes: 86 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use clap::{ArgGroup, Parser, Subcommand};
#[cfg(target_os = "linux")]
use inotify::{EventMask, Inotify, WatchMask};
use litra::{Device, DeviceError, DeviceHandle, Litra};
use serde::Serialize;
use std::fmt;
Expand Down Expand Up @@ -67,6 +69,25 @@ enum Commands {
#[clap(long, short, action, help = "Return the results in JSON format")]
json: bool,
},
#[cfg(target_os = "linux")]
/// Automatically turn the Logitech Litra device on when your webcam turns on, and off when the webcam turns off
AutoToggle {
#[clap(long, short, help = "The serial number of the Logitech Litra device")]
serial_number: Option<String>,
},
}

#[cfg(target_os = "linux")]
fn get_video_device_paths() -> std::io::Result<Vec<std::path::PathBuf>> {
Ok(std::fs::read_dir("/dev")?
.filter_map(|entry| entry.ok())
.filter_map(|e| {
e.file_name()
.to_str()
.filter(|name| name.starts_with("video"))
.map(|_| e.path())
})
.collect())
}

fn percentage_within_range(percentage: u32, start_range: u32, end_range: u32) -> u32 {
Expand Down Expand Up @@ -105,6 +126,8 @@ fn check_serial_number_if_some(serial_number: Option<&str>) -> impl Fn(&Device)
#[derive(Debug)]
enum CliError {
DeviceError(DeviceError),
#[cfg(target_os = "linux")]
IoError(std::io::Error),
SerializationFailed(serde_json::Error),
BrightnessPrecentageCalculationFailed(TryFromIntError),
DeviceNotFound,
Expand All @@ -114,6 +137,8 @@ impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CliError::DeviceError(error) => error.fmt(f),
#[cfg(target_os = "linux")]
CliError::IoError(error) => write!(f, "Input/Output error: {}", error),
CliError::SerializationFailed(error) => error.fmt(f),
CliError::BrightnessPrecentageCalculationFailed(error) => {
write!(f, "Failed to calculate brightness: {}", error)
Expand All @@ -129,6 +154,13 @@ impl From<DeviceError> for CliError {
}
}

#[cfg(target_os = "linux")]
impl From<std::io::Error> for CliError {
fn from(error: std::io::Error) -> Self {
CliError::IoError(error)
}
}

type CliResult = Result<(), CliError>;

fn get_first_supported_device(
Expand Down Expand Up @@ -277,6 +309,56 @@ fn handle_temperature_command(serial_number: Option<&str>, value: u16) -> CliRes
Ok(())
}

#[cfg(target_os = "linux")]
fn handle_autotoggle_command(serial_number: Option<&str>) -> CliResult {
let context = Litra::new()?;
let device_handle = get_first_supported_device(&context, serial_number)?;

let mut inotify = Inotify::init()?;
for path in get_video_device_paths()? {
match inotify
.watches()
.add(&path, WatchMask::OPEN | WatchMask::CLOSE)
{
Ok(_) => println!("Watching device {}", path.display()),
Err(_) => eprintln!("Failed to watch device {}", path.display()),
}
}

let mut num_devices_open: usize = 0;
loop {
// Read events that were added with `Watches::add` above.
let mut buffer = [0; 1024];
let events = inotify.read_events_blocking(&mut buffer)?;
for event in events {
match event.mask {
EventMask::OPEN => {
match event.name.and_then(std::ffi::OsStr::to_str) {
Some(name) => println!("Video device opened: {}", name),
None => println!("Video device opened"),
}
num_devices_open = num_devices_open.saturating_add(1);
}
EventMask::CLOSE_WRITE | EventMask::CLOSE_NOWRITE => {
match event.name.and_then(std::ffi::OsStr::to_str) {
Some(name) => println!("Video device closed: {}", name),
None => println!("Video device closed"),
}
num_devices_open = num_devices_open.saturating_sub(1);
}
_ => (),
}
}
if num_devices_open == 0 {
println!("No video devices open, turning off light");
device_handle.set_on(false)?;
} else {
println!("{} video devices open, turning on light", num_devices_open);
device_handle.set_on(true)?;
}
}
}

fn main() -> ExitCode {
let args = Cli::parse();

Expand All @@ -294,6 +376,10 @@ fn main() -> ExitCode {
serial_number,
value,
} => handle_temperature_command(serial_number.as_deref(), *value),
#[cfg(target_os = "linux")]
Commands::AutoToggle { serial_number } => {
handle_autotoggle_command(serial_number.as_deref())
}
};

if let Err(error) = result {
Expand Down

0 comments on commit 56cb6cb

Please sign in to comment.