Initial commit

This commit is contained in:
Jonas Schievink 2023-04-20 21:04:10 +02:00
commit 20b4b2c18f
9 changed files with 958 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

478
Cargo.lock generated Normal file
View file

@ -0,0 +1,478 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
[[package]]
name = "argh"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab257697eb9496bf75526f0217b5ed64636a9cfafa78b8365c71bd283fcef93e"
dependencies = [
"argh_derive",
"argh_shared",
]
[[package]]
name = "argh_derive"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b382dbd3288e053331f03399e1db106c9fb0d8562ad62cb04859ae926f324fa6"
dependencies = [
"argh_shared",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "argh_shared"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64cb94155d965e3d37ffbbe7cc5b82c3dd79dd33bd48e536f73d2cfb8d85506f"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "bytemuck"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
]
[[package]]
name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "env_logger"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
dependencies = [
"humantime",
"is-terminal",
"log",
"termcolor",
]
[[package]]
name = "errno"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "evdev"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bed59fcc8cfd6b190814a509018388462d3b203cf6dd10db5c00087e72a83f3"
dependencies = [
"bitvec",
"cfg-if",
"libc",
"nix 0.23.2",
"thiserror",
]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "io-lifetimes"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
dependencies = [
"hermit-abi",
"libc",
"windows-sys",
]
[[package]]
name = "is-terminal"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
dependencies = [
"hermit-abi",
"io-lifetimes",
"rustix",
"windows-sys",
]
[[package]]
name = "keylightd"
version = "0.1.0"
dependencies = [
"anyhow",
"argh",
"bytemuck",
"env_logger",
"evdev",
"log",
"nix 0.26.2",
]
[[package]]
name = "libc"
version = "0.2.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
[[package]]
name = "linux-raw-sys"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b085a4f2cde5781fc4b1717f2e86c62f5cda49de7ba99a7c2eae02b61c9064c"
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "memoffset"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg",
]
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
[[package]]
name = "nix"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
dependencies = [
"bitflags",
"cc",
"cfg-if",
"libc",
"memoffset 0.6.5",
]
[[package]]
name = "nix"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
dependencies = [
"bitflags",
"cfg-if",
"libc",
"memoffset 0.7.1",
"pin-utils",
"static_assertions",
]
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rustix"
version = "0.37.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "722529a737f5a942fdbac3a46cee213053196737c5eaa3386d52e85b786f2659"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "termcolor"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
]
[[package]]
name = "unicode-ident"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]

20
Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "keylightd"
version = "1.0.0"
edition = "2021"
license = "0BSD"
description = "Keyboard backlight daemon for Framework laptops"
repository = "https://github.com/jonas-schievink/keylightd"
categories = ["hardware-support", "command-line-utilities"]
[dependencies]
evdev = "0.12.1"
nix = { version = "0.26.2", features = ["user"] }
anyhow = "1.0.70"
bytemuck = { version = "1.13.1", features = ["derive"] }
log = "0.4.17"
env_logger = { version = "0.10.0", default-features = false, features = [
"auto-color",
"humantime",
] }
argh = "0.1.10"

10
LICENSE Normal file
View file

@ -0,0 +1,10 @@
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

56
README.md Normal file
View file

@ -0,0 +1,56 @@
<div align="center">
# `keylightd`
### Keyboard backlight daemon for Framework laptops
</div>
`keylightd` is a small system daemon for [Framework] laptops that listens to keyboard and touchpad input, and turns on the keyboard backlight while either is being used.
[Framework]: https://frame.work/
## Installation
To install from source, clone the repository and run:
```shell
$ cargo build --release
$ sudo cp target/release/keylightd /usr/local/bin
```
`keylightd` has no native dependencies you have to install first (apart from a recent Rust toolchain for building it, of course).
It implements communication with the Embedded Controller itself, and talks to the input devices using `evdev` ioctls directly.
It also does not have any hard dependencies on a desktop environment or display server.
If you want to configure `keylightd` as a systemd service that starts on boot, you can use the provided service file:
```shell
$ sudo cp etc/keylightd.service /etc/systemd/system
$ sudo systemctl enable --now keylightd
```
## Running
Note that `keylightd` needs to be run as root, since it accesses the Embedded Controller to control the keyboard backlight.
`keylightd` takes the following command-line arguments:
```
Usage: keylightd [--brightness <brightness>] [--timeout <timeout>]
keylightd - automatic keyboard backlight daemon for Framework laptops
Options:
--brightness brightness level when active (0-100) [default=30]
--timeout activity timeout in seconds [default=10]
--help display usage information
```
If you're using the provided `keylightd.service` file, you can adjust the command line parameters there.
## Contributing
This project does not generally accept contributions. It is finished and does what I want of it.
Minor fixes and readme additions *may* be accepted.

9
etc/keylightd.service Normal file
View file

@ -0,0 +1,9 @@
[Unit]
Description=Keyboard backlight daemon
[Service]
Type=exec
ExecStart=/usr/local/bin/keylightd
[Install]
WantedBy=multi-user.target

108
src/command.rs Normal file
View file

@ -0,0 +1,108 @@
use bytemuck::{NoUninit, Pod, Zeroable};
/// Trait implemented by Embedded Controller commands.
pub trait Command: NoUninit {
/// The command ID.
const CMD: Cmd;
/// Command version.
///
/// Some commands come in multiple versions (although none of the ones supported here).
const VERSION: u32 = 0;
/// The associated response type.
type Response: Pod;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cmd {
#[allow(unused)] // no longer used by cros-ec
ProtoVersion = 0x0000,
Hello = 0x0001,
GetVersion = 0x0002,
// ...
GetKeyboardBacklight = 0x0022,
SetKeyboardBacklight = 0x0023,
}
//////////////////////////////////
// Hello
//////////////////////////////////
#[derive(Clone, Copy, NoUninit)]
#[repr(C)]
pub struct Hello {
pub in_data: u32,
}
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct HelloResponse {
pub out_data: u32,
}
impl Command for Hello {
const CMD: Cmd = Cmd::Hello;
type Response = HelloResponse;
}
//////////////////////////////////
// GetVersion
//////////////////////////////////
#[derive(Clone, Copy, NoUninit)]
#[repr(C)]
pub struct GetVersion;
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct GetVersionResponse {
version_string_ro: [u8; 32],
version_string_rw: [u8; 32],
reserved: [u8; 32],
current_image: u32,
}
impl Command for GetVersion {
const CMD: Cmd = Cmd::GetVersion;
type Response = GetVersionResponse;
}
//////////////////////////////////
// GetKeyboardBacklight
//////////////////////////////////
#[derive(Clone, Copy, NoUninit)]
#[repr(C)]
pub struct GetKeyboardBacklight;
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct GetKeyboardBacklightResponse {
pub percent: u8,
pub enabled: u8,
}
impl Command for GetKeyboardBacklight {
const CMD: Cmd = Cmd::GetKeyboardBacklight;
type Response = GetKeyboardBacklightResponse;
}
//////////////////////////////////
// SetKeyboardBacklight
//////////////////////////////////
#[derive(Clone, Copy, NoUninit)]
#[repr(C)]
pub struct SetKeyboardBacklight {
pub percent: u8,
}
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct SetKeyboardBacklightResponse;
impl Command for SetKeyboardBacklight {
const CMD: Cmd = Cmd::SetKeyboardBacklight;
type Response = SetKeyboardBacklightResponse;
}

152
src/ec.rs Normal file
View file

@ -0,0 +1,152 @@
use std::{
fs::File,
io,
mem::{size_of, size_of_val, MaybeUninit},
os::fd::AsRawFd,
};
use nix::{errno::Errno, libc::ioctl, request_code_readwrite};
use crate::command::{self, Hello};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IoctlVersion {
V1,
V2,
}
/// A handle to the system's ChromiumOS Embedded Controller.
///
/// This uses the ioctl interface of `/dev/cros_ec` to issue commands.
pub struct EmbeddedController {
fd: File,
version: IoctlVersion,
}
impl EmbeddedController {
pub fn open() -> io::Result<Self> {
let mut this = Self {
fd: File::options()
.read(true)
.write(true)
.open("/dev/cros_ec")?,
version: IoctlVersion::V1,
};
// The framework EC uses ioctl interface version 2, but this mirrors the logic in ectool
// just to make sure it doesn't do something nonsensical on non-Framework machines.
this.version = match this.cmd_v1(Hello {
in_data: 0xa0b0c0d0,
}) {
Err(Errno::ENOTTY) => IoctlVersion::V2,
_ => IoctlVersion::V1,
};
log::debug!("ioctl version {:?}", this.version);
// Test communication by issuing a `Hello` command and reading back the result.
let magic = 0xaa55dead;
let resp = this.command(Hello { in_data: magic })?;
let expected = magic + 0x01020304;
if resp.out_data != expected {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"failed to connect to EC: invalid response to hello command (received {:010x}, expected {:010x})",
resp.out_data, expected,
),
));
}
log::info!("connected to embedded controller");
Ok(this)
}
pub fn command<C: command::Command>(&self, cmd: C) -> io::Result<C::Response> {
match self.version {
IoctlVersion::V1 => self.cmd_v1(cmd),
IoctlVersion::V2 => self.cmd_v2(cmd),
}
.map_err(Into::into)
}
fn cmd_v1<C: command::Command>(&self, cmd: C) -> nix::Result<C::Response> {
let mut resp = MaybeUninit::<C::Response>::uninit();
let mut cmd = CommandV1 {
version: C::VERSION,
command: C::CMD as u32,
outdata: bytemuck::bytes_of(&cmd).as_ptr() as *mut _,
outsize: size_of_val(&cmd).try_into().unwrap(),
indata: resp.as_mut_ptr().cast(),
insize: size_of_val(&resp).try_into().unwrap(),
result: 0xff,
};
unsafe {
let ret = ioctl(
self.fd.as_raw_fd(),
request_code_readwrite!(':', 0, size_of::<CommandV1>()),
&mut cmd,
);
Errno::result(ret)?;
Ok(resp.assume_init())
}
}
fn cmd_v2<C: command::Command>(&self, cmd: C) -> nix::Result<C::Response> {
let mut cmd = CommandV2 {
header: CommandV2Header {
version: C::VERSION,
command: C::CMD as u32,
outsize: size_of::<C>().try_into().unwrap(),
insize: size_of::<C::Response>().try_into().unwrap(),
result: 0xff,
},
data: CommandV2Union { req: cmd },
};
unsafe {
let ret = ioctl(
self.fd.as_raw_fd(),
request_code_readwrite!(0xEC, 0, size_of::<CommandV2Header>()),
&mut cmd,
);
Errno::result(ret)?;
Ok(cmd.data.resp)
}
}
}
#[repr(C)]
struct CommandV1 {
version: u32,
command: u32,
outdata: *mut u8,
outsize: u32,
indata: *mut u8,
insize: u32,
result: u32,
}
#[repr(C)]
struct CommandV2<C: command::Command> {
header: CommandV2Header,
// Request and response are stored in a `union` rather than using an empty trailing array like
// the C code does. I believe this is ABI-equivalent, so it shouldn't cause problems.
data: CommandV2Union<C>,
}
#[repr(C)]
struct CommandV2Header {
version: u32,
command: u32,
outsize: u32,
insize: u32,
result: u32,
}
#[repr(C)]
union CommandV2Union<C: command::Command> {
req: C,
resp: C::Response,
}

124
src/main.rs Normal file
View file

@ -0,0 +1,124 @@
use std::{
io,
sync::{Arc, Condvar, Mutex},
thread,
time::{Duration, Instant},
};
use argh::FromArgs;
use command::{GetKeyboardBacklight, SetKeyboardBacklight};
use ec::EmbeddedController;
mod command;
mod ec;
/// keylightd - automatic keyboard backlight daemon for Framework laptops
#[derive(FromArgs)]
struct Args {
/// brightness level when active (0-100) [default=30]
#[argh(option, default = "30", from_str_fn(parse_brightness))]
brightness: u8,
/// activity timeout in seconds [default=10]
#[argh(option, default = "10")]
timeout: u32,
}
fn parse_brightness(s: &str) -> Result<u8, String> {
let brightness = s.parse::<u8>().map_err(|e| e.to_string())?;
if brightness > 100 {
return Err("invalid brightness value {brightness} (valid range: 0-100)".into());
}
Ok(brightness)
}
fn main() -> anyhow::Result<()> {
env_logger::builder()
.filter_module(env!("CARGO_PKG_NAME"), log::LevelFilter::Info)
.init();
let args: Args = argh::from_env();
let ec = EmbeddedController::open()?;
let fade_to = |target: u8| -> io::Result<()> {
let resp = ec.command(GetKeyboardBacklight)?;
let mut cur = if resp.enabled != 0 { resp.percent } else { 0 };
while cur != target {
if cur > target {
cur -= 1;
} else {
cur += 1;
}
ec.command(SetKeyboardBacklight { percent: cur })?;
thread::sleep(Duration::from_millis(3));
}
Ok(())
};
let act = Arc::new(ActivityState {
last_activity: Mutex::new(Instant::now()),
condvar: Condvar::new(),
});
for (path, mut device) in evdev::enumerate() {
// Filter devices so that only the Framework's builtin touchpad and keyboard are listened
// to. Since we don't support hotplug, listening on USB devices wouldn't work reliably.
match device.name() {
Some("PIXA3854:00 093A:0274 Touchpad" | "AT Translated Set 2 keyboard") => {
let act = act.clone();
thread::spawn(move || -> io::Result<()> {
let name = device.name();
let name = name.as_deref().unwrap_or("<unknown>").to_string();
log::info!("starting listener on {}: {name}", path.display());
loop {
if let Err(e) = device.fetch_events() {
log::warn!(
"error while fetching events for device '{name}': {e}; closing"
);
return Err(e);
}
*act.last_activity.lock().unwrap() = Instant::now();
act.condvar.notify_one();
// Delay a bit, to avoid busy looping.
thread::sleep(Duration::from_millis(500));
}
});
}
_ => {}
}
}
log::info!("idle timeout: {} seconds", args.timeout);
log::info!("brightness level: {}%", args.brightness);
let mut state = None;
loop {
let guard = act.last_activity.lock().unwrap();
let last = *guard;
let (_, result) = act
.condvar
.wait_timeout_while(guard, Duration::from_secs(args.timeout.into()), |instant| {
*instant == last
})
.unwrap();
let new_state = !result.timed_out();
if state != Some(new_state) {
log::info!("activity state changed: {state:?} -> {new_state}");
if new_state {
// Fade in
fade_to(args.brightness)?;
} else {
// Fade out
fade_to(0)?;
}
state = Some(new_state);
}
}
}
struct ActivityState {
last_activity: Mutex<Instant>,
condvar: Condvar,
}