Browse Source

Added support for single-touch screens directly via evdev

pull/5894/head
Nikita Tsukanov 5 years ago
parent
commit
cb636c5021
  1. 141
      src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs
  2. 65
      src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs
  3. 38
      src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDeviceDescription.cs
  4. 114
      src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs
  5. 18
      src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreenDeviceDescription.cs
  6. 20
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  7. 53
      src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs

141
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
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
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
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
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 };
}
}
}

20
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();

53
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;
}
}

Loading…
Cancel
Save