1 /*
2 * Copyright 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 //! A Rust implementation of the evemu-record command from the [FreeDesktop evemu suite][evemu] of
18 //! tools.
19 //!
20 //! [evemu]: https://gitlab.freedesktop.org/libevdev/evemu
21
22 use std::cmp;
23 use std::error::Error;
24 use std::fs;
25 use std::io;
26 use std::io::{BufRead, Write};
27 use std::path::PathBuf;
28
29 use clap::{Parser, ValueEnum};
30 use nix::sys::time::TimeVal;
31
32 mod evdev;
33
34 /// Records evdev events from an input device in a format compatible with the FreeDesktop evemu
35 /// library.
36 #[derive(Parser, Debug)]
37 struct Args {
38 /// The path to the input device to record. If omitted, offers a list of devices to choose from.
39 device: Option<PathBuf>,
40 /// The file to save the recording to. Defaults to standard output.
41 output_file: Option<PathBuf>,
42
43 /// The base time that timestamps should be relative to (Android-specific extension)
44 #[arg(long, value_enum, default_value_t = TimestampBase::FirstEvent)]
45 timestamp_base: TimestampBase,
46 }
47
48 #[derive(Clone, Debug, ValueEnum)]
49 enum TimestampBase {
50 /// The first event received from the device.
51 FirstEvent,
52
53 /// The Unix epoch (00:00:00 UTC on 1st January 1970), so that all timestamps are Unix
54 /// timestamps. This makes the events in the recording easier to match up with those from other
55 /// log sources.
56 Epoch,
57 }
58
get_choice(max: u32) -> u3259 fn get_choice(max: u32) -> u32 {
60 fn read_u32() -> Result<u32, std::num::ParseIntError> {
61 io::stdin().lock().lines().next().unwrap().unwrap().parse::<u32>()
62 }
63 let mut choice = read_u32();
64 while choice.is_err() || choice.clone().unwrap() > max {
65 eprint!("Enter a number between 0 and {max} inclusive: ");
66 choice = read_u32();
67 }
68 choice.unwrap()
69 }
70
pick_input_device() -> Result<PathBuf, io::Error>71 fn pick_input_device() -> Result<PathBuf, io::Error> {
72 eprintln!("Available devices:");
73 let mut entries =
74 fs::read_dir("/dev/input")?.filter_map(|entry| entry.ok()).collect::<Vec<_>>();
75 entries.sort_by_key(|entry| entry.path());
76 let mut highest_number = 0;
77 for entry in entries {
78 let path = entry.path();
79 let file_name = path.file_name().unwrap().to_str().unwrap();
80 if path.is_dir() || !file_name.starts_with("event") {
81 continue;
82 }
83 let number = file_name.strip_prefix("event").unwrap().parse::<u32>();
84 if number.is_err() {
85 continue;
86 }
87 let number = number.unwrap();
88 match evdev::Device::open(path.as_path()) {
89 Ok(dev) => {
90 highest_number = cmp::max(highest_number, number);
91 eprintln!(
92 "{}:\t{}",
93 path.display(),
94 dev.name().unwrap_or("[could not read name]".to_string()),
95 );
96 }
97 Err(_) => {
98 eprintln!("Couldn't open {}", path.display());
99 }
100 }
101 }
102 eprint!("Select the device event number [0-{highest_number}]: ");
103 let choice = get_choice(highest_number);
104 Ok(PathBuf::from(format!("/dev/input/event{choice}")))
105 }
106
print_device_description( device: &evdev::Device, output: &mut impl Write, ) -> Result<(), Box<dyn Error>>107 fn print_device_description(
108 device: &evdev::Device,
109 output: &mut impl Write,
110 ) -> Result<(), Box<dyn Error>> {
111 // TODO(b/302297266): report LED and SW states, then bump the version to EVEMU 1.3.
112 writeln!(output, "# EVEMU 1.2")?;
113 writeln!(output, "N: {}", device.name()?)?;
114
115 let ids = device.ids()?;
116 writeln!(
117 output,
118 "I: {:04x} {:04x} {:04x} {:04x}",
119 ids.bus_type, ids.vendor, ids.product, ids.version,
120 )?;
121
122 fn print_in_8_byte_chunks(
123 output: &mut impl Write,
124 prefix: &str,
125 data: &[u8],
126 ) -> Result<(), io::Error> {
127 for (i, byte) in data.iter().enumerate() {
128 if i % 8 == 0 {
129 write!(output, "{prefix}")?;
130 }
131 write!(output, " {:02x}", byte)?;
132 if (i + 1) % 8 == 0 {
133 writeln!(output)?;
134 }
135 }
136 if data.len() % 8 != 0 {
137 for _ in (data.len() % 8)..8 {
138 write!(output, " 00")?;
139 }
140 writeln!(output)?;
141 }
142 Ok(())
143 }
144
145 let props = device.properties_bitmap()?;
146 print_in_8_byte_chunks(output, "P:", &props)?;
147
148 // The SYN event type can't be queried through the EVIOCGBIT ioctl, so just hard-code it to
149 // SYN_REPORT, SYN_CONFIG, and SYN_DROPPED.
150 writeln!(output, "B: 00 0b 00 00 00 00 00 00 00")?;
151 for event_type in evdev::EVENT_TYPES_WITH_BITMAPS {
152 let bits = device.bitmap_for_event_type(event_type)?;
153 print_in_8_byte_chunks(output, format!("B: {:02x}", event_type as u16).as_str(), &bits)?;
154 }
155
156 for axis in device.supported_axes_of_type(evdev::EventType::ABS)? {
157 let info = device.absolute_axis_info(axis)?;
158 writeln!(
159 output,
160 "A: {axis:02x} {} {} {} {} {}",
161 info.minimum, info.maximum, info.fuzz, info.flat, info.resolution
162 )?;
163 }
164 Ok(())
165 }
166
print_events( device: &evdev::Device, output: &mut impl Write, timestamp_base: TimestampBase, ) -> Result<(), Box<dyn Error>>167 fn print_events(
168 device: &evdev::Device,
169 output: &mut impl Write,
170 timestamp_base: TimestampBase,
171 ) -> Result<(), Box<dyn Error>> {
172 fn print_event(output: &mut impl Write, event: &evdev::InputEvent) -> Result<(), io::Error> {
173 // TODO(b/302297266): Translate events into human-readable names and add those as comments.
174 writeln!(
175 output,
176 "E: {}.{:06} {:04x} {:04x} {:04}",
177 event.time.tv_sec(),
178 event.time.tv_usec(),
179 event.type_,
180 event.code,
181 event.value,
182 )?;
183 Ok(())
184 }
185 let event = device.read_event()?;
186 let start_time = match timestamp_base {
187 // Due to a bug in the C implementation of evemu-play [0] that has since become part of the
188 // API, the timestamp of the first event in a recording shouldn't be exactly 0.0 seconds,
189 // so offset it by 1µs.
190 //
191 // [0]: https://gitlab.freedesktop.org/libevdev/evemu/-/commit/eba96a4d2be7260b5843e65c4b99c8b06a1f4c9d
192 TimestampBase::FirstEvent => event.time - TimeVal::new(0, 1),
193 TimestampBase::Epoch => TimeVal::new(0, 0),
194 };
195 print_event(output, &event.offset_time_by(start_time))?;
196 loop {
197 let event = device.read_event()?;
198 print_event(output, &event.offset_time_by(start_time))?;
199 }
200 }
201
main() -> Result<(), Box<dyn Error>>202 fn main() -> Result<(), Box<dyn Error>> {
203 let args = Args::parse();
204
205 let device_path = args.device.unwrap_or_else(|| pick_input_device().unwrap());
206
207 let device = evdev::Device::open(device_path.as_path())?;
208 let mut output = match args.output_file {
209 Some(path) => Box::new(fs::File::create(path)?) as Box<dyn Write>,
210 None => Box::new(io::stdout().lock()),
211 };
212 print_device_description(&device, &mut output)?;
213 print_events(&device, &mut output, args.timestamp_base)?;
214 Ok(())
215 }
216