Browse Source

Added support for single-touch screens directly via evdev

Nikita Tsukanov 4 years ago
parent
commit
cb636c5021

+ 141 - 0
src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs

@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Threading;
+using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods;
+
+namespace Avalonia.LinuxFramebuffer.Input.EvDev
+{
+    public class EvDevBackend : IInputBackend
+    {
+        private readonly EvDevDeviceDescription[] _deviceDescriptions;
+        private readonly List<EvDevDeviceHandler> _handlers = new List<EvDevDeviceHandler>();
+        private int _epoll;
+        private Queue<RawInputEventArgs> _inputQueue = new Queue<RawInputEventArgs>();
+        private bool _isQueueHandlerTriggered;
+        private object _lock = new object();
+        private Action<RawInputEventArgs> _onInput;
+        private IInputRoot _inputRoot;
+
+        public EvDevBackend(EvDevDeviceDescription[] devices)
+        {
+            _deviceDescriptions = devices;
+        }
+        
+        unsafe void InputThread()
+        {
+            const int MaxEvents = 16;
+            var events = stackalloc epoll_event[MaxEvents];
+            while (true)
+            {
+                var eventCount = Math.Min(MaxEvents, epoll_wait(_epoll, events, MaxEvents, 1000));
+                for (var c = 0; c < eventCount; c++)
+                {
+                    try
+                    {
+                        var ev = events[c];
+                        var handler = _handlers[(int)ev.data.u32];
+                        handler.HandleEvents();
+                    }
+                    catch (Exception e)
+                    {
+                        Console.Error.WriteLine(e.ToString());
+                    }
+                }
+            }
+        }
+
+        private void OnRawEvent(RawInputEventArgs obj)
+        {
+            lock (_lock)
+            {
+                _inputQueue.Enqueue(obj);
+                TriggerQueueHandler();
+            }
+                
+        }
+        
+        void TriggerQueueHandler()
+        {
+            if (_isQueueHandlerTriggered)
+                return;
+            _isQueueHandlerTriggered = true;
+            Dispatcher.UIThread.Post(InputQueueHandler, DispatcherPriority.Input);
+
+        }
+        
+        void InputQueueHandler()
+        {
+            RawInputEventArgs ev;
+            lock (_lock)
+            {
+                _isQueueHandlerTriggered = false;
+                if(_inputQueue.Count == 0)
+                    return;
+                ev = _inputQueue.Dequeue();
+            }
+
+            _onInput?.Invoke(ev);
+
+            lock (_lock)
+            {
+                if (_inputQueue.Count > 0)
+                    TriggerQueueHandler();
+            }
+        }
+        
+        public void Initialize(IScreenInfoProvider info, Action<RawInputEventArgs> onInput)
+        {
+            _onInput = onInput;
+            _epoll = epoll_create1(0);
+            for (var c = 0; c < _deviceDescriptions.Length; c++)
+            {
+                var description = _deviceDescriptions[c];
+                var dev = EvDevDevice.Open(description.Path);
+                EvDevDeviceHandler handler;
+                if (description is EvDevTouchScreenDeviceDescription touch)
+                    handler = new EvDevSingleTouchScreen(dev, touch, info) { InputRoot = _inputRoot };
+                else
+                    throw new Exception("Unknown device description type " + description.GetType().FullName);
+
+                handler.OnEvent += OnRawEvent;
+                _handlers.Add(handler);
+
+                var ev = new epoll_event { events = EPOLLIN, data = { u32 = (uint)c } };
+                epoll_ctl(_epoll, EPOLL_CTL_ADD, dev.Fd, ref ev);
+            }
+
+            new Thread(InputThread) { IsBackground = true }.Start();
+        }
+
+        public void SetInputRoot(IInputRoot root)
+        {
+            _inputRoot = root;
+            foreach (var h in _handlers)
+                h.InputRoot = root;
+        }
+
+
+        public static EvDevBackend CreateFromEnvironment()
+        {
+            var env = Environment.GetEnvironmentVariables();
+            var deviceDescriptions = new List<EvDevDeviceDescription>();
+            foreach (string key in env.Keys)
+            {
+                if (key.StartsWith("AVALONIA_EVDEV_DEVICE_"))
+                {
+                    var value = (string)env[key];
+                    deviceDescriptions.Add(EvDevDeviceDescription.ParseFromEnv(value));
+                }
+            }
+
+            if (deviceDescriptions.Count == 0)
+                throw new Exception(
+                    "No device device description found, specify devices by adding AVALONIA_EVDEV_DEVICE_{name} environment variables");
+
+            return new EvDevBackend(deviceDescriptions.ToArray());
+        }
+    }
+}

+ 65 - 0
src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs

@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.LinuxFramebuffer.Input.EvDev
+{
+    unsafe class EvDevDevice
+    {
+        public int Fd { get; }
+        private IntPtr _dev;
+        public string Name { get; }
+        public List<EvType> EventTypes { get; private set; } = new List<EvType>();
+        public input_absinfo? AbsX { get; }
+        public input_absinfo? AbsY { get; }
+
+        public EvDevDevice(int fd, IntPtr dev)
+        {
+            Fd = fd;
+            _dev = dev;
+            Name = Marshal.PtrToStringAnsi(NativeUnsafeMethods.libevdev_get_name(_dev));
+            foreach (EvType type in Enum.GetValues(typeof(EvType)))
+            {
+                if (NativeUnsafeMethods.libevdev_has_event_type(dev, type) != 0)
+                    EventTypes.Add(type);
+            }
+            var ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int) AbsAxis.ABS_X);
+            if (ptr != null)
+                AbsX = *ptr;
+            ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int)AbsAxis.ABS_Y);
+            if (ptr != null)
+                AbsY = *ptr;
+        }
+        
+        public input_event? NextEvent()
+        {
+            input_event ev;
+            if (NativeUnsafeMethods.libevdev_next_event(_dev, 2, out ev) == 0)
+                return ev;
+            return null;
+        }
+
+        public static EvDevDevice Open(string device)
+        {
+            var fd = NativeUnsafeMethods.open(device, 2048, 0);
+            if (fd <= 0)
+                throw new Exception($"Unable to open {device} code {Marshal.GetLastWin32Error()}");
+            IntPtr dev;
+            var rc = NativeUnsafeMethods.libevdev_new_from_fd(fd, out dev);
+            if (rc < 0)
+            {
+                NativeUnsafeMethods.close(fd);
+                throw new Exception($"Unable to initialize evdev for {device} code {Marshal.GetLastWin32Error()}");
+            }
+            return new EvDevDevice(fd, dev);
+        }
+    }
+
+    internal class EvDevAxisInfo
+    {
+        public int Minimum { get; set; }
+        public int Maximum { get; set; }
+    }
+}

+ 38 - 0
src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDeviceDescription.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Linq;
+
+namespace Avalonia.LinuxFramebuffer.Input.EvDev
+{
+    public abstract class EvDevDeviceDescription
+    {
+        protected internal EvDevDeviceDescription()
+        {
+            
+        }
+        
+        public string Path { get; set; }
+
+        internal static EvDevDeviceDescription ParseFromEnv(string env)
+        {
+            var formatEx = new ArgumentException(
+                "Invalid device format, expected `(path):type=(touchscreen):[calibration=m11,m12,m21,m22,m31,m32]");
+
+
+            var items = env.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
+            if (items.Length < 2)
+                throw formatEx;
+            var path = items[0];
+            var dic = items.Skip(1)
+                .Select(i => i.Split(new[] { '=' }, 2))
+                .ToDictionary(x => x[0], x => x[1]);
+
+            if (!dic.TryGetValue("type", out var type))
+                throw formatEx;
+
+            if (type == "touchscreen")
+                return EvDevTouchScreenDeviceDescription.ParseFromEnv(path, dic);
+            
+            throw formatEx;
+        }
+    }
+}

+ 114 - 0
src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs

@@ -0,0 +1,114 @@
+using System;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+
+namespace Avalonia.LinuxFramebuffer.Input.EvDev
+{
+    internal class EvDevSingleTouchScreen : EvDevDeviceHandler
+    {
+        private readonly IScreenInfoProvider _screenInfo;
+        private readonly int _width, _height;
+        private readonly Matrix _calibration;
+        private input_absinfo _axisX;
+        private input_absinfo _axisY;
+        private TouchDevice _device = new TouchDevice();
+
+        private int _currentX, _currentY;
+        private bool _hasMovement;
+        private bool? _pressAction;
+
+        public EvDevSingleTouchScreen(EvDevDevice device, EvDevTouchScreenDeviceDescription description,
+            IScreenInfoProvider screenInfo) : base(device)
+        {
+            if (device.AbsX == null || device.AbsY == null)
+                throw new ArgumentException("Device is not a touchscreen");
+            _screenInfo = screenInfo;
+
+            _calibration = description.CalibrationMatrix;
+            _axisX = device.AbsX.Value;
+            _axisY = device.AbsY.Value;
+        }
+
+        protected override void HandleEvent(input_event ev)
+        {
+            if (ev.Type == EvType.EV_ABS)
+            {
+                if (ev.Axis == AbsAxis.ABS_X)
+                {
+                    _currentX = ev.value;
+                    _hasMovement = true;
+                }
+
+                if (ev.Axis == AbsAxis.ABS_Y)
+                {
+                    _currentY = ev.value;
+                    _hasMovement = true;
+                }
+            }
+
+            if (ev.Type == EvType.EV_KEY)
+            {
+                if (ev.Key == EvKey.BTN_TOUCH)
+                {
+                    _pressAction = ev.value != 0;
+                }
+            }
+            
+            if (ev.Type == EvType.EV_SYN)
+            {
+                if (_pressAction != null)
+                    RaiseEvent(_pressAction == true ? RawPointerEventType.TouchBegin : RawPointerEventType.TouchEnd,
+                        ev.Timestamp);
+                else if(_hasMovement)
+                    RaiseEvent(RawPointerEventType.TouchUpdate, ev.Timestamp);
+                _hasMovement = false;
+                _pressAction = null;
+            }
+        }
+
+        void RaiseEvent(RawPointerEventType type, ulong timestamp)
+        {
+            var point = new Point(_currentX, _currentY);
+
+            var touchWidth = _axisX.maximum - _axisX.minimum;
+            var touchHeight = _axisY.maximum - _axisY.minimum;
+
+            var screenSize = _screenInfo.ScaledSize;
+            
+            // Normalize to 0-(max-min)
+            point -= new Point(_axisX.minimum, _axisY.minimum);
+
+            // Apply calibration matrix
+            point *= _calibration;
+            
+            // Transform to display pixel grid 
+            point = new Point(point.X * screenSize.Width / touchWidth, point.Y * screenSize.Height / touchHeight);
+            
+            RaiseEvent(new RawTouchEventArgs(_device, timestamp, InputRoot,
+                type, point, RawInputModifiers.None, 1));
+        }
+    }
+
+    internal abstract class EvDevDeviceHandler
+    {
+        public event Action<RawInputEventArgs> OnEvent;
+        public EvDevDeviceHandler(EvDevDevice device)
+        {
+            Device = device;
+        }
+
+        public EvDevDevice Device { get; }
+        public IInputRoot InputRoot { get; set; }
+
+        public void HandleEvents()
+        {
+            input_event? ev;
+            while ((ev = Device.NextEvent()) != null) 
+                HandleEvent(ev.Value);
+        }
+
+        protected void RaiseEvent(RawInputEventArgs ev) => OnEvent?.Invoke(ev);
+
+        protected abstract void HandleEvent(input_event ev);
+    }
+}

+ 18 - 0
src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreenDeviceDescription.cs

@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace Avalonia.LinuxFramebuffer.Input.EvDev
+{
+    public sealed class EvDevTouchScreenDeviceDescription : EvDevDeviceDescription
+    {
+        public Matrix CalibrationMatrix { get; set; } = Matrix.Identity;
+
+        internal static EvDevTouchScreenDeviceDescription ParseFromEnv(string path, Dictionary<string, string> options)
+        {
+            var calibrationMatrix = Matrix.Identity;
+            if (options.TryGetValue("calibration", out var calibration))
+                calibrationMatrix = Matrix.Parse(calibration);
+            
+            return new EvDevTouchScreenDeviceDescription { Path = path, CalibrationMatrix = calibrationMatrix };
+        }
+    }
+}

+ 19 - 1
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@@ -8,12 +8,15 @@ using Avalonia.Controls.Platform;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.LinuxFramebuffer;
+using Avalonia.LinuxFramebuffer.Input;
+using Avalonia.LinuxFramebuffer.Input.EvDev;
 using Avalonia.LinuxFramebuffer.Input.LibInput;
 using Avalonia.LinuxFramebuffer.Output;
 using Avalonia.OpenGL;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Threading;
+using JetBrains.Annotations;
 
 namespace Avalonia.LinuxFramebuffer
 {
@@ -58,6 +61,7 @@ namespace Avalonia.LinuxFramebuffer
     class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime
     {
         private readonly IOutputBackend _fb;
+        [CanBeNull] private readonly IInputBackend _inputBackend;
         private TopLevel _topLevel;
         private readonly CancellationTokenSource _cts = new CancellationTokenSource();
         public CancellationToken Token => _cts.Token;
@@ -67,6 +71,12 @@ namespace Avalonia.LinuxFramebuffer
             _fb = fb;
         }
         
+        public LinuxFramebufferLifetime(IOutputBackend fb, IInputBackend input)
+        {
+            _fb = fb;
+            _inputBackend = input;
+        }
+        
         public Control MainView
         {
             get => (Control)_topLevel?.Content;
@@ -74,8 +84,16 @@ namespace Avalonia.LinuxFramebuffer
             {
                 if (_topLevel == null)
                 {
+                    var inputBackend = _inputBackend;
+                    if (inputBackend == null)
+                    {
+                        if (Environment.GetEnvironmentVariable("AVALONIA_USE_EVDEV") == "1")
+                            inputBackend = EvDevBackend.CreateFromEnvironment();
+                        else
+                            inputBackend = new LibInputBackend();
+                    }
 
-                    var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, new LibInputBackend()));
+                    var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, inputBackend));
                     tl.Prepare();
                     _topLevel = tl;
                     _topLevel.Renderer.Start();

+ 50 - 3
src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs

@@ -50,6 +50,19 @@ namespace Avalonia.LinuxFramebuffer
         public static extern IntPtr libevdev_get_name(IntPtr dev);
         [DllImport("libevdev.so.2", EntryPoint = "libevdev_get_abs_info", SetLastError = true)]
         public static extern input_absinfo* libevdev_get_abs_info(IntPtr dev, int code);
+        
+        [DllImport("libc")]
+        public extern static int epoll_create1(int size);
+
+        [DllImport("libc")]
+        public extern static int epoll_ctl(int epfd, int op, int fd, ref epoll_event __event);
+
+        [DllImport("libc")]
+        public extern static int epoll_wait(int epfd, epoll_event* events, int maxevents, int timeout);
+        
+        public const int EPOLLIN = 1;
+        public const int EPOLL_CTL_ADD = 1;
+        public const int O_NONBLOCK = 2048;
     }
 
     [StructLayout(LayoutKind.Sequential)]
@@ -190,9 +203,22 @@ namespace Avalonia.LinuxFramebuffer
     [StructLayout(LayoutKind.Sequential)]
     struct input_event
     {
-        private IntPtr crap1, crap2;
-        public ushort type, code;
+        private IntPtr timeval1, timeval2;
+        public ushort _type, _code;
         public int value;
+        public EvType Type => (EvType)_type;
+        public EvKey Key => (EvKey)_code;
+        public AbsAxis Axis => (AbsAxis)_code;
+
+        public ulong Timestamp
+        {
+            get
+            {
+                var ms = (ulong)timeval2.ToInt64() / 1000;
+                var s = (ulong)timeval1.ToInt64() * 1000;
+                return s + ms;
+            }
+        }
     }
 
     [StructLayout(LayoutKind.Sequential)]
@@ -249,7 +275,8 @@ namespace Avalonia.LinuxFramebuffer
     {
         BTN_LEFT = 0x110,
         BTN_RIGHT = 0x111,
-        BTN_MIDDLE = 0x112
+        BTN_MIDDLE = 0x112,
+        BTN_TOUCH = 0x14a
     }
 
     [StructLayout(LayoutKind.Sequential)]
@@ -263,4 +290,24 @@ namespace Avalonia.LinuxFramebuffer
         public __s32 resolution;
 
     }
+    
+    [StructLayout(LayoutKind.Explicit)]
+    struct epoll_data
+    {
+        [FieldOffset(0)]
+        public IntPtr ptr;
+        [FieldOffset(0)]
+        public int fd;
+        [FieldOffset(0)]
+        public uint u32;
+        [FieldOffset(0)]
+        public ulong u64;
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    struct epoll_event
+    {
+        public uint events;
+        public epoll_data data;
+    }
 }