[Linux] Implemented evdev joystick device discovery

This commit is contained in:
thefiddler 2014-08-07 18:08:53 +02:00
parent 41f1f92cdf
commit 397bdda076
3 changed files with 386 additions and 190 deletions

View file

@ -29,6 +29,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices;
using OpenTK.Input; using OpenTK.Input;
namespace OpenTK.Platform.Linux namespace OpenTK.Platform.Linux
@ -36,6 +37,10 @@ namespace OpenTK.Platform.Linux
// Bindings for linux/input.h // Bindings for linux/input.h
class Evdev class Evdev
{ {
public const int KeyCount = 0x300;
public const int AxisCount = 0x40;
public const int EventCount = (int)EvdevType.CNT;
#region KeyMap #region KeyMap
public static readonly Key[] KeyMap = new Key[] public static readonly Key[] KeyMap = new Key[]
@ -365,9 +370,101 @@ namespace OpenTK.Platform.Linux
return MouseButton.Left; return MouseButton.Left;
} }
} }
static uint IOCreate(DirectionFlags dir, int number, int length)
{
long v =
((byte)dir << 30) |
((byte)'E' << 8) |
(number << 0) |
(length << 16);
return (uint)v;
} }
enum EvdevButton : uint public static int GetBit(int fd, EvdevType ev, int length, IntPtr data)
{
// EVIOCGBIT = _IOC(_IOC_READ, 'E', 0x20 + (ev), len)
uint ioctl = IOCreate(DirectionFlags.Read, (int)ev + 0x20, length);
int retval = Libc.ioctl(fd, ioctl, data);
return retval;
}
public static int GetName(int fd, out string name)
{
unsafe
{
sbyte* pname = stackalloc sbyte[129];
int ret = Libc.ioctl(fd, EvdevIoctl.Name128, new IntPtr(pname));
name = new string(pname);
return ret;
}
}
public static int GetId(int fd, out EvdevInputId id)
{
id = default(EvdevInputId);
unsafe
{
fixed (EvdevInputId* pid = &id)
{
return Libc.ioctl(fd, EvdevIoctl.Id, new IntPtr(pid));
}
}
}
}
enum EvdevAxis
{
X = 0x00,
Y = 0x01,
Z = 0x02,
RX = 0x03,
RY = 0x04,
RZ = 0x05,
THROTTLE = 0x06,
RUDDER = 0x07,
WHEEL = 0x08,
GAS = 0x09,
BRAKE = 0x0a,
HAT0X = 0x10,
HAT0Y = 0x11,
HAT1X = 0x12,
HAT1Y = 0x13,
HAT2X = 0x14,
HAT2Y = 0x15,
HAT3X = 0x16,
HAT3Y = 0x17,
PRESSURE = 0x18,
DISTANCE = 0x19,
TILT_X = 0x1a,
TILT_Y = 0x1b,
TOOL_WIDTH = 0x1c,
VOLUME = 0x20,
MISC = 0x28,
MT_SLOT = 0x2f, /* MT slot being modified */
MT_TOUCH_MAJOR = 0x30, /* Major axis of touching ellipse */
MT_TOUCH_MINOR = 0x31, /* Minor axis (omit if circular) */
MT_WIDTH_MAJOR = 0x32, /* Major axis of approaching ellipse */
MT_WIDTH_MINOR = 0x33, /* Minor axis (omit if circular) */
MT_ORIENTATION = 0x34, /* Ellipse orientation */
MT_POSITION_X = 0x35, /* Center X touch position */
MT_POSITION_Y = 0x36, /* Center Y touch position */
MT_TOOL_TYPE = 0x37, /* Type of touching device */
MT_BLOB_ID = 0x38, /* Group a set of packets as a blob */
MT_TRACKING_ID = 0x39, /* Unique ID of initiated contact */
MT_PRESSURE = 0x3a, /* Pressure on contact area */
MT_DISTANCE = 0x3b, /* Contact hover distance */
MT_TOOL_X = 0x3c, /* Center X tool position */
MT_TOOL_Y = 0x3d, /* Center Y tool position */
MAX = 0x3f,
CNT = (MAX+1),
}
enum EvdevButton
{ {
MISC = 0x100, MISC = 0x100,
BTN0 = 0x100, BTN0 = 0x100,
@ -447,6 +544,64 @@ namespace OpenTK.Platform.Linux
WHEEL = 0x150, WHEEL = 0x150,
GEAR_DOWN = 0x150, GEAR_DOWN = 0x150,
GEAR_UP = 0x151, GEAR_UP = 0x151,
DPAD_UP = 0x220,
DPAD_DOWN = 0x221,
DPAD_LEFT = 0x222,
DPAD_RIGHT = 0x223,
Last = 0x300,
}
enum EvdevType : byte
{
SYN = 0x00,
KEY = 0x01,
REL = 0x02,
ABS = 0x03,
MSC = 0x04,
SW = 0x05,
LED = 0x11,
SND = 0x12,
REP = 0x14,
FF = 0x15,
PWR = 0x16,
FF_STATUS = 0x17,
MAX = 0x1f,
CNT = (MAX+1),
}
enum EvdevIoctl : uint
{
Id = (2u << 30) | ((byte)'E' << 8) | (0x02u << 0) | (8u << 16), //EVIOCGID = _IOR('E', 0x02, struct input_id)
Name128 = (2u << 30) | ((byte)'E' << 8) | (0x06u << 0) | (128u << 16), //EVIOCGNAME(len) = _IOC(_IOC_READ, 'E', 0x06, len)
}
[StructLayout(LayoutKind.Sequential)]
struct InputId
{
public ushort BusType;
public ushort Vendor;
public ushort Product;
public ushort Version;
}
[StructLayout(LayoutKind.Sequential)]
struct InputEvent
{
public TimeVal Time;
ushort type;
public ushort Code;
public int Value;
public EvdevType Type { get { return (EvdevType)type; } }
}
[StructLayout(LayoutKind.Sequential)]
struct TimeVal
{
public IntPtr Seconds;
public IntPtr MicroSeconds;
} }
} }

View file

@ -52,7 +52,10 @@ namespace OpenTK.Platform.Linux
public static extern int ioctl(int d, JoystickIoctlCode request, StringBuilder data); public static extern int ioctl(int d, JoystickIoctlCode request, StringBuilder data);
[DllImport(lib)] [DllImport(lib)]
public static extern int ioctl(int d, EvdevIoctlCode request, out EvdevInputId data); public static extern int ioctl(int d, EvdevIoctl request, [Out] IntPtr data);
[DllImport(lib)]
public static extern int ioctl(int d, uint request, [Out] IntPtr data);
[DllImport(lib)] [DllImport(lib)]
public static extern int ioctl(int d, KeyboardIoctlCode request, ref IntPtr data); public static extern int ioctl(int d, KeyboardIoctlCode request, ref IntPtr data);
@ -106,6 +109,14 @@ namespace OpenTK.Platform.Linux
InvalidValue = 22, InvalidValue = 22,
} }
[Flags]
enum DirectionFlags
{
None = 0,
Write = 1,
Read = 2
}
[Flags] [Flags]
enum OpenFlags enum OpenFlags
{ {
@ -116,11 +127,6 @@ namespace OpenTK.Platform.Linux
CloseOnExec = 0x0080000 CloseOnExec = 0x0080000
} }
enum EvdevIoctlCode : uint
{
Id = ((byte)'E' << 8) | (0x02 << 0) //EVIOCGID, which is _IOR('E', 0x02, struct input_id)
}
[Flags] [Flags]
enum JoystickEventType : byte enum JoystickEventType : byte
{ {

View file

@ -35,14 +35,23 @@ using OpenTK.Input;
namespace OpenTK.Platform.Linux namespace OpenTK.Platform.Linux
{ {
struct LinuxJoyDetails class LinuxJoystickDetails
{ {
public Guid Guid; public Guid Guid;
public string Name;
public int FileDescriptor; public int FileDescriptor;
public int PathIndex; // e.g. "0" for "/dev/input/event0". Used as a hardware id
public JoystickState State; public JoystickState State;
public JoystickCapabilities Caps;
public readonly Dictionary<EvdevAxis, JoystickAxis> AxisMap =
new Dictionary<EvdevAxis, JoystickAxis>();
public readonly Dictionary<EvdevButton, JoystickButton> ButtonMap =
new Dictionary<EvdevButton, JoystickButton>();
public readonly Dictionary<int, JoystickHat> HatMap =
new Dictionary<int, JoystickHat>();
} }
// Note: despite what the name says, this class is Linux-specific.
sealed class LinuxJoystick : IJoystickDriver2 sealed class LinuxJoystick : IJoystickDriver2
{ {
#region Fields #region Fields
@ -51,8 +60,8 @@ namespace OpenTK.Platform.Linux
readonly FileSystemWatcher watcher = new FileSystemWatcher(); readonly FileSystemWatcher watcher = new FileSystemWatcher();
readonly Dictionary<int, int> index_to_stick = new Dictionary<int, int>(); readonly DeviceCollection<LinuxJoystickDetails> Sticks =
List<JoystickDevice<LinuxJoyDetails>> sticks = new List<JoystickDevice<LinuxJoyDetails>>(); new DeviceCollection<LinuxJoystickDetails>();
bool disposed; bool disposed;
@ -89,12 +98,10 @@ namespace OpenTK.Platform.Linux
{ {
foreach (string file in Directory.GetFiles(path)) foreach (string file in Directory.GetFiles(path))
{ {
JoystickDevice<LinuxJoyDetails> stick = OpenJoystick(file); LinuxJoystickDetails stick = OpenJoystick(file);
if (stick != null) if (stick != null)
{ {
//stick.Description = String.Format("USB Joystick {0} ({1} axes, {2} buttons, {3}{0})", Sticks.Add(stick.PathIndex, stick);
//number, stick.Axis.Count, stick.Button.Count, path);
sticks.Add(stick);
} }
} }
} }
@ -102,10 +109,11 @@ namespace OpenTK.Platform.Linux
int GetJoystickNumber(string path) int GetJoystickNumber(string path)
{ {
if (path.StartsWith("js")) const string evdev = "event";
if (path.StartsWith(evdev))
{ {
int num; int num;
if (Int32.TryParse(path.Substring(2), out num)) if (Int32.TryParse(path.Substring(evdev.Length), out num))
{ {
return num; return num;
} }
@ -129,23 +137,11 @@ namespace OpenTK.Platform.Linux
int number = GetJoystickNumber(file); int number = GetJoystickNumber(file);
if (number != -1) if (number != -1)
{ {
// Find which joystick id matches this number var stick = Sticks.FromHardwareId(number);
int i; if (stick != null)
for (i = 0; i < sticks.Count; i++)
{ {
if (sticks[i].Id == number) CloseJoystick(stick);
{ Sticks.TryRemove(number);
break;
}
}
if (i == sticks.Count)
{
Debug.Print("[Evdev] Joystick id {0} does not exist.", number);
}
else
{
CloseJoystick(sticks[i]);
} }
} }
} }
@ -155,29 +151,9 @@ namespace OpenTK.Platform.Linux
#region Private Members #region Private Members
Guid CreateGuid(JoystickDevice<LinuxJoyDetails> js, string path, int number) Guid CreateGuid(EvdevInputId id, string name)
{ {
byte[] bytes = new byte[16]; byte[] bytes = new byte[16];
for (int i = 0; i < Math.Min(bytes.Length, js.Description.Length); i++)
{
bytes[i] = (byte)js.Description[i];
}
return new Guid(bytes);
#if false // Todo: move to /dev/input/event* from /dev/input/js*
string evdev_path = Path.Combine(Path.GetDirectoryName(path), "event" + number);
if (!File.Exists(evdev_path))
return new Guid();
int event_fd = UnsafeNativeMethods.open(evdev_path, OpenFlags.NonBlock);
if (event_fd < 0)
return new Guid();
try
{
EventInputId id;
if (UnsafeNativeMethods.ioctl(event_fd, EvdevInputId.Id, out id) < 0)
return new Guid();
int i = 0; int i = 0;
byte[] bus = BitConverter.GetBytes(id.BusType); byte[] bus = BitConverter.GetBytes(id.BusType);
@ -206,24 +182,98 @@ namespace OpenTK.Platform.Linux
} }
else else
{ {
for (; i < bytes.Length; i++) for (int j = 0; j < bytes.Length - i; j++)
{ {
bytes[i] = (byte)js.Description[i]; bytes[i + j] = (byte)name[j];
} }
} }
return new Guid(bytes); return new Guid(bytes);
} }
finally
unsafe static bool TestBit(byte* ptr, int bit)
{ {
UnsafeNativeMethods.close(event_fd); int byte_offset = bit / 8;
} int bit_offset = bit % 8;
#endif return (*(ptr + byte_offset) & (1 << bit_offset)) != 0;
} }
JoystickDevice<LinuxJoyDetails> OpenJoystick(string path) unsafe static int AddAxes(LinuxJoystickDetails stick, byte* axisbit, int bytecount)
{ {
JoystickDevice<LinuxJoyDetails> stick = null; JoystickAxis axes = 0;
JoystickHat hats = 0;
int bitcount = bytecount * 8;
for (EvdevAxis axis = 0; axis < EvdevAxis.CNT && (int)axis < bitcount; axis++)
{
if (axis >= EvdevAxis.HAT0X && axis <= EvdevAxis.HAT3Y)
{
// Axis is analogue hat - skip
continue;
}
if (TestBit(axisbit, (int)axis))
{
stick.AxisMap.Add(axis, axes++);
}
else
{
stick.AxisMap.Add(axis, (JoystickAxis)(-1));
}
}
return (int)axes;
}
unsafe static int AddButtons(LinuxJoystickDetails stick, byte* keybit, int bytecount)
{
JoystickButton buttons = 0;
int bitcount = bytecount * 8;
for (EvdevButton button = 0; button < EvdevButton.Last && (int)button < bitcount; button++)
{
if (button >= EvdevButton.DPAD_UP && button <= EvdevButton.DPAD_RIGHT)
{
// Button is dpad (hat) - skip
continue;
}
if (TestBit(keybit, (int)button))
{
stick.ButtonMap.Add(button, buttons++);
}
else
{
stick.ButtonMap.Add(button, (JoystickButton)(-1));
}
}
return (int)buttons;
}
unsafe static int AddHats(LinuxJoystickDetails stick,
byte* axisbit, int axiscount,
byte* keybit, int keycount)
{
JoystickHat hats = 0;
for (EvdevAxis hat = EvdevAxis.HAT0X; hat < EvdevAxis.HAT3Y && (int)hat < axiscount * 8; hat++)
{
if (TestBit(axisbit, (int)hat))
{
stick.HatMap.Add((int)hat, hats++);
}
}
for (EvdevButton dpad = EvdevButton.DPAD_UP; dpad < EvdevButton.DPAD_RIGHT && (int)dpad < keycount * 8; dpad++)
{
if (TestBit(axisbit, (int)dpad))
{
stick.HatMap.Add((int)dpad, hats++);
}
}
return (int)hats;
}
LinuxJoystickDetails OpenJoystick(string path)
{
LinuxJoystickDetails stick = null;
int number = GetJoystickNumber(Path.GetFileName(path)); int number = GetJoystickNumber(Path.GetFileName(path));
if (number >= 0) if (number >= 0)
@ -235,121 +285,112 @@ namespace OpenTK.Platform.Linux
if (fd == -1) if (fd == -1)
return null; return null;
// Check joystick driver version (must be 1.0+) unsafe
int driver_version = 0x00000800;
Libc.ioctl(fd, JoystickIoctlCode.Version, ref driver_version);
if (driver_version < 0x00010000)
return null;
// Get number of joystick axes
int axes = 0;
Libc.ioctl(fd, JoystickIoctlCode.Axes, ref axes);
// Get number of joystick buttons
int buttons = 0;
Libc.ioctl(fd, JoystickIoctlCode.Buttons, ref buttons);
stick = new JoystickDevice<LinuxJoyDetails>(number, axes, buttons);
StringBuilder sb = new StringBuilder(128);
Libc.ioctl(fd, JoystickIoctlCode.Name128, sb);
stick.Description = sb.ToString();
stick.Details.FileDescriptor = fd;
stick.Details.State.SetIsConnected(true);
stick.Details.Guid = CreateGuid(stick, path, number);
// Find the first disconnected joystick (if any)
int i;
for (i = 0; i < sticks.Count; i++)
{ {
if (!sticks[i].Details.State.IsConnected) const int evsize = Evdev.EventCount / 8;
const int axissize = Evdev.AxisCount / 8;
const int keysize = Evdev.KeyCount / 8;
byte* evbit = stackalloc byte[evsize];
byte* axisbit = stackalloc byte[axissize];
byte* keybit = stackalloc byte[keysize];
string name;
EvdevInputId id;
// Ensure this is a joystick device
bool is_valid = true;
is_valid &= Evdev.GetBit(fd, 0, evsize, new IntPtr(evbit)) >= 0;
is_valid &= Evdev.GetBit(fd, EvdevType.ABS, axissize, new IntPtr(axisbit)) >= 0;
is_valid &= Evdev.GetBit(fd, EvdevType.KEY, keysize, new IntPtr(keybit)) >= 0;
is_valid &= TestBit(evbit, (int)EvdevType.KEY);
is_valid &= TestBit(evbit, (int)EvdevType.ABS);
is_valid &= TestBit(axisbit, (int)EvdevAxis.X);
is_valid &= TestBit(axisbit, (int)EvdevAxis.Y);
is_valid &= Evdev.GetName(fd, out name) >= 0;
is_valid &= Evdev.GetId(fd, out id) >= 0;
if (is_valid)
{ {
break; stick = new LinuxJoystickDetails
{
FileDescriptor = fd,
PathIndex = number,
State = new JoystickState(),
Name = name,
Guid = CreateGuid(id, name),
};
stick.Caps = new JoystickCapabilities(
AddAxes(stick, axisbit, axissize),
AddButtons(stick, keybit, keysize),
AddHats(stick, axisbit, axissize, keybit, keysize),
true);
stick.State.SetIsConnected(true);
} }
} }
// If no disconnected joystick exists, append a new slot
if (i == sticks.Count)
{
sticks.Add(stick);
}
else
{
sticks[i] = stick;
}
// Map player index to joystick
index_to_stick.Add(index_to_stick.Count, i);
Debug.Print("Found joystick on path {0}", path); Debug.Print("Found joystick on path {0}", path);
} }
catch (Exception e)
{
Debug.Print("Error opening joystick: {0}", e.ToString());
}
finally finally
{ {
if (stick == null && fd != -1) if (stick == null && fd != -1)
{
// Not a joystick
Libc.close(fd); Libc.close(fd);
} }
} }
}
return stick; return stick;
} }
void CloseJoystick(JoystickDevice<LinuxJoyDetails> js) void CloseJoystick(LinuxJoystickDetails js)
{ {
Libc.close(js.Details.FileDescriptor); Sticks.Remove(js.FileDescriptor);
js.Details.State = new JoystickState(); // clear joystick state
js.Details.FileDescriptor = -1;
// find and remove the joystick index from index_to_stick Libc.close(js.FileDescriptor);
int key = -1; js.FileDescriptor = -1;
foreach (int i in index_to_stick.Keys) js.State = new JoystickState(); // clear joystick state
{ js.Caps = new JoystickCapabilities();
if (sticks[index_to_stick[i]] == js)
{
key = i;
break;
}
} }
if (index_to_stick.ContainsKey(key)) void PollJoystick(LinuxJoystickDetails js)
{ {
index_to_stick.Remove(key);
}
}
void PollJoystick(JoystickDevice<LinuxJoyDetails> js)
{
JoystickEvent e;
unsafe unsafe
{ {
while ((long)Libc.read(js.Details.FileDescriptor, (void*)&e, (UIntPtr)sizeof(JoystickEvent)) > 0) const int EventCount = 32;
{ InputEvent* events = stackalloc InputEvent[EventCount];
e.Type &= ~JoystickEventType.Init;
switch (e.Type) long length = 0;
while (true)
{ {
case JoystickEventType.Axis: length = (long)Libc.read(js.FileDescriptor, (void*)events, (UIntPtr)(sizeof(InputEvent) * EventCount));
// Flip vertical axes so that +1 point up. if (length <= 0)
if (e.Number % 2 == 0)
js.Details.State.SetAxis((JoystickAxis)e.Number, e.Value);
else
js.Details.State.SetAxis((JoystickAxis)e.Number, unchecked((short)-e.Value));
break; break;
case JoystickEventType.Button: length /= sizeof(InputEvent);
js.Details.State.SetButton((JoystickButton)e.Number, e.Value != 0); for (int i = 0; i < length; i++)
{
InputEvent *e = events + i;
switch (e->Type)
{
case EvdevType.ABS:
break;
case EvdevType.KEY:
break; break;
} }
js.Details.State.SetPacketNumber(unchecked((int)e.Time)); //js.State.SetPacketNumber(unchecked((int)e->Time.Seconds));
} }
} }
} }
bool IsValid(int index)
{
return index_to_stick.ContainsKey(index);
} }
static readonly string JoystickPath = "/dev/input"; static readonly string JoystickPath = "/dev/input";
@ -374,7 +415,7 @@ namespace OpenTK.Platform.Linux
} }
watcher.Dispose(); watcher.Dispose();
foreach (JoystickDevice<LinuxJoyDetails> js in sticks) foreach (LinuxJoystickDetails js in Sticks)
{ {
CloseJoystick(js); CloseJoystick(js);
} }
@ -394,39 +435,33 @@ namespace OpenTK.Platform.Linux
JoystickState IJoystickDriver2.GetState(int index) JoystickState IJoystickDriver2.GetState(int index)
{ {
if (IsValid(index)) LinuxJoystickDetails js = Sticks.FromIndex(index);
if (js != null)
{ {
JoystickDevice<LinuxJoyDetails> js =
sticks[index_to_stick[index]];
PollJoystick(js); PollJoystick(js);
return js.Details.State; return js.State;
} }
return new JoystickState(); return new JoystickState();
} }
JoystickCapabilities IJoystickDriver2.GetCapabilities(int index) JoystickCapabilities IJoystickDriver2.GetCapabilities(int index)
{ {
JoystickCapabilities caps = new JoystickCapabilities(); LinuxJoystickDetails js = Sticks.FromIndex(index);
if (IsValid(index)) if (js != null)
{ {
JoystickDevice<LinuxJoyDetails> js = sticks[index_to_stick[index]]; return js.Caps;
caps = new JoystickCapabilities(
js.Axis.Count,
js.Button.Count,
0, // hats not supported by /dev/js
js.Details.State.IsConnected);
} }
return caps; return new JoystickCapabilities();
} }
Guid IJoystickDriver2.GetGuid(int index) Guid IJoystickDriver2.GetGuid(int index)
{ {
if (IsValid(index)) LinuxJoystickDetails js = Sticks.FromIndex(index);
if (js != null)
{ {
JoystickDevice<LinuxJoyDetails> js = sticks[index_to_stick[index]]; return js.Guid;
return js.Details.Guid;
} }
return new Guid(); return Guid.Empty;
} }
#endregion #endregion