#region License // // The Open Toolkit Library License // // Copyright (c) 2006 - 2008 the Open Toolkit library, except where noted. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text; using OpenTK.Input; namespace OpenTK.Platform.Linux { struct AxisInfo { public JoystickAxis Axis; public InputAbsInfo Info; } 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 AxisMap = new Dictionary(); public readonly Dictionary ButtonMap = new Dictionary(); } sealed class LinuxJoystick : IJoystickDriver2 { #region Fields static readonly HatPosition[,] HatPositions = new HatPosition[,] { { HatPosition.UpLeft, HatPosition.Up, HatPosition.UpRight }, { HatPosition.Left, HatPosition.Centered, HatPosition.Right }, { HatPosition.DownLeft, HatPosition.Down, HatPosition.DownRight } }; readonly object sync = new object(); readonly FileSystemWatcher watcher = new FileSystemWatcher(); readonly DeviceCollection Sticks = new DeviceCollection(); bool disposed; #endregion #region Constructors public LinuxJoystick() { string path = Directory.Exists(JoystickPath) ? JoystickPath : Directory.Exists(JoystickPathLegacy) ? JoystickPathLegacy : String.Empty; if (!String.IsNullOrEmpty(path)) { watcher.Path = path; watcher.Created += JoystickAdded; watcher.Deleted += JoystickRemoved; watcher.EnableRaisingEvents = true; OpenJoysticks(path); } } #endregion #region Private Members void OpenJoysticks(string path) { lock (sync) { foreach (string file in Directory.GetFiles(path)) { LinuxJoystickDetails stick = OpenJoystick(file); if (stick != null) { Sticks.Add(stick.PathIndex, stick); } } } } int GetJoystickNumber(string path) { const string evdev = "event"; if (path.StartsWith(evdev)) { int num; if (Int32.TryParse(path.Substring(evdev.Length), out num)) { return num; } } return -1; } void JoystickAdded(object sender, FileSystemEventArgs e) { lock (sync) { OpenJoystick(e.FullPath); } } void JoystickRemoved(object sender, FileSystemEventArgs e) { lock (sync) { string file = Path.GetFileName(e.FullPath); int number = GetJoystickNumber(file); if (number != -1) { var stick = Sticks.FromHardwareId(number); if (stick != null) { CloseJoystick(stick); } } } } #endregion #region Private Members Guid CreateGuid(EvdevInputId id, string name) { // Note: the first 8bytes of the Guid are byteswapped // in three parts when using `new Guid(byte[])`: // (int, short, short). // We need to take that into account to match the expected // Guid in the database. Ugh. byte[] bytes = new byte[16]; int i = 0; byte[] bus = BitConverter.GetBytes((int)id.BusType); bytes[i++] = bus[3]; bytes[i++] = bus[2]; bytes[i++] = bus[1]; bytes[i++] = bus[0]; if (id.Vendor != 0 && id.Product != 0 && id.Version != 0) { byte[] vendor = BitConverter.GetBytes(id.Vendor); byte[] product = BitConverter.GetBytes(id.Product); byte[] version = BitConverter.GetBytes(id.Version); bytes[i++] = vendor[1]; bytes[i++] = vendor[0]; bytes[i++] = 0; bytes[i++] = 0; bytes[i++] = product[0]; // no byteswapping bytes[i++] = product[1]; bytes[i++] = 0; bytes[i++] = 0; bytes[i++] = version[0]; // no byteswapping bytes[i++] = version[1]; bytes[i++] = 0; bytes[i++] = 0; } else { for (int j = 0; j < bytes.Length - i; j++) { 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 void QueryCapabilities(LinuxJoystickDetails stick, byte* axisbit, int axisbytes, byte* keybit, int keybytes, out int axes, out int buttons, out int hats) { // Note: since we are trying to be compatible with the SDL2 gamepad database, // we have to match SDL2 behavior bug-for-bug. This means: // HAT0-HAT3 are all reported as hats (even if the docs say that HAT1 and HAT2 can be analogue triggers) // DPAD buttons are reported as buttons, not as hats (unlike Windows and Mac OS X) axes = buttons = hats = 0; for (EvdevAxis axis = 0; axis < EvdevAxis.CNT && (int)axis < axisbytes * 8; axis++) { InputAbsInfo info; bool is_valid = true; is_valid &= TestBit(axisbit, (int)axis); is_valid &= Evdev.GetAbs(stick.FileDescriptor, axis, out info) >= 0; if (is_valid) { if (axis >= EvdevAxis.HAT0X && axis <= EvdevAxis.HAT3Y) { // Analogue hat stick.AxisMap.Add(axis, new AxisInfo { Axis = (JoystickAxis)(JoystickHat)hats++, Info = info }); } else { // Regular axis stick.AxisMap.Add(axis, new AxisInfo { Axis = (JoystickAxis)axes++, Info = info }); } } } for (EvdevButton button = 0; button < EvdevButton.Last && (int)button < keybytes * 8; button++) { if (TestBit(keybit, (int)button)) { stick.ButtonMap.Add(button, (JoystickButton)buttons++); } } } LinuxJoystickDetails OpenJoystick(string path) { LinuxJoystickDetails stick = null; int number = GetJoystickNumber(Path.GetFileName(path)); if (number >= 0) { int fd = -1; try { fd = Libc.open(path, OpenFlags.NonBlock); if (fd == -1) return null; unsafe { 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) { stick = new LinuxJoystickDetails { FileDescriptor = fd, PathIndex = number, State = new JoystickState(), Name = name, Guid = CreateGuid(id, name), }; int axes, buttons, hats; QueryCapabilities(stick, axisbit, axissize, keybit, keysize, out axes, out buttons, out hats); stick.Caps = new JoystickCapabilities(axes, buttons, hats, false); // Poll the joystick once, to initialize its state PollJoystick(stick); } } 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(LinuxJoystickDetails js) { Sticks.Remove(js.FileDescriptor); Libc.close(js.FileDescriptor); js.FileDescriptor = -1; js.State = new JoystickState(); // clear joystick state js.Caps = new JoystickCapabilities(); } JoystickHatState TranslateHat(int x, int y) { return new JoystickHatState(HatPositions[x, y]); } void PollJoystick(LinuxJoystickDetails js) { unsafe { const int EventCount = 32; InputEvent* events = stackalloc InputEvent[EventCount]; long length = 0; while (true) { length = (long)Libc.read(js.FileDescriptor, (void*)events, (UIntPtr)(sizeof(InputEvent) * EventCount)); if (length <= 0) break; // Only mark the joystick as connected when we actually start receiving events. // Otherwise, the Xbox wireless receiver will register 4 joysticks even if no // actual joystick is connected to the receiver. js.Caps.SetIsConnected(true); js.State.SetIsConnected(true); length /= sizeof(InputEvent); for (int i = 0; i < length; i++) { InputEvent *e = events + i; switch (e->Type) { case EvdevType.ABS: { AxisInfo axis; if (js.AxisMap.TryGetValue((EvdevAxis)e->Code, out axis)) { if (axis.Info.Maximum > axis.Info.Minimum) { if (e->Code >= (int)EvdevAxis.HAT0X && e->Code <= (int)EvdevAxis.HAT3Y) { // We currently treat analogue hats as digital hats // to maintain compatibility with SDL2. We can do // better than this, however. JoystickHat hat = JoystickHat.Hat0 + (e->Code - (int)EvdevAxis.HAT0X) / 2; JoystickHatState pos = js.State.GetHat(hat); int xy_axis = (int)axis.Axis & 0x1; switch (xy_axis) { case 0: // X-axis pos = TranslateHat( e->Value.CompareTo(0) + 1, pos.IsUp ? 0 : pos.IsDown ? 2 : 1); break; case 1: // Y-axis pos = TranslateHat( pos.IsLeft ? 0 : pos.IsRight ? 2 : 1, e->Value.CompareTo(0) + 1); break; } js.State.SetHat(hat, pos); } else { // This axis represents a regular axis or trigger js.State.SetAxis( axis.Axis, (short)Common.HidHelper.ScaleValue(e->Value, axis.Info.Minimum, axis.Info.Maximum, short.MinValue, short.MaxValue)); } } } break; } case EvdevType.KEY: { JoystickButton button; if (js.ButtonMap.TryGetValue((EvdevButton)e->Code, out button)) { js.State.SetButton(button, e->Value != 0); } break; } } // Create a serial number (total seconds in 24.8 fixed point format) int sec = (int)((long)e->Time.Seconds & 0xffffffff); int msec = (int)e->Time.MicroSeconds / 1000; int packet = ((sec & 0x00ffffff) << 24) | Common.HidHelper.ScaleValue(msec, 0, 1000, 0, 255); js.State.SetPacketNumber(packet); } } } } static readonly string JoystickPath = "/dev/input"; static readonly string JoystickPathLegacy = "/dev"; #endregion #region IDisposable Members public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } void Dispose(bool manual) { if (!disposed) { if (manual) { } watcher.Dispose(); foreach (LinuxJoystickDetails js in Sticks) { CloseJoystick(js); } disposed = true; } } ~LinuxJoystick() { Dispose(false); } #endregion #region IJoystickDriver2 Members JoystickState IJoystickDriver2.GetState(int index) { LinuxJoystickDetails js = Sticks.FromIndex(index); if (js != null) { PollJoystick(js); return js.State; } return new JoystickState(); } JoystickCapabilities IJoystickDriver2.GetCapabilities(int index) { LinuxJoystickDetails js = Sticks.FromIndex(index); if (js != null) { return js.Caps; } return new JoystickCapabilities(); } Guid IJoystickDriver2.GetGuid(int index) { LinuxJoystickDetails js = Sticks.FromIndex(index); if (js != null) { return js.Guid; } return Guid.Empty; } #endregion } }