diff --git a/Cargo.lock b/Cargo.lock index fe6475d..8b9a735 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.5" @@ -56,24 +65,93 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +[[package]] +name = "approx" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[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 = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[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 = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cgmath" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" +dependencies = [ + "approx", + "num-traits", +] + [[package]] name = "clap" version = "4.4.14" @@ -120,6 +198,42 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "epoll" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74351c3392ea1ff6cd2628e0042d268ac2371cb613252ff383b6dfa50d22fa79" +dependencies = [ + "bitflags 2.4.1", + "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", + "thiserror", +] + [[package]] name = "fehler" version = "1.0.0" @@ -140,25 +254,127 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "libremarkable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "665568ee80be33f72306e91a1f2513098da90b85d60874bd7ede74bd9262f397" +dependencies = [ + "atomic", + "cgmath", + "env_logger", + "epoll", + "evdev", + "fxhash", + "libc", + "log", + "once_cell", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + [[package]] name = "lz-fear" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06aad1ce45e4ccf7a8d7d43e0c3ad38dc5d2255174a5f29a3c39d961fbc6181d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "byteorder", "fehler", "thiserror", "twox-hash", ] +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "proc-macro2" version = "1.0.76" @@ -177,12 +393,48 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "restream" version = "1.2.0" dependencies = [ "anyhow", "clap", + "libremarkable", "lz-fear", ] @@ -220,6 +472,21 @@ dependencies = [ "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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.56" @@ -262,6 +529,37 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +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.52.0" @@ -327,3 +625,12 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[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 index d05445f..e3c3f09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,5 @@ edition = "2018" anyhow = "1.0" lz-fear = "0.1" clap = { version = "4.4", features = [ "derive" ] } +libremarkable = { version = "0.6", features = [ "input" ], default-features = false } + diff --git a/reStream.sh b/reStream.sh index fa4fa4f..bf8cb3f 100755 --- a/reStream.sh +++ b/reStream.sh @@ -12,6 +12,7 @@ rm2_old_firmware_version="3.7.0.1930" # default values for arguments remarkable="${REMARKABLE_IP:-10.11.99.1}" # remarkable IP address landscape=true # rotate 90 degrees to the right +cursor=false # show a cursor where the pen is hovering output_path=- # display output through ffplay format=- # automatic output format webcam=false # not to a webcam @@ -32,6 +33,10 @@ while [ $# -gt 0 ]; do landscape=false shift ;; + -c | --cursor) + cursor=true + shift + ;; -s | --source) remarkable="$2" shift @@ -87,10 +92,11 @@ while [ $# -gt 0 ]; do shift ;; -h | --help | *) - echo "Usage: $0 [-p] [-u] [-s ] [-o ] [-f ] [-t ] [-m] [-w] [--hflip]" + echo "Usage: $0 [-p] [-c] [-u] [-s <source>] [-o <output>] [-f <format>] [-t <title>] [-m] [-w] [--hflip]" echo "Examples:" echo " $0 # live view in landscape" echo " $0 -p # live view in portrait" + echo " $0 -c # show a cursor where the pen is hovering (rM2 only)" echo " $0 -s 192.168.0.10 # connect to different IP" echo " $0 -o remarkable.mp4 # record to a file" echo " $0 -o udp://dest:1234 -f mpegts # record to a stream" @@ -255,6 +261,10 @@ set -e # stop if an error occurs restream_options="-h $height -w $width -b $bytes_per_pixel -f $fb_file" +if "$cursor"; then + restream_options="$restream_options -c" +fi + # shellcheck disable=SC2089 restream_rs="PATH=\"\$PATH:/opt/bin/:.\" restream $restream_options" if $unsecure_connection; then diff --git a/src/main.rs b/src/main.rs index 8452c59..a25da32 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,12 +6,15 @@ use anyhow::{Context, Result}; use clap::Parser; use lz_fear::CompressionSettings; +use libremarkable::cgmath; +use libremarkable::input::{ev::EvDevContext, InputDevice, InputEvent, WacomEvent, WacomPen}; + use std::default::Default; use std::fs::File; use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write}; use std::net::{TcpListener, TcpStream}; use std::process::Command; -use std::sync::mpsc::channel; +use std::sync::mpsc::{channel, Receiver}; use std::thread; use std::time::Duration; @@ -37,6 +40,10 @@ pub struct Opts { /// File containing the framebuffer data. If this equals the string ':mem:' it will try to read the framebuffer from xochitl's process memory (rM2 only). #[arg(long, name = "path", short = 'f')] file: String, + + /// Show a cursor where the pen is hovering. + #[arg(long, name = "cursor", short = 'c')] + show_cursor: bool, } fn main() -> Result<()> { @@ -51,7 +58,14 @@ fn main() -> Result<()> { (opts.file.to_owned(), 0) }; - let streamer = ReStreamer::init(&file, offset, opts.width, opts.height, opts.bytes_per_pixel)?; + let streamer = ReStreamer::init( + &file, + offset, + opts.width, + opts.height, + opts.bytes_per_pixel, + opts.show_cursor, + )?; let stdout = std::io::stdout(); let data_target: Box<dyn Write> = if let Some(port) = opts.listen { @@ -125,6 +139,14 @@ pub struct ReStreamer { start: u64, cursor: usize, size: usize, + width: usize, + height: usize, + bytes_per_pixel: usize, + + show_cursor: bool, + input_rx: Receiver<InputEvent>, + pen_pos: Option<(usize, usize)>, + drawing: bool, } impl ReStreamer { @@ -134,16 +156,30 @@ impl ReStreamer { width: usize, height: usize, bytes_per_pixel: usize, + show_cursor: bool, ) -> Result<ReStreamer> { let start = offset as u64; let size = width * height * bytes_per_pixel; let cursor = 0; let file = File::open(path)?; + + let (input_tx, input_rx) = channel::<InputEvent>(); + if show_cursor { + EvDevContext::new(InputDevice::Wacom, input_tx).start(); + } + let mut streamer = ReStreamer { file, start: start, cursor, size, + width, + height, + bytes_per_pixel, + show_cursor, + input_rx, + pen_pos: None, + drawing: false, }; streamer.next_frame()?; Ok(streamer) @@ -152,10 +188,122 @@ impl ReStreamer { pub fn next_frame(&mut self) -> std::io::Result<()> { self.file.seek(SeekFrom::Start(self.start))?; self.cursor = 0; + if self.show_cursor { + self.read_input(); + } + Ok(()) } + + /// Read input events to figure out pen state and position. + fn read_input(&mut self) { + let mut down = self.pen_pos.is_some(); + let mut pos = self.pen_pos.unwrap_or((0, 0)); + while let Ok(event) = self.input_rx.try_recv() { + match event { + InputEvent::WacomEvent { event: e } => match e { + WacomEvent::InstrumentChange { + pen: WacomPen::ToolPen, + state: s, + } => { + down = s; + } + WacomEvent::Hover { + position: cgmath::Point2 { x, y }, + .. + } => { + pos = (x as usize, y as usize); + self.drawing = false; + } + WacomEvent::Draw { .. } => { + // no need to show position while drawing + self.drawing = true; + } + _ => (), + }, + _ => (), + } + } + + self.pen_pos = if down { Some(pos) } else { None }; + } + + /// Draw pen position into fb data, if necessary (in hover range and not drawing). + fn draw_pen_position(&mut self, buf: &mut [u8]) { + if let (false, Some((y, x))) = (self.drawing, self.pen_pos) { + let flip = self.width > self.height; + let (x, y) = if flip { (y, x) } else { (x, y) }; + // we need negative numbers to calculate offsets correctly + let width = if flip { self.height } else { self.width } as isize; + let height = if flip { self.width } else { self.height } as isize; + let bpp = self.bytes_per_pixel as isize; + let cursor = self.cursor as isize; + for (i, (yoff, no)) in PEN_IMAGE.iter().enumerate() { + // we draw vertically (lines along y) + let xoff = i as isize - (PEN_IMAGE.len() as isize / 2); + let xstart = x as isize + xoff; + // line outside of canvas? + if xstart < 0 || xstart >= width { + continue; + } + let mut ystart = (height - y as isize) + yoff; + let mut no = *no; + // cut-off at sides + if ystart < 0 { + no += ystart; + ystart = 0; + } + if ystart + no > height { + no = height - ystart; + } + if no <= 0 { + continue; + } + // translate to buf indexes, check bounds and draw + let mut px_start = (xstart * height + ystart) * bpp; + let mut px_end = px_start + no * bpp; + // outside current buf? + if px_end < cursor || px_start >= cursor + buf.len() as isize { + continue; + } + // truncate if partially outside + if px_start < cursor { + px_start = cursor; + } + if px_end > cursor + buf.len() as isize { + px_end = cursor + buf.len() as isize; + } + // invert pixel (on RM2) + // TODO: Do something sensible on RM1 + for b in buf[(px_start - cursor) as usize..(px_end - cursor) as usize].iter_mut() { + *b = 255 - *b; + } + } + } + } } +// Image of pen, given as (offset from pen position, number of pixels) +static PEN_IMAGE: [(isize, isize); 17] = [ + (0, 1), // 00000000100000000 + (0, 1), // 00000000100000000 + (0, 1), // 00000000100000000 + (-1, 3), // 00000001110000000 + (-3, 7), // 00000111111100000 + (-4, 9), // 00001111111110000 + (-4, 9), // 00001111111110000 + (-5, 11), // 00011111111111000 + (-8, 17), // 11111111111111111 + (-5, 11), // 00011111111111000 + (-4, 9), // 00001111111110000 + (-4, 9), // 00001111111110000 + (-3, 7), // 00000111111100000 + (-1, 3), // 00000001110000000 + (0, 1), // 00000000100000000 + (0, 1), // 00000000100000000 + (0, 1), // 00000000100000000 +]; + impl Read for ReStreamer { fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { let requested = buf.len(); @@ -165,6 +313,11 @@ impl Read for ReStreamer { let rest = self.size - self.cursor; self.file.read(&mut buf[0..rest])? }; + + if self.show_cursor { + self.draw_pen_position(&mut buf[0..bytes_read]); + } + self.cursor += bytes_read; if self.cursor == self.size { self.next_frame()?;