xref: /aosp_15_r20/frameworks/native/cmds/evemu-record/main.rs (revision 38e8c45f13ce32b0dcecb25141ffecaf386fa17f)
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