# HG changeset patch # User Emmanuel Gil Peyrot # Date 1604271969 -3600 # Node ID adab131459942a93a7d09d1f4677bf57d65d1a89 # Parent 97e543f50f62dfae01515110012bcf0a25bd76f2 Add support for remote clients. diff --git a/Cargo.toml b/Cargo.toml --- a/Cargo.toml +++ b/Cargo.toml @@ -8,13 +8,19 @@ description = "Tablet emulator, for people who don’t own one" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["gui"] + +gui = ["cairo-rs", "gdk", "gio", "glib", "gtk"] + [dependencies] -cairo-rs = "0.9.1" -gdk = "0.13.2" -gio = "0.9.1" -glib = "0.10.2" -gtk = "0.9.2" input-linux = "0.3.0" +bitflags = "1.2.1" +cairo-rs = { version = "0.9.1", optional = true } +gdk = { version = "0.13.2", optional = true } +gio = { version = "0.9.1", optional = true } +glib = { version = "0.10.2", optional = true } +gtk = { version = "0.9.2", optional = true } [profile.release] lto = true diff --git a/src/gtk.rs b/src/gtk.rs --- a/src/gtk.rs +++ b/src/gtk.rs @@ -18,9 +18,8 @@ use gio::prelude::*; use glib::clone; use gtk::prelude::*; -use std::env::args; use std::io::ErrorKind; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use input_linux::Key; use crate::{ @@ -68,7 +67,7 @@ fn build_ui(application: >k::Applicati build_main_menu(application); let state = match State::new() { - Ok(state) => state, + Ok(state) => Arc::new(Mutex::new(state)), Err(err) => { match err.kind() { ErrorKind::NotFound => { @@ -179,12 +178,12 @@ fn build_ui(application: >k::Applicati window.show_all(); } -pub fn main() { +pub fn main(args: &[String]) { let application = gtk::Application::new( Some("fr.linkmauve.TabletEmu"), gio::ApplicationFlags::empty(), ) .expect("Initialisation failed…"); application.connect_activate(build_ui); - application.run(&args().collect::>()); + application.run(args); } diff --git a/src/main.rs b/src/main.rs --- a/src/main.rs +++ b/src/main.rs @@ -14,16 +14,48 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +#[cfg(feature = "gui")] mod gtk; +mod server; mod uinput; mod state; +use std::env::args; + const MAX_X: i32 = 69920; const MAX_Y: i32 = 39980; const DEFAULT_WIDTH: i32 = 320; const DEFAULT_HEIGHT: i32 = 180; +#[derive(Debug)] +enum Ui { + Gtk, + Server, +} + fn main() { - gtk::main(); + let mut args: Vec<_> = args().collect(); + let ui = match if args.len() > 1 { + args.remove(1) + } else { + String::from("gui") + }.as_str() { + "gui" => Ui::Gtk, + "server" => Ui::Server, + name => { + eprintln!("Wrong UI “{}”, expected gui or server.", name); + std::process::exit(2); + } + }; + + match ui { + #[cfg(feature = "gui")] + Ui::Gtk => gtk::main(&args), + + #[cfg(not(feature = "gui"))] + Ui::Gtk => panic!("tablet-emu has been compiled without GUI support."), + + Ui::Server => server::main(&args), + } } diff --git a/src/server.rs b/src/server.rs new file mode 100644 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,142 @@ +// Tablet emulator, for people who don’t own one +// Copyright © 2020 Emmanuel Gil Peyrot +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use std::net::UdpSocket; +use std::io::{self, ErrorKind}; +use crate::state::State; +use bitflags::bitflags; +use input_linux::Key; + +bitflags! { + /// This is the memory layout of the buttons on the 3DS. + #[derive(Default)] + struct Buttons: u32 { + const A = 0x00000001; + const B = 0x00000002; + const SELECT = 0x00000004; + const START = 0x00000008; + const RIGHT = 0x00000010; + const LEFT = 0x00000020; + const UP = 0x00000040; + const DOWN = 0x00000080; + const R = 0x00000100; + const L = 0x00000200; + const X = 0x00000400; + const Y = 0x00000800; + // Nothing + // Nothing + const ZL = 0x00004000; + const ZR = 0x00008000; + // Nothing + // Nothing + // Nothing + // Nothing + const TOUCH = 0x00100000; + // Nothing + // Nothing + // Nothing + const C_RIGHT = 0x01000000; + const C_LEFT = 0x02000000; + const C_UP = 0x04000000; + const C_DOWN = 0x08000000; + const CIRCLE_RIGHT = 0x10000000; + const CIRCLE_LEFT = 0x20000000; + const CIRCLE_UP = 0x40000000; + const CIRCLE_DOWN = 0x80000000; + } +} + +#[derive(Debug, Default)] +struct Event { + buttons: Buttons, + pad: (i16, i16), + c_pad: (i16, i16), + touch: (u16, u16), +} + +pub fn run_server(address: &str) -> io::Result<()> { + let mut state = match State::new() { + Ok(state) => state, + Err(err) => { + match err.kind() { + ErrorKind::NotFound => { + eprintln!("Couldn’t find /dev/uinput: {}", err); + eprintln!("Maybe you forgot to `modprobe uinput`?"); + } + ErrorKind::PermissionDenied => { + eprintln!("Couldn’t open /dev/uinput for writing: {}", err); + eprintln!("Maybe you aren’t allowed to create input devices?"); + } + _ => eprintln!("Couldn’t open /dev/uinput for writing: {}", err), + } + std::process::exit(1); + } + }; + + let socket = UdpSocket::bind(address)?; + println!("Listening on {:?}", socket); + println!("Here is an example client: https://hg.linkmauve.fr/remote-gamepad"); + + let mut event: Event = Default::default(); + let mut last = Some((0., 0.)); + state.set_size((320, 240)); + loop { + // TODO: Yolo-alignment. + let buf: &mut [u8; 16] = unsafe { std::mem::transmute(&mut event) }; + let (amount, source) = socket.recv_from(buf)?; + if amount != std::mem::size_of::() { + eprintln!("Invalid data length: {}", amount); + continue; + } + println!("{:?} from {:?}", event, source); + if event.buttons.contains(Buttons::A) { + state.select_tool(Key::ButtonToolPen); + } else if event.buttons.contains(Buttons::B) { + state.select_tool(Key::ButtonToolRubber); + } else if event.buttons.contains(Buttons::X) { + state.select_tool(Key::ButtonToolBrush); + } else if event.buttons.contains(Buttons::Y) { + state.select_tool(Key::ButtonToolPencil); + } else if event.buttons.contains(Buttons::SELECT) { + state.select_tool(Key::ButtonToolAirbrush); + } + let (x, y) = event.touch; + if event.buttons.contains(Buttons::TOUCH) { + if let None = last { + state.press(x as f64, y as f64)?; + last = Some((x as f64, y as f64)); + continue; + } + } else { + if let Some((x, y)) = last { + state.release(x, y)?; + last = None; + continue; + } + } + state.motion(x as f64, y as f64)?; + last = Some((x as f64, y as f64)); + } +} + +pub fn main(args: &[String]) { + let address = if args.len() > 1 { + args[1].clone() + } else { + String::from("0.0.0.0:16150") + }; + run_server(&address).unwrap(); +} diff --git a/src/state.rs b/src/state.rs --- a/src/state.rs +++ b/src/state.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::fs::File; -use std::sync::{Arc, Mutex}; use input_linux::{AbsoluteAxis, Key, MiscKind, SynchronizeKind, UInputHandle}; use crate::{DEFAULT_WIDTH, DEFAULT_HEIGHT, MAX_X, MAX_Y}; @@ -30,7 +29,7 @@ pub struct State { } impl State { - pub fn new() -> std::io::Result>> { + pub fn new() -> std::io::Result { let dev = create_uinput_device()?; println!( "New device at {:?} ({:?})", @@ -38,13 +37,13 @@ impl State { dev.sys_path()? ); - Ok(Arc::new(Mutex::new(State { + Ok(State { dev, width: DEFAULT_WIDTH as f64, height: DEFAULT_HEIGHT as f64, selected_tool: Key::ButtonToolPen, pressed: false, - }))) + }) } pub fn set_size(&mut self, (width, height): (u32, u32)) {