| /* |
| * Copyright 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| //! A Rust implementation of the evemu-record command from the [FreeDesktop evemu suite][evemu] of |
| //! tools. |
| //! |
| //! [evemu]: https://gitlab.freedesktop.org/libevdev/evemu |
| |
| use std::cmp; |
| use std::error::Error; |
| use std::fs; |
| use std::io; |
| use std::io::{BufRead, Write}; |
| use std::path::PathBuf; |
| |
| use clap::{Parser, ValueEnum}; |
| use nix::sys::time::TimeVal; |
| |
| mod evdev; |
| |
| /// Records evdev events from an input device in a format compatible with the FreeDesktop evemu |
| /// library. |
| #[derive(Parser, Debug)] |
| struct Args { |
| /// The path to the input device to record. If omitted, offers a list of devices to choose from. |
| device: Option<PathBuf>, |
| /// The file to save the recording to. Defaults to standard output. |
| output_file: Option<PathBuf>, |
| |
| /// The base time that timestamps should be relative to (Android-specific extension) |
| #[arg(long, value_enum, default_value_t = TimestampBase::FirstEvent)] |
| timestamp_base: TimestampBase, |
| } |
| |
| #[derive(Clone, Debug, ValueEnum)] |
| enum TimestampBase { |
| /// The first event received from the device. |
| FirstEvent, |
| |
| /// The time when the system booted. |
| Boot, |
| } |
| |
| fn get_choice(max: u32) -> u32 { |
| fn read_u32() -> Result<u32, std::num::ParseIntError> { |
| io::stdin().lock().lines().next().unwrap().unwrap().parse::<u32>() |
| } |
| let mut choice = read_u32(); |
| while choice.is_err() || choice.clone().unwrap() > max { |
| eprint!("Enter a number between 0 and {max} inclusive: "); |
| choice = read_u32(); |
| } |
| choice.unwrap() |
| } |
| |
| fn pick_input_device() -> Result<PathBuf, io::Error> { |
| eprintln!("Available devices:"); |
| let mut entries = |
| fs::read_dir("/dev/input")?.filter_map(|entry| entry.ok()).collect::<Vec<_>>(); |
| entries.sort_by_key(|entry| entry.path()); |
| let mut highest_number = 0; |
| for entry in entries { |
| let path = entry.path(); |
| let file_name = path.file_name().unwrap().to_str().unwrap(); |
| if path.is_dir() || !file_name.starts_with("event") { |
| continue; |
| } |
| let number = file_name.strip_prefix("event").unwrap().parse::<u32>(); |
| if number.is_err() { |
| continue; |
| } |
| let number = number.unwrap(); |
| match evdev::Device::open(path.as_path()) { |
| Ok(dev) => { |
| highest_number = cmp::max(highest_number, number); |
| eprintln!( |
| "{}:\t{}", |
| path.display(), |
| dev.name().unwrap_or("[could not read name]".to_string()), |
| ); |
| } |
| Err(_) => { |
| eprintln!("Couldn't open {}", path.display()); |
| } |
| } |
| } |
| eprint!("Select the device event number [0-{highest_number}]: "); |
| let choice = get_choice(highest_number); |
| Ok(PathBuf::from(format!("/dev/input/event{choice}"))) |
| } |
| |
| fn print_device_description( |
| device: &evdev::Device, |
| output: &mut impl Write, |
| ) -> Result<(), Box<dyn Error>> { |
| // TODO(b/302297266): report LED and SW states, then bump the version to EVEMU 1.3. |
| writeln!(output, "# EVEMU 1.2")?; |
| writeln!(output, "N: {}", device.name()?)?; |
| |
| let ids = device.ids()?; |
| writeln!( |
| output, |
| "I: {:04x} {:04x} {:04x} {:04x}", |
| ids.bus_type, ids.vendor, ids.product, ids.version, |
| )?; |
| |
| fn print_in_8_byte_chunks( |
| output: &mut impl Write, |
| prefix: &str, |
| data: &Vec<u8>, |
| ) -> Result<(), io::Error> { |
| for (i, byte) in data.iter().enumerate() { |
| if i % 8 == 0 { |
| write!(output, "{prefix}")?; |
| } |
| write!(output, " {:02x}", byte)?; |
| if (i + 1) % 8 == 0 { |
| writeln!(output)?; |
| } |
| } |
| if data.len() % 8 != 0 { |
| for _ in (data.len() % 8)..8 { |
| write!(output, " 00")?; |
| } |
| writeln!(output)?; |
| } |
| Ok(()) |
| } |
| |
| let props = device.properties_bitmap()?; |
| print_in_8_byte_chunks(output, "P:", &props)?; |
| |
| // The SYN event type can't be queried through the EVIOCGBIT ioctl, so just hard-code it to |
| // SYN_REPORT, SYN_CONFIG, and SYN_DROPPED. |
| writeln!(output, "B: 00 0b 00 00 00 00 00 00 00")?; |
| for event_type in evdev::EVENT_TYPES_WITH_BITMAPS { |
| let bits = device.bitmap_for_event_type(event_type)?; |
| print_in_8_byte_chunks(output, format!("B: {:02x}", event_type as u16).as_str(), &bits)?; |
| } |
| |
| for axis in device.supported_axes_of_type(evdev::EventType::ABS)? { |
| let info = device.absolute_axis_info(axis)?; |
| writeln!( |
| output, |
| "A: {axis:02x} {} {} {} {} {}", |
| info.minimum, info.maximum, info.fuzz, info.flat, info.resolution |
| )?; |
| } |
| Ok(()) |
| } |
| |
| fn print_events( |
| device: &evdev::Device, |
| output: &mut impl Write, |
| timestamp_base: TimestampBase, |
| ) -> Result<(), Box<dyn Error>> { |
| fn print_event(output: &mut impl Write, event: &evdev::InputEvent) -> Result<(), io::Error> { |
| // TODO(b/302297266): Translate events into human-readable names and add those as comments. |
| writeln!( |
| output, |
| "E: {}.{:06} {:04x} {:04x} {:04}", |
| event.time.tv_sec(), |
| event.time.tv_usec(), |
| event.type_, |
| event.code, |
| event.value, |
| )?; |
| Ok(()) |
| } |
| let event = device.read_event()?; |
| let start_time = match timestamp_base { |
| // Due to a bug in the C implementation of evemu-play [0] that has since become part of the |
| // API, the timestamp of the first event in a recording shouldn't be exactly 0.0 seconds, |
| // so offset it by 1µs. |
| // |
| // [0]: https://gitlab.freedesktop.org/libevdev/evemu/-/commit/eba96a4d2be7260b5843e65c4b99c8b06a1f4c9d |
| TimestampBase::FirstEvent => event.time - TimeVal::new(0, 1), |
| TimestampBase::Boot => TimeVal::new(0, 0), |
| }; |
| print_event(output, &event.offset_time_by(start_time))?; |
| loop { |
| let event = device.read_event()?; |
| print_event(output, &event.offset_time_by(start_time))?; |
| } |
| } |
| |
| fn main() -> Result<(), Box<dyn Error>> { |
| let args = Args::parse(); |
| |
| let device_path = args.device.unwrap_or_else(|| pick_input_device().unwrap()); |
| |
| let device = evdev::Device::open(device_path.as_path())?; |
| let mut output = match args.output_file { |
| Some(path) => Box::new(fs::File::create(path)?) as Box<dyn Write>, |
| None => Box::new(io::stdout().lock()), |
| }; |
| print_device_description(&device, &mut output)?; |
| print_events(&device, &mut output, args.timestamp_base)?; |
| Ok(()) |
| } |