From 20b4b2c18f3c2acb3cc96e1cea4bd4ed2b0d3ffc Mon Sep 17 00:00:00 2001 From: Jonas Schievink Date: Thu, 20 Apr 2023 21:04:10 +0200 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 478 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 20 ++ LICENSE | 10 + README.md | 56 +++++ etc/keylightd.service | 9 + src/command.rs | 108 ++++++++++ src/ec.rs | 152 ++++++++++++++ src/main.rs | 124 +++++++++++ 9 files changed, 958 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 etc/keylightd.service create mode 100644 src/command.rs create mode 100644 src/ec.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b3a867e --- /dev/null +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fe52b0e --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c7ffe15 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea1186a --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +
+ +# `keylightd` + +### Keyboard backlight daemon for Framework laptops + +
+ +`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 ] [--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. diff --git a/etc/keylightd.service b/etc/keylightd.service new file mode 100644 index 0000000..e71e564 --- /dev/null +++ b/etc/keylightd.service @@ -0,0 +1,9 @@ +[Unit] +Description=Keyboard backlight daemon + +[Service] +Type=exec +ExecStart=/usr/local/bin/keylightd + +[Install] +WantedBy=multi-user.target diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..2abadc1 --- /dev/null +++ b/src/command.rs @@ -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; +} diff --git a/src/ec.rs b/src/ec.rs new file mode 100644 index 0000000..70a3038 --- /dev/null +++ b/src/ec.rs @@ -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 { + 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(&self, cmd: C) -> io::Result { + match self.version { + IoctlVersion::V1 => self.cmd_v1(cmd), + IoctlVersion::V2 => self.cmd_v2(cmd), + } + .map_err(Into::into) + } + + fn cmd_v1(&self, cmd: C) -> nix::Result { + let mut resp = MaybeUninit::::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::()), + &mut cmd, + ); + Errno::result(ret)?; + Ok(resp.assume_init()) + } + } + + fn cmd_v2(&self, cmd: C) -> nix::Result { + let mut cmd = CommandV2 { + header: CommandV2Header { + version: C::VERSION, + command: C::CMD as u32, + outsize: size_of::().try_into().unwrap(), + insize: size_of::().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::()), + &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 { + 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, +} + +#[repr(C)] +struct CommandV2Header { + version: u32, + command: u32, + outsize: u32, + insize: u32, + result: u32, +} + +#[repr(C)] +union CommandV2Union { + req: C, + resp: C::Response, +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..15cdd7c --- /dev/null +++ b/src/main.rs @@ -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 { + let brightness = s.parse::().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("").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, + condvar: Condvar, +}