[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.Diagnostics;
using System.Runtime.InteropServices;
using OpenTK.Input;
namespace OpenTK.Platform.Linux
@ -36,6 +37,10 @@ namespace OpenTK.Platform.Linux
// Bindings for linux/input.h
class Evdev
{
public const int KeyCount = 0x300;
public const int AxisCount = 0x40;
public const int EventCount = (int)EvdevType.CNT;
#region KeyMap
public static readonly Key[] KeyMap = new Key[]
@ -365,9 +370,101 @@ namespace OpenTK.Platform.Linux
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;
}
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 EvdevButton : uint
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,
BTN0 = 0x100,
@ -447,6 +544,64 @@ namespace OpenTK.Platform.Linux
WHEEL = 0x150,
GEAR_DOWN = 0x150,
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);
[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)]
public static extern int ioctl(int d, KeyboardIoctlCode request, ref IntPtr data);
@ -106,6 +109,14 @@ namespace OpenTK.Platform.Linux
InvalidValue = 22,
}
[Flags]
enum DirectionFlags
{
None = 0,
Write = 1,
Read = 2
}
[Flags]
enum OpenFlags
{
@ -116,11 +127,6 @@ namespace OpenTK.Platform.Linux
CloseOnExec = 0x0080000
}
enum EvdevIoctlCode : uint
{
Id = ((byte)'E' << 8) | (0x02 << 0) //EVIOCGID, which is _IOR('E', 0x02, struct input_id)
}
[Flags]
enum JoystickEventType : byte
{

View file

@ -35,14 +35,23 @@ using OpenTK.Input;
namespace OpenTK.Platform.Linux
{
struct LinuxJoyDetails
class LinuxJoystickDetails
{
public Guid Guid;
public string Name;
public int FileDescriptor;
public int PathIndex; // e.g. "0" for "/dev/input/event0". Used as a hardware id
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
{
#region Fields
@ -51,8 +60,8 @@ namespace OpenTK.Platform.Linux
readonly FileSystemWatcher watcher = new FileSystemWatcher();
readonly Dictionary<int, int> index_to_stick = new Dictionary<int, int>();
List<JoystickDevice<LinuxJoyDetails>> sticks = new List<JoystickDevice<LinuxJoyDetails>>();
readonly DeviceCollection<LinuxJoystickDetails> Sticks =
new DeviceCollection<LinuxJoystickDetails>();
bool disposed;
@ -89,12 +98,10 @@ namespace OpenTK.Platform.Linux
{
foreach (string file in Directory.GetFiles(path))
{
JoystickDevice<LinuxJoyDetails> stick = OpenJoystick(file);
LinuxJoystickDetails stick = OpenJoystick(file);
if (stick != null)
{
//stick.Description = String.Format("USB Joystick {0} ({1} axes, {2} buttons, {3}{0})",
//number, stick.Axis.Count, stick.Button.Count, path);
sticks.Add(stick);
Sticks.Add(stick.PathIndex, stick);
}
}
}
@ -102,10 +109,11 @@ namespace OpenTK.Platform.Linux
int GetJoystickNumber(string path)
{
if (path.StartsWith("js"))
const string evdev = "event";
if (path.StartsWith(evdev))
{
int num;
if (Int32.TryParse(path.Substring(2), out num))
if (Int32.TryParse(path.Substring(evdev.Length), out num))
{
return num;
}
@ -129,23 +137,11 @@ namespace OpenTK.Platform.Linux
int number = GetJoystickNumber(file);
if (number != -1)
{
// Find which joystick id matches this number
int i;
for (i = 0; i < sticks.Count; i++)
var stick = Sticks.FromHardwareId(number);
if (stick != null)
{
if (sticks[i].Id == number)
{
break;
}
}
if (i == sticks.Count)
{
Debug.Print("[Evdev] Joystick id {0} does not exist.", number);
}
else
{
CloseJoystick(sticks[i]);
CloseJoystick(stick);
Sticks.TryRemove(number);
}
}
}
@ -155,75 +151,129 @@ namespace OpenTK.Platform.Linux
#region Private Members
Guid CreateGuid(JoystickDevice<LinuxJoyDetails> js, string path, int number)
Guid CreateGuid(EvdevInputId id, string name)
{
byte[] bytes = new byte[16];
for (int i = 0; i < Math.Min(bytes.Length, js.Description.Length); i++)
int i = 0;
byte[] bus = BitConverter.GetBytes(id.BusType);
bytes[i++] = bus[0];
bytes[i++] = bus[1];
bytes[i++] = 0;
bytes[i++] = 0;
if (id.Vendor != 0 && id.Product != 0 && id.Version != 0)
{
bytes[i] = (byte)js.Description[i];
byte[] vendor = BitConverter.GetBytes(id.Vendor);
byte[] product = BitConverter.GetBytes(id.Product);
byte[] version = BitConverter.GetBytes(id.Version);
bytes[i++] = vendor[0];
bytes[i++] = vendor[1];
bytes[i++] = 0;
bytes[i++] = 0;
bytes[i++] = product[0];
bytes[i++] = product[1];
bytes[i++] = 0;
bytes[i++] = 0;
bytes[i++] = version[0];
bytes[i++] = version[1];
bytes[i++] = 0;
bytes[i++] = 0;
}
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
else
{
EventInputId id;
if (UnsafeNativeMethods.ioctl(event_fd, EvdevInputId.Id, out id) < 0)
return new Guid();
int i = 0;
byte[] bus = BitConverter.GetBytes(id.BusType);
bytes[i++] = bus[0];
bytes[i++] = bus[1];
bytes[i++] = 0;
bytes[i++] = 0;
if (id.Vendor != 0 && id.Product != 0 && id.Version != 0)
for (int j = 0; j < bytes.Length - i; j++)
{
byte[] vendor = BitConverter.GetBytes(id.Vendor);
byte[] product = BitConverter.GetBytes(id.Product);
byte[] version = BitConverter.GetBytes(id.Version);
bytes[i++] = vendor[0];
bytes[i++] = vendor[1];
bytes[i++] = 0;
bytes[i++] = 0;
bytes[i++] = product[0];
bytes[i++] = product[1];
bytes[i++] = 0;
bytes[i++] = 0;
bytes[i++] = version[0];
bytes[i++] = version[1];
bytes[i++] = 0;
bytes[i++] = 0;
bytes[i + j] = (byte)name[j];
}
}
return new Guid(bytes);
}
unsafe static bool TestBit(byte* ptr, int bit)
{
int byte_offset = bit / 8;
int bit_offset = bit % 8;
return (*(ptr + byte_offset) & (1 << bit_offset)) != 0;
}
unsafe static int AddAxes(LinuxJoystickDetails stick, byte* axisbit, int bytecount)
{
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
{
for (; i < bytes.Length; i++)
{
bytes[i] = (byte)js.Description[i];
}
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;
}
return new Guid(bytes);
if (TestBit(keybit, (int)button))
{
stick.ButtonMap.Add(button, buttons++);
}
else
{
stick.ButtonMap.Add(button, (JoystickButton)(-1));
}
}
finally
{
UnsafeNativeMethods.close(event_fd);
}
#endif
return (int)buttons;
}
JoystickDevice<LinuxJoyDetails> OpenJoystick(string path)
unsafe static int AddHats(LinuxJoystickDetails stick,
byte* axisbit, int axiscount,
byte* keybit, int keycount)
{
JoystickDevice<LinuxJoyDetails> stick = null;
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));
if (number >= 0)
@ -235,123 +285,114 @@ namespace OpenTK.Platform.Linux
if (fd == -1)
return null;
// Check joystick driver version (must be 1.0+)
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++)
unsafe
{
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);
}
catch (Exception e)
{
Debug.Print("Error opening joystick: {0}", e.ToString());
}
finally
{
if (stick == null && fd != -1)
{
// Not a joystick
Libc.close(fd);
}
}
}
return stick;
}
void CloseJoystick(JoystickDevice<LinuxJoyDetails> js)
void CloseJoystick(LinuxJoystickDetails js)
{
Libc.close(js.Details.FileDescriptor);
js.Details.State = new JoystickState(); // clear joystick state
js.Details.FileDescriptor = -1;
// find and remove the joystick index from index_to_stick
int key = -1;
foreach (int i in index_to_stick.Keys)
{
if (sticks[index_to_stick[i]] == js)
{
key = i;
break;
}
}
Sticks.Remove(js.FileDescriptor);
if (index_to_stick.ContainsKey(key))
{
index_to_stick.Remove(key);
}
Libc.close(js.FileDescriptor);
js.FileDescriptor = -1;
js.State = new JoystickState(); // clear joystick state
js.Caps = new JoystickCapabilities();
}
void PollJoystick(JoystickDevice<LinuxJoyDetails> js)
void PollJoystick(LinuxJoystickDetails js)
{
JoystickEvent e;
unsafe
{
while ((long)Libc.read(js.Details.FileDescriptor, (void*)&e, (UIntPtr)sizeof(JoystickEvent)) > 0)
const int EventCount = 32;
InputEvent* events = stackalloc InputEvent[EventCount];
long length = 0;
while (true)
{
e.Type &= ~JoystickEventType.Init;
length = (long)Libc.read(js.FileDescriptor, (void*)events, (UIntPtr)(sizeof(InputEvent) * EventCount));
if (length <= 0)
break;
switch (e.Type)
length /= sizeof(InputEvent);
for (int i = 0; i < length; i++)
{
case JoystickEventType.Axis:
// Flip vertical axes so that +1 point up.
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;
InputEvent *e = events + i;
switch (e->Type)
{
case EvdevType.ABS:
break;
case JoystickEventType.Button:
js.Details.State.SetButton((JoystickButton)e.Number, e.Value != 0);
break;
case EvdevType.KEY:
break;
}
//js.State.SetPacketNumber(unchecked((int)e->Time.Seconds));
}
js.Details.State.SetPacketNumber(unchecked((int)e.Time));
}
}
}
bool IsValid(int index)
{
return index_to_stick.ContainsKey(index);
}
static readonly string JoystickPath = "/dev/input";
static readonly string JoystickPathLegacy = "/dev";
@ -374,7 +415,7 @@ namespace OpenTK.Platform.Linux
}
watcher.Dispose();
foreach (JoystickDevice<LinuxJoyDetails> js in sticks)
foreach (LinuxJoystickDetails js in Sticks)
{
CloseJoystick(js);
}
@ -394,39 +435,33 @@ namespace OpenTK.Platform.Linux
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);
return js.Details.State;
return js.State;
}
return new JoystickState();
}
JoystickCapabilities IJoystickDriver2.GetCapabilities(int index)
{
JoystickCapabilities caps = new JoystickCapabilities();
if (IsValid(index))
LinuxJoystickDetails js = Sticks.FromIndex(index);
if (js != null)
{
JoystickDevice<LinuxJoyDetails> js = sticks[index_to_stick[index]];
caps = new JoystickCapabilities(
js.Axis.Count,
js.Button.Count,
0, // hats not supported by /dev/js
js.Details.State.IsConnected);
return js.Caps;
}
return caps;
return new JoystickCapabilities();
}
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.Details.Guid;
return js.Guid;
}
return new Guid();
return Guid.Empty;
}
#endregion