From 7dc2722d7c09eb12b9ca5e56f4f87340ce676170 Mon Sep 17 00:00:00 2001 From: danemadsen Date: Tue, 13 Jun 2017 22:36:51 +1000 Subject: [PATCH 1/2] Added Razer Raiju PID --- DS4Windows/DS4Library/DS4Devices.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DS4Windows/DS4Library/DS4Devices.cs b/DS4Windows/DS4Library/DS4Devices.cs index 5e7be1b77c..63d6e63792 100644 --- a/DS4Windows/DS4Library/DS4Devices.cs +++ b/DS4Windows/DS4Library/DS4Devices.cs @@ -31,7 +31,7 @@ public static void findControllers() { lock (Devices) { - int[] pid = { 0xBA0, 0x5C4, 0x09CC }; + int[] pid = { 0xBA0, 0x5C4, 0x09CC, 0x1000 }; IEnumerable hDevices = HidDevices.Enumerate(0x054C, pid); // Sort Bluetooth first in case USB is also connected on the same controller. hDevices = hDevices.OrderBy((HidDevice d) => { return DS4Device.HidConnectionType(d); }); From 927a932b28cd1db63a3c24b001e67c2296e22712 Mon Sep 17 00:00:00 2001 From: danemadsen Date: Wed, 14 Jun 2017 19:10:15 +1000 Subject: [PATCH 2/2] Add files via upload --- DS4Library/DS4Audio.cs | 251 +++++++ DS4Library/DS4Device.cs | 1316 +++++++++++++++++++++++++++++++++ DS4Library/DS4Devices.cs | 311 ++++++++ DS4Library/DS4Sixaxis.cs | 104 +++ DS4Library/DS4State.cs | 158 ++++ DS4Library/DS4StateExposed.cs | 95 +++ DS4Library/DS4Touchpad.cs | 215 ++++++ 7 files changed, 2450 insertions(+) create mode 100644 DS4Library/DS4Audio.cs create mode 100644 DS4Library/DS4Device.cs create mode 100644 DS4Library/DS4Devices.cs create mode 100644 DS4Library/DS4Sixaxis.cs create mode 100644 DS4Library/DS4State.cs create mode 100644 DS4Library/DS4StateExposed.cs create mode 100644 DS4Library/DS4Touchpad.cs diff --git a/DS4Library/DS4Audio.cs b/DS4Library/DS4Audio.cs new file mode 100644 index 0000000000..b256870005 --- /dev/null +++ b/DS4Library/DS4Audio.cs @@ -0,0 +1,251 @@ +using System; +using System.Runtime.InteropServices; +using DS4Windows.DS4Library.CoreAudio; + +namespace DS4Windows.DS4Library +{ + public class DS4Audio + { + private IAudioEndpointVolume endpointVolume; + + private static Guid IID_IAudioEndpointVolume = new Guid("5CDF2C82-841E-4546-9722-0CF74078229A"); + private static readonly PropertyKey PKEY_Device_FriendlyName = + new PropertyKey(new Guid(unchecked((int)0xa45c254e), unchecked((short)0xdf1c), 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0), 14); + + public uint Volume + { + get + { + float pfLevel = 0; + + if (endpointVolume != null) + endpointVolume.GetMasterVolumeLevelScalar(out pfLevel); + + return Convert.ToUInt32(pfLevel * 100); + } + } + + public uint getVolume() + { + float pfLevel = 0; + + if (endpointVolume != null) + endpointVolume.GetMasterVolumeLevelScalar(out pfLevel); + + return Convert.ToUInt32(pfLevel * 100); + } + + public DS4Audio(DataFlow audioFlags = DataFlow.Render) + { + var audioEnumerator = new MMDeviceEnumeratorComObject() as IMMDeviceEnumerator; + IMMDeviceCollection audioDevices; + audioEnumerator.EnumAudioEndpoints(audioFlags, DeviceState.Active, out audioDevices); + + int numAudioDevices; + Marshal.ThrowExceptionForHR(audioDevices.GetCount(out numAudioDevices)); + + for (int deviceNumber = 0; deviceNumber < numAudioDevices; ++deviceNumber) + { + IMMDevice audioDevice; + Marshal.ThrowExceptionForHR(audioDevices.Item(deviceNumber, out audioDevice)); + string deviceName = GetAudioDeviceName(ref audioDevice); + + if (deviceName.Contains("DUALSHOCK®4 USB Wireless Adaptor")) + { + object interfacePointer; + Marshal.ThrowExceptionForHR(audioDevice.Activate(ref IID_IAudioEndpointVolume, ClsCtx.ALL, IntPtr.Zero, out interfacePointer)); + endpointVolume = interfacePointer as IAudioEndpointVolume; + } + + Marshal.ReleaseComObject(audioDevice); + } + + Marshal.ReleaseComObject(audioDevices); + Marshal.ReleaseComObject(audioEnumerator); + } + + ~DS4Audio() + { + if (endpointVolume != null) + { + Marshal.ReleaseComObject(endpointVolume); + endpointVolume = null; + } + } + + private string GetAudioDeviceName(ref IMMDevice audioDevice) + { + IPropertyStore propertyStore; + Marshal.ThrowExceptionForHR(audioDevice.OpenPropertyStore(StorageAccessMode.Read, out propertyStore)); + + int numProperties; + Marshal.ThrowExceptionForHR(propertyStore.GetCount(out numProperties)); + + string deviceName = String.Empty; + + for (int propertyNum = 0; propertyNum < numProperties; ++propertyNum) + { + PropertyKey propertyKey; + Marshal.ThrowExceptionForHR(propertyStore.GetAt(propertyNum, out propertyKey)); + + if ((propertyKey.formatId == PKEY_Device_FriendlyName.formatId) && (propertyKey.propertyId == PKEY_Device_FriendlyName.propertyId)) + { + PropVariant propertyValue; + Marshal.ThrowExceptionForHR(propertyStore.GetValue(ref propertyKey, out propertyValue)); + deviceName = Marshal.PtrToStringUni(propertyValue.pointerValue); + break; + } + } + + Marshal.ReleaseComObject(propertyStore); + return deviceName; + } + } +} + +namespace DS4Windows.DS4Library.CoreAudio +{ + public enum DataFlow + { + Render, + Capture, + All + }; + + [Flags] + public enum DeviceState + { + Active = 0x00000001, + Disabled = 0x00000002, + NotPresent = 0x00000004, + Unplugged = 0x00000008, + All = 0x0000000F + } + + enum StorageAccessMode + { + Read, + Write, + ReadWrite + } + + [Flags] + public enum ClsCtx + { + INPROC_SERVER = 0x1, + INPROC_HANDLER = 0x2, + LOCAL_SERVER = 0x4, + INPROC_SERVER16 = 0x8, + REMOTE_SERVER = 0x10, + INPROC_HANDLER16 = 0x20, + NO_CODE_DOWNLOAD = 0x400, + NO_CUSTOM_MARSHAL = 0x1000, + ENABLE_CODE_DOWNLOAD = 0x2000, + NO_FAILURE_LOG = 0x4000, + DISABLE_AAA = 0x8000, + ENABLE_AAA = 0x10000, + FROM_DEFAULT_CONTEXT = 0x20000, + ACTIVATE_32_BIT_SERVER = 0x40000, + ACTIVATE_64_BIT_SERVER = 0x80000, + ENABLE_CLOAKING = 0x100000, + PS_DLL = unchecked((int)0x80000000), + INPROC = INPROC_SERVER | INPROC_HANDLER, + SERVER = INPROC_SERVER | LOCAL_SERVER | REMOTE_SERVER, + ALL = SERVER | INPROC_HANDLER + } + + public struct PropertyKey + { + public Guid formatId; + public int propertyId; + public PropertyKey(Guid formatId, int propertyId) + { + this.formatId = formatId; + this.propertyId = propertyId; + } + } + + [StructLayout(LayoutKind.Explicit)] + public struct PropVariant + { + [FieldOffset(0)] private short vt; + [FieldOffset(2)] private short wReserved1; + [FieldOffset(4)] private short wReserved2; + [FieldOffset(6)] private short wReserved3; + [FieldOffset(8)] public IntPtr pointerValue; + } + + [Guid("886d8eeb-8cf2-4446-8d02-cdba1dbdcf99"), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface IPropertyStore + { + int GetCount(out int propCount); + int GetAt(int property, out PropertyKey key); + int GetValue(ref PropertyKey key, out PropVariant value); + int SetValue(ref PropertyKey key, ref PropVariant value); + int Commit(); + } + + [Guid("D666063F-1587-4E43-81F1-B948E807363F"), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface IMMDevice + { + int Activate(ref Guid id, ClsCtx clsCtx, IntPtr activationParams, + [MarshalAs(UnmanagedType.IUnknown)] out object interfacePointer); + + int OpenPropertyStore(StorageAccessMode stgmAccess, out IPropertyStore properties); + + int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id); + } + + [Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E"), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface IMMDeviceCollection + { + int GetCount(out int numDevices); + int Item(int deviceNumber, out IMMDevice device); + } + + [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface IMMDeviceEnumerator + { + int EnumAudioEndpoints(DataFlow dataFlow, DeviceState stateMask, out IMMDeviceCollection devices); + } + + [ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] + class MMDeviceEnumeratorComObject + { + } + + [Guid("657804FA-D6AD-4496-8A60-352752AF4F89"), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IAudioEndpointVolumeCallback + { + void OnNotify(IntPtr notifyData); + }; + + [Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IAudioEndpointVolume + { + int RegisterControlChangeNotify(IAudioEndpointVolumeCallback pNotify); + int UnregisterControlChangeNotify(IAudioEndpointVolumeCallback pNotify); + int GetChannelCount(out int pnChannelCount); + int SetMasterVolumeLevel(float fLevelDB, ref Guid pguidEventContext); + int SetMasterVolumeLevelScalar(float fLevel, ref Guid pguidEventContext); + int GetMasterVolumeLevel(out float pfLevelDB); + int GetMasterVolumeLevelScalar(out float pfLevel); + int SetChannelVolumeLevel(uint nChannel, float fLevelDB, ref Guid pguidEventContext); + int SetChannelVolumeLevelScalar(uint nChannel, float fLevel, ref Guid pguidEventContext); + int GetChannelVolumeLevel(uint nChannel, out float pfLevelDB); + int GetChannelVolumeLevelScalar(uint nChannel, out float pfLevel); + int SetMute([MarshalAs(UnmanagedType.Bool)] Boolean bMute, ref Guid pguidEventContext); + int GetMute(out bool pbMute); + int GetVolumeStepInfo(out uint pnStep, out uint pnStepCount); + int VolumeStepUp(ref Guid pguidEventContext); + int VolumeStepDown(ref Guid pguidEventContext); + int QueryHardwareSupport(out uint pdwHardwareSupportMask); + int GetVolumeRange(out float pflVolumeMindB, out float pflVolumeMaxdB, out float pflVolumeIncrementdB); + } +} \ No newline at end of file diff --git a/DS4Library/DS4Device.cs b/DS4Library/DS4Device.cs new file mode 100644 index 0000000000..e567a183e4 --- /dev/null +++ b/DS4Library/DS4Device.cs @@ -0,0 +1,1316 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; + +using System.Runtime.InteropServices; +using System.Diagnostics; + +using System.Linq; +using System.Drawing; +using DS4Windows.DS4Library; + +namespace DS4Windows +{ + public struct DS4Color + { + public byte red; + public byte green; + public byte blue; + public DS4Color(Color c) + { + red = c.R; + green = c.G; + blue = c.B; + } + + public DS4Color(byte r, byte g, byte b) + { + red = r; + green = g; + blue = b; + } + + public override bool Equals(object obj) + { + if (obj is DS4Color) + { + DS4Color dsc = ((DS4Color)obj); + return (this.red == dsc.red && this.green == dsc.green && this.blue == dsc.blue); + } + else + return false; + } + + public Color ToColor => Color.FromArgb(red, green, blue); + public Color ToColorA + { + get + { + byte alphacolor = Math.Max(red, Math.Max(green, blue)); + Color reg = Color.FromArgb(red, green, blue); + Color full = HuetoRGB(reg.GetHue(), reg.GetBrightness(), reg); + return Color.FromArgb((alphacolor > 205 ? 255 : (alphacolor + 50)), full); + } + } + + private Color HuetoRGB(float hue, float light, Color rgb) + { + float L = (float)Math.Max(.5, light); + float C = (1 - Math.Abs(2 * L - 1)); + float X = (C * (1 - Math.Abs((hue / 60) % 2 - 1))); + float m = L - C / 2; + float R = 0, G = 0, B = 0; + if (light == 1) return Color.White; + else if (rgb.R == rgb.G && rgb.G == rgb.B) return Color.White; + else if (0 <= hue && hue < 60) { R = C; G = X; } + else if (60 <= hue && hue < 120) { R = X; G = C; } + else if (120 <= hue && hue < 180) { G = C; B = X; } + else if (180 <= hue && hue < 240) { G = X; B = C; } + else if (240 <= hue && hue < 300) { R = X; B = C; } + else if (300 <= hue && hue < 360) { R = C; B = X; } + return Color.FromArgb((int)((R + m) * 255), (int)((G + m) * 255), (int)((B + m) * 255)); + } + + public static bool TryParse(string value, ref DS4Color ds4color) + { + try + { + string[] ss = value.Split(','); + return byte.TryParse(ss[0], out ds4color.red) &&byte.TryParse(ss[1], out ds4color.green) && byte.TryParse(ss[2], out ds4color.blue); + } + catch { return false; } + } + + public override string ToString() => $"Red: {red} Green: {green} Blue: {blue}"; + } + + public enum ConnectionType : byte { BT, SONYWA, USB }; // Prioritize Bluetooth when both BT and USB are connected. + + /** + * The haptics engine uses a stack of these states representing the light bar and rumble motor settings. + * It (will) handle composing them and the details of output report management. + */ + public struct DS4HapticState + { + public DS4Color LightBarColor; + public bool LightBarExplicitlyOff; + public byte LightBarFlashDurationOn, LightBarFlashDurationOff; + public byte RumbleMotorStrengthLeftHeavySlow, RumbleMotorStrengthRightLightFast; + public bool RumbleMotorsExplicitlyOff; + + public bool IsLightBarSet() + { + return LightBarExplicitlyOff || LightBarColor.red != 0 || LightBarColor.green != 0 || LightBarColor.blue != 0; + } + + public bool IsRumbleSet() + { + return RumbleMotorsExplicitlyOff || RumbleMotorStrengthLeftHeavySlow != 0 || RumbleMotorStrengthRightLightFast != 0; + } + } + + public class DS4Device + { + private const int BT_OUTPUT_REPORT_LENGTH = 78; + private const int BT_INPUT_REPORT_LENGTH = 547; + // Use large value for worst case scenario + private const int READ_STREAM_TIMEOUT = 1000; + // Isolated BT report can have latency as high as 15 ms + // due to hardware. + private const int WARN_INTERVAL_BT = 20; + private const int WARN_INTERVAL_USB = 10; + // Maximum values for battery level when no USB cable is connected + // and when a USB cable is connected + private const int BATTERY_MAX = 8; + private const int BATTERY_MAX_USB = 11; + public const string blankSerial = "00:00:00:00:00:00"; + private HidDevice hDevice; + private string Mac; + private DS4State cState = new DS4State(); + private DS4State pState = new DS4State(); + private ConnectionType conType; + private byte[] accel = new byte[6]; + private byte[] gyro = new byte[6]; + private byte[] inputReport; + //private byte[] inputReport2; + private byte[] btInputReport = null; + private byte[] outputReportBuffer, outputReport; + private readonly DS4Touchpad touchpad = null; + private readonly DS4SixAxis sixAxis = null; + private byte rightLightFastRumble; + private byte leftHeavySlowRumble; + private DS4Color ligtBarColor; + private byte ledFlashOn, ledFlashOff; + private Thread ds4Input, ds4Output; + private int battery; + private DS4Audio audio = null; + private DS4Audio micAudio = null; + public DateTime lastActive = DateTime.UtcNow; + public DateTime firstActive = DateTime.UtcNow; + private bool charging; + private bool outputRumble = false; + private int warnInterval = WARN_INTERVAL_USB; + public int getWarnInterval() + { + return warnInterval; + } + + private bool exitOutputThread = false; + private bool exitInputThread = false; + private object exitLocker = new object(); + + public event EventHandler Report = null; + public event EventHandler Removal = null; + public event EventHandler SyncChange = null; + public event EventHandler SerialChange = null; + public event EventHandler PublishRemoval = null; + + public HidDevice HidDevice => hDevice; + public bool IsExclusive => HidDevice.IsExclusive; + public bool isExclusive() + { + return HidDevice.IsExclusive; + } + + private bool isDisconnecting = false; + public bool IsDisconnecting + { + get { return isDisconnecting; } + private set + { + this.isDisconnecting = value; + } + } + + public bool isDisconnectingStatus() + { + return this.isDisconnecting; + } + + private bool isRemoving = false; + public bool IsRemoving + { + get { return isRemoving; } + set + { + this.isRemoving = value; + } + } + + private bool isRemoved = false; + public bool IsRemoved + { + get { return isRemoved; } + set + { + this.isRemoved = value; + } + } + + public object removeLocker = new object(); + + public string MacAddress => Mac; + public string getMacAddress() + { + return this.Mac; + } + + public ConnectionType ConnectionType => conType; + public ConnectionType getConnectionType() + { + return this.conType; + } + + // behavior only active when > 0 + private int idleTimeout = 0; + public int IdleTimeout + { + get { return idleTimeout; } + set + { + idleTimeout = value; + } + } + + public int getIdleTimeout() + { + return idleTimeout; + } + + public void setIdleTimeout(int value) + { + if (idleTimeout != value) + { + idleTimeout = value; + } + } + + public int Battery => battery; + public int getBattery() + { + return battery; + } + + public bool Charging => charging; + public bool isCharging() + { + return charging; + } + + private long lastTimeElapsed = 0; + public long getLastTimeElapsed() + { + return lastTimeElapsed; + } + + public byte RightLightFastRumble + { + get { return rightLightFastRumble; } + set + { + if (rightLightFastRumble != value) + rightLightFastRumble = value; + } + } + + public byte LeftHeavySlowRumble + { + get { return leftHeavySlowRumble; } + set + { + if (leftHeavySlowRumble != value) + leftHeavySlowRumble = value; + } + } + + public byte getLeftHeavySlowRumble() + { + return leftHeavySlowRumble; + } + + public DS4Color LightBarColor + { + get { return ligtBarColor; } + set + { + if (ligtBarColor.red != value.red || ligtBarColor.green != value.green || ligtBarColor.blue != value.blue) + { + ligtBarColor = value; + } + } + } + + public byte LightBarOnDuration + { + get { return ledFlashOn; } + set + { + if (ledFlashOn != value) + { + ledFlashOn = value; + } + } + } + + public byte getLightBarOnDuration() + { + return ledFlashOn; + } + + public byte LightBarOffDuration + { + get { return ledFlashOff; } + set + { + if (ledFlashOff != value) + { + ledFlashOff = value; + } + } + } + + public byte getLightBarOffDuration() + { + return ledFlashOff; + } + + // Specify the poll rate interval used for the DS4 hardware when + // connected via Bluetooth + private int btPollRate = 0; + public int BTPollRate + { + get { return btPollRate; } + set + { + if (btPollRate != value && value >= 0 && value <= 16) + { + btPollRate = value; + } + } + } + + public int getBTPollRate() + { + return btPollRate; + } + + public void setBTPollRate(int value) + { + if (btPollRate != value && value >= 0 && value <= 16) + { + btPollRate = value; + } + } + + public DS4Touchpad Touchpad { get { return touchpad; } } + public DS4SixAxis SixAxis { get { return sixAxis; } } + + public static ConnectionType HidConnectionType(HidDevice hidDevice) + { + ConnectionType result = ConnectionType.USB; + if (hidDevice.Capabilities.InputReportByteLength == 64) + { + if (hidDevice.Capabilities.NumberFeatureDataIndices == 22) + { + result = ConnectionType.SONYWA; + } + } + else + { + result = ConnectionType.BT; + } + + return result; + } + + private SynchronizationContext uiContext = null; + public SynchronizationContext getUiContext() + { + return uiContext; + } + public void setUiContext(SynchronizationContext uiContext) + { + this.uiContext = uiContext; + } + + private Queue eventQueue = new Queue(); + private object eventQueueLock = new object(); + + private Thread timeoutCheckThread = null; + private bool timeoutExecuted = false; + private bool timeoutEvent = false; + + public DS4Device(HidDevice hidDevice) + { + hDevice = hidDevice; + conType = HidConnectionType(hDevice); + Mac = hDevice.readSerial(); + if (conType == ConnectionType.USB || conType == ConnectionType.SONYWA) + { + inputReport = new byte[64]; + //inputReport2 = new byte[64]; + outputReport = new byte[hDevice.Capabilities.OutputReportByteLength]; + outputReportBuffer = new byte[hDevice.Capabilities.OutputReportByteLength]; + if (conType == ConnectionType.USB) + { + warnInterval = WARN_INTERVAL_USB; + synced = true; + } + else + { + warnInterval = WARN_INTERVAL_BT; + audio = new DS4Audio(); + micAudio = new DS4Audio(DS4Library.CoreAudio.DataFlow.Render); + synced = isValidSerial(); + } + } + else + { + btInputReport = new byte[BT_INPUT_REPORT_LENGTH]; + inputReport = new byte[btInputReport.Length - 2]; + outputReport = new byte[BT_OUTPUT_REPORT_LENGTH]; + outputReportBuffer = new byte[BT_OUTPUT_REPORT_LENGTH]; + warnInterval = WARN_INTERVAL_BT; + synced = isValidSerial(); + } + + touchpad = new DS4Touchpad(); + sixAxis = new DS4SixAxis(); + } + + private void timeoutTestThread() + { + while (!timeoutExecuted) + { + if (timeoutEvent) + { + timeoutExecuted = true; + this.sendOutputReport(true); // Kick Windows into noticing the disconnection. + } + else + { + timeoutEvent = true; + Thread.Sleep(READ_STREAM_TIMEOUT); + } + } + } + + public void StartUpdate() + { + if (ds4Input == null) + { + if (!hDevice.IsFileStreamOpen()) + { + hDevice.OpenFileStream(inputReport.Length); + } + + //Console.WriteLine(MacAddress.ToString() + " " + System.DateTime.UtcNow.ToString("o") + "> start"); + sendOutputReport(true); // initialize the output report + + if (conType == ConnectionType.BT) + { + // Only use the output thread for Bluetooth connections. + // USB will utilize overlapped IO instead. + ds4Output = new Thread(performDs4Output); + ds4Output.Priority = ThreadPriority.AboveNormal; + ds4Output.Name = "DS4 Output thread: " + Mac; + ds4Output.IsBackground = true; + ds4Output.Start(); + + timeoutCheckThread = new Thread(timeoutTestThread); + timeoutCheckThread.IsBackground = true; + timeoutCheckThread.Start(); + } + + ds4Input = new Thread(performDs4Input); + ds4Input.Priority = ThreadPriority.AboveNormal; + ds4Input.Name = "DS4 Input thread: " + Mac; + ds4Input.IsBackground = true; + ds4Input.Start(); + } + else + Console.WriteLine("Thread already running for DS4: " + Mac); + } + + public void StopUpdate() + { + if (ds4Input != null && + ds4Input.IsAlive && !ds4Input.ThreadState.HasFlag(System.Threading.ThreadState.Stopped) && + !ds4Input.ThreadState.HasFlag(System.Threading.ThreadState.AbortRequested)) + { + try + { + exitInputThread = true; + //ds4Input.Abort(); + ds4Input.Join(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + } + + StopOutputUpdate(); + } + + private void StopOutputUpdate() + { + lock (exitLocker) + { + if (ds4Output != null && + ds4Output.IsAlive && !ds4Output.ThreadState.HasFlag(System.Threading.ThreadState.Stopped) && + !ds4Output.ThreadState.HasFlag(System.Threading.ThreadState.AbortRequested)) + { + try + { + exitOutputThread = true; + /*lock (outputReport) + { + Monitor.PulseAll(outputReport); + } + */ + + ds4Output.Interrupt(); + ds4Output.Join(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + } + } + } + + private bool writeOutput() + { + if (conType == ConnectionType.BT) + { + return hDevice.WriteOutputReportViaControl(outputReport); + } + else + { + return hDevice.WriteOutputReportViaInterrupt(outputReport, READ_STREAM_TIMEOUT); + //return hDevice.WriteAsyncOutputReportViaInterrupt(outputReport); + } + } + + private void performDs4Output() + { + lock (outputReport) + { + try + { + int lastError = 0; + while (!exitOutputThread) + { + bool result = false; + if (outputRumble) + { + result = writeOutput(); + + if (!result) + { + int thisError = Marshal.GetLastWin32Error(); + if (lastError != thisError) + { + Console.WriteLine(Mac.ToString() + " " + System.DateTime.UtcNow.ToString("o") + "> encountered write failure: " + thisError); + //Log.LogToGui(Mac.ToString() + " encountered write failure: " + thisError, true); + lastError = thisError; + } + } + else + { + outputRumble = false; + } + } + + if (!outputRumble) + { + lastError = 0; + Monitor.Wait(outputReport); + /*if (testRumble.IsRumbleSet()) // repeat test rumbles periodically; rumble has auto-shut-off in the DS4 firmware + Monitor.Wait(outputReport, 10000); // DS4 firmware stops it after 5 seconds, so let the motors rest for that long, too. + else + Monitor.Wait(outputReport); + */ + } + } + } + catch (ThreadInterruptedException) + { + + } + } + } + + /** Is the device alive and receiving valid sensor input reports? */ + public bool IsAlive() + { + return priorInputReport30 != 0xff; + } + + private byte priorInputReport30 = 0xff; + + private bool synced = false; + public bool Synced + { + get { return synced; } + set + { + if (synced != value) + { + synced = value; + } + } + } + + public bool isSynced() + { + return synced; + } + + public double Latency = 0; + public string error; + public bool firstReport = false; + public bool oldCharging = false; + + private void performDs4Input() + { + firstActive = DateTime.UtcNow; + NativeMethods.HidD_SetNumInputBuffers(hDevice.safeReadHandle.DangerousGetHandle(), 2); + //List latencyList = new List(51); // Set capacity at max + 1 to avoid any list resizing + Queue latencyQueue = new Queue(51); // Set capacity at max + 1 to avoid any resizing + int tempLatencyCount = 0; + long oldtime = 0; + string currerror = string.Empty; + long curtime = 0; + Stopwatch sw = new Stopwatch(); + sw.Start(); + timeoutEvent = false; + + int maxBatteryValue = 0; + int tempBattery = 0; + + while (!exitInputThread) + { + oldCharging = charging; + currerror = string.Empty; + curtime = sw.ElapsedMilliseconds; + this.lastTimeElapsed = curtime - oldtime; + //latencyList.Add(this.lastTimeElapsed); + latencyQueue.Enqueue(this.lastTimeElapsed); + tempLatencyCount++; + oldtime = curtime; + + if (tempLatencyCount > 50) + { + //latencyList.RemoveAt(0); + latencyQueue.Dequeue(); + tempLatencyCount--; + } + + //Latency = latencyList.Average(); + //latencyList.Average(); + Latency = latencyQueue.Average(); + + if (conType == ConnectionType.BT) + { + //HidDevice.ReadStatus res = hDevice.ReadFile(btInputReport); + //HidDevice.ReadStatus res = hDevice.ReadAsyncWithFileStream(btInputReport, READ_STREAM_TIMEOUT); + HidDevice.ReadStatus res = hDevice.ReadWithFileStream(btInputReport); + timeoutEvent = false; + //HidDevice.ReadStatus res = hDevice.ReadFileOverlapped(btInputReport, READ_STREAM_TIMEOUT); + if (res == HidDevice.ReadStatus.Success) + { + Array.Copy(btInputReport, 2, inputReport, 0, inputReport.Length); + } + else + { + if (res == HidDevice.ReadStatus.WaitTimedOut) + { + Log.LogToGui(Mac.ToString() + " disconnected due to timeout", true); + } + else + { + int winError = Marshal.GetLastWin32Error(); + Console.WriteLine(Mac.ToString() + " " + System.DateTime.UtcNow.ToString("o") + "> disconnect due to read failure: " + winError); + //Log.LogToGui(Mac.ToString() + " disconnected due to read failure: " + winError, true); + } + + sendOutputReport(true); // Kick Windows into noticing the disconnection. + StopOutputUpdate(); + isDisconnecting = true; + uiContext.Send(new SendOrPostCallback(delegate (object state4) + { + Removal?.Invoke(this, EventArgs.Empty); + }), null); + + //System.Threading.Tasks.Task.Factory.StartNew(() => { Removal?.Invoke(this, EventArgs.Empty); }); + //Removal?.Invoke(this, EventArgs.Empty); + + timeoutExecuted = true; + return; + } + } + else + { + //HidDevice.ReadStatus res = hDevice.ReadFile(inputReport); + //Array.Clear(inputReport, 0, inputReport.Length); + //HidDevice.ReadStatus res = hDevice.ReadAsyncWithFileStream(inputReport, READ_STREAM_TIMEOUT); + HidDevice.ReadStatus res = hDevice.ReadWithFileStream(inputReport); + //HidDevice.ReadStatus res = hDevice.ReadFileOverlapped(inputReport, READ_STREAM_TIMEOUT); + if (res != HidDevice.ReadStatus.Success) + { + if (res == HidDevice.ReadStatus.WaitTimedOut) + { + Log.LogToGui(Mac.ToString() + " disconnected due to timeout", true); + } + else + { + int winError = Marshal.GetLastWin32Error(); + Console.WriteLine(Mac.ToString() + " " + System.DateTime.UtcNow.ToString("o") + "> disconnect due to read failure: " + winError); + //Log.LogToGui(Mac.ToString() + " disconnected due to read failure: " + winError, true); + } + + StopOutputUpdate(); + isDisconnecting = true; + uiContext.Send(new SendOrPostCallback(delegate (object state4) + { + Removal?.Invoke(this, EventArgs.Empty); + }), null); + + //System.Threading.Tasks.Task.Factory.StartNew(() => { Removal?.Invoke(this, EventArgs.Empty); }); + //Removal?.Invoke(this, EventArgs.Empty); + + timeoutExecuted = true; + return; + } + else + { + //Array.Copy(inputReport2, 0, inputReport, 0, inputReport.Length); + } + } + + if (conType == ConnectionType.BT && btInputReport[0] != 0x11) + { + //Received incorrect report, skip it + continue; + } + + DateTime utcNow = DateTime.UtcNow; // timestamp with UTC in case system time zone changes + resetHapticState(); + cState.ReportTimeStamp = utcNow; + cState.LX = inputReport[1]; + cState.LY = inputReport[2]; + cState.RX = inputReport[3]; + cState.RY = inputReport[4]; + cState.L2 = inputReport[8]; + cState.R2 = inputReport[9]; + + cState.Triangle = (inputReport[5] & (1 << 7)) != 0; + cState.Circle = (inputReport[5] & (1 << 6)) != 0; + cState.Cross = (inputReport[5] & (1 << 5)) != 0; + cState.Square = (inputReport[5] & (1 << 4)) != 0; + + // First 4 bits denote dpad state. Clock representation + // with 8 meaning centered and 0 meaning DpadUp. + byte dpad_state = (byte)(inputReport[5] & 0x0F); + + switch (dpad_state) + { + case 0: cState.DpadUp = true; cState.DpadDown = false; cState.DpadLeft = false; cState.DpadRight = false; break; + case 1: cState.DpadUp = true; cState.DpadDown = false; cState.DpadLeft = false; cState.DpadRight = true; break; + case 2: cState.DpadUp = false; cState.DpadDown = false; cState.DpadLeft = false; cState.DpadRight = true; break; + case 3: cState.DpadUp = false; cState.DpadDown = true; cState.DpadLeft = false; cState.DpadRight = true; break; + case 4: cState.DpadUp = false; cState.DpadDown = true; cState.DpadLeft = false; cState.DpadRight = false; break; + case 5: cState.DpadUp = false; cState.DpadDown = true; cState.DpadLeft = true; cState.DpadRight = false; break; + case 6: cState.DpadUp = false; cState.DpadDown = false; cState.DpadLeft = true; cState.DpadRight = false; break; + case 7: cState.DpadUp = true; cState.DpadDown = false; cState.DpadLeft = true; cState.DpadRight = false; break; + case 8: + default: cState.DpadUp = false; cState.DpadDown = false; cState.DpadLeft = false; cState.DpadRight = false; break; + } + + cState.R3 = (inputReport[6] & (1 << 7)) != 0; + cState.L3 = (inputReport[6] & (1 << 6)) != 0; + cState.Options = (inputReport[6] & (1 << 5)) != 0; + cState.Share = (inputReport[6] & (1 << 4)) != 0; + cState.R1 = (inputReport[6] & (1 << 1)) != 0; + cState.L1 = (inputReport[6] & (1 << 0)) != 0; + + cState.PS = (inputReport[7] & (1 << 0)) != 0; + cState.TouchButton = (inputReport[7] & (1 << 2 - 1)) != 0; + cState.FrameCounter = (byte)(inputReport[7] >> 2); + + // Store Gyro and Accel values + Array.Copy(inputReport, 14, accel, 0, 6); + Array.Copy(inputReport, 20, gyro, 0, 6); + sixAxis.handleSixaxis(gyro, accel, cState); + + try + { + charging = (inputReport[30] & 0x10) != 0; + maxBatteryValue = charging ? BATTERY_MAX_USB : BATTERY_MAX; + tempBattery = (inputReport[30] & 0x0f) * 100 / maxBatteryValue; + battery = Math.Min((byte)tempBattery, (byte)100); + cState.Battery = (byte)battery; + //System.Diagnostics.Debug.WriteLine("CURRENT BATTERY: " + (inputReport[30] & 0x0f) + " | " + tempBattery + " | " + battery); + if (inputReport[30] != priorInputReport30) + { + priorInputReport30 = inputReport[30]; + //Console.WriteLine(MacAddress.ToString() + " " + System.DateTime.UtcNow.ToString("o") + "> power subsystem octet: 0x" + inputReport[30].ToString("x02")); + } + } + catch { currerror = "Index out of bounds: battery"; } + + // XXX DS4State mapping needs fixup, turn touches into an array[4] of structs. And include the touchpad details there instead. + try + { + // Only care if one touch packet is detected. Other touch packets + // don't seem to contain relevant data. ds4drv does not use them either. + for (int touches = Math.Max((int)(inputReport[-1 + DS4Touchpad.TOUCHPAD_DATA_OFFSET - 1]), 1), touchOffset = 0; touches > 0; touches--, touchOffset += 9) + //for (int touches = inputReport[-1 + DS4Touchpad.TOUCHPAD_DATA_OFFSET - 1], touchOffset = 0; touches > 0; touches--, touchOffset += 9) + { + cState.TouchPacketCounter = inputReport[-1 + DS4Touchpad.TOUCHPAD_DATA_OFFSET + touchOffset]; + cState.Touch1 = (inputReport[0 + DS4Touchpad.TOUCHPAD_DATA_OFFSET + touchOffset] >> 7) != 0 ? false : true; // >= 1 touch detected + cState.Touch1Identifier = (byte)(inputReport[0 + DS4Touchpad.TOUCHPAD_DATA_OFFSET + touchOffset] & 0x7f); + cState.Touch2 = (inputReport[4 + DS4Touchpad.TOUCHPAD_DATA_OFFSET + touchOffset] >> 7) != 0 ? false : true; // 2 touches detected + cState.Touch2Identifier = (byte)(inputReport[4 + DS4Touchpad.TOUCHPAD_DATA_OFFSET + touchOffset] & 0x7f); + cState.TouchLeft = (inputReport[1 + DS4Touchpad.TOUCHPAD_DATA_OFFSET + touchOffset] + ((inputReport[2 + DS4Touchpad.TOUCHPAD_DATA_OFFSET + touchOffset] & 0xF) * 255) >= 1920 * 2 / 5) ? false : true; + cState.TouchRight = (inputReport[1 + DS4Touchpad.TOUCHPAD_DATA_OFFSET + touchOffset] + ((inputReport[2 + DS4Touchpad.TOUCHPAD_DATA_OFFSET + touchOffset] & 0xF) * 255) < 1920 * 2 / 5) ? false : true; + // Even when idling there is still a touch packet indicating no touch 1 or 2 + touchpad.handleTouchpad(inputReport, cState, touchOffset); + } + } + catch { currerror = "Index out of bounds: touchpad"; } + + /* Debug output of incoming HID data: + if (cState.L2 == 0xff && cState.R2 == 0xff) + { + Console.Write(MacAddress.ToString() + " " + System.DateTime.UtcNow.ToString("o") + ">"); + for (int i = 0; i < inputReport.Length; i++) + Console.Write(" " + inputReport[i].ToString("x2")); + Console.WriteLine(); + } */ + + if (conType == ConnectionType.SONYWA) + { + bool controllerSynced = inputReport[31] == 0; + if (controllerSynced != synced) + { + synced = controllerSynced; + SyncChange?.Invoke(this, EventArgs.Empty); + } + } + + bool ds4Idle = cState.FrameCounter == pState.FrameCounter; + if (!ds4Idle) + { + isRemoved = false; + } + + if (conType == ConnectionType.USB) + { + lastActive = utcNow; + } + else + { + bool shouldDisconnect = false; + int idleTime = idleTimeout; + if (!isRemoved && idleTime > 0) + { + bool idleInput = isDS4Idle(); + if (idleInput) + { + DateTime timeout = lastActive + TimeSpan.FromSeconds(idleTime); + if (!charging) + shouldDisconnect = utcNow >= timeout; + } + else + { + lastActive = utcNow; + } + } + else + { + lastActive = utcNow; + } + + if (shouldDisconnect) + { + Log.LogToGui(Mac.ToString() + " disconnecting due to idle disconnect", false); + + if (conType == ConnectionType.BT) + { + if (DisconnectBT(true)) + { + timeoutExecuted = true; + return; // all done + } + } + else if (conType == ConnectionType.SONYWA) + { + DisconnectDongle(); + } + } + } + + if (oldCharging != charging && conType == ConnectionType.BT) + { + if (Global.getQuickCharge() && charging) + { + DisconnectBT(true); + timeoutExecuted = true; + return; + } + } + + if (Report != null) + Report(this, EventArgs.Empty); + + bool syncWriteReport = true; + if (conType == ConnectionType.BT) + { + syncWriteReport = false; + } + sendOutputReport(syncWriteReport); + + if (!string.IsNullOrEmpty(currerror)) + error = currerror; + else if (!string.IsNullOrEmpty(error)) + error = string.Empty; + + cState.CopyTo(pState); + + lock (eventQueueLock) + { + Action tempAct = null; + for (int actInd = 0, actLen = eventQueue.Count; actInd < actLen; actInd++) + //foreach (Action tempAct in eventQueue) + { + tempAct = eventQueue.Dequeue(); + tempAct.Invoke(); + } + + //eventQueue.Clear(); + } + } + + timeoutExecuted = true; + } + + public void FlushHID() + { + hDevice.flush_Queue(); + } + + private void sendOutputReport(bool synchronous) + { + setTestRumble(); + setHapticState(); + + if (conType == ConnectionType.BT) + { + outputReportBuffer[0] = 0x11; + //outputReportBuffer[1] = 0x80; + //outputReportBuffer[1] = 0x84; + outputReportBuffer[1] = (byte)(0x80 | btPollRate); // input report rate + outputReportBuffer[3] = 0xff; + outputReportBuffer[6] = rightLightFastRumble; // fast motor + outputReportBuffer[7] = leftHeavySlowRumble; // slow motor + outputReportBuffer[8] = LightBarColor.red; // red + outputReportBuffer[9] = LightBarColor.green; // green + outputReportBuffer[10] = LightBarColor.blue; // blue + outputReportBuffer[11] = ledFlashOn; // flash on duration + outputReportBuffer[12] = ledFlashOff; // flash off duration + } + else + { + outputReportBuffer[0] = 0x05; + outputReportBuffer[1] = 0xff; + outputReportBuffer[4] = rightLightFastRumble; // fast motor + outputReportBuffer[5] = leftHeavySlowRumble; // slow motor + outputReportBuffer[6] = LightBarColor.red; // red + outputReportBuffer[7] = LightBarColor.green; // green + outputReportBuffer[8] = LightBarColor.blue; // blue + outputReportBuffer[9] = ledFlashOn; // flash on duration + outputReportBuffer[10] = ledFlashOff; // flash off duration + if (conType == ConnectionType.SONYWA) + { + // Headphone volume levels + outputReportBuffer[19] = outputReportBuffer[20] = Convert.ToByte(audio.getVolume()); + // Microphone volume level + outputReportBuffer[21] = Convert.ToByte(micAudio.getVolume()); + } + } + + bool quitOutputThread = false; + + lock (outputReport) + { + if (synchronous) + { + outputRumble = false; + outputReportBuffer.CopyTo(outputReport, 0); + try + { + if (!writeOutput()) + { + int winError = Marshal.GetLastWin32Error(); + Console.WriteLine(Mac.ToString() + " " + System.DateTime.UtcNow.ToString("o") + "> encountered synchronous write failure: " + winError); + //Log.LogToGui(Mac.ToString() + " encountered synchronous write failure: " + winError, true); + quitOutputThread = true; + } + } + catch + { + // If it's dead already, don't worry about it. + } + } + else + { + bool output = false; + for (int i = 0, arlen = outputReport.Length; !output && i < arlen; i++) + output = outputReport[i] != outputReportBuffer[i]; + + if (output) + { + outputRumble = true; + outputReportBuffer.CopyTo(outputReport, 0); + Monitor.Pulse(outputReport); + } + } + } + + if (quitOutputThread) + { + StopOutputUpdate(); + } + } + + public bool DisconnectBT(bool callRemoval = false) + { + if (Mac != null) + { + Console.WriteLine("Trying to disconnect BT device " + Mac); + IntPtr btHandle = IntPtr.Zero; + int IOCTL_BTH_DISCONNECT_DEVICE = 0x41000c; + + byte[] btAddr = new byte[8]; + string[] sbytes = Mac.Split(':'); + for (int i = 0; i < 6; i++) + { + // parse hex byte in reverse order + btAddr[5 - i] = Convert.ToByte(sbytes[i], 16); + } + + long lbtAddr = BitConverter.ToInt64(btAddr, 0); + + bool success = false; + // Wait for output report to be written + lock (outputReport) + { + NativeMethods.BLUETOOTH_FIND_RADIO_PARAMS p = new NativeMethods.BLUETOOTH_FIND_RADIO_PARAMS(); + p.dwSize = Marshal.SizeOf(typeof(NativeMethods.BLUETOOTH_FIND_RADIO_PARAMS)); + IntPtr searchHandle = NativeMethods.BluetoothFindFirstRadio(ref p, ref btHandle); + int bytesReturned = 0; + + while (!success && btHandle != IntPtr.Zero) + { + success = NativeMethods.DeviceIoControl(btHandle, IOCTL_BTH_DISCONNECT_DEVICE, ref lbtAddr, 8, IntPtr.Zero, 0, ref bytesReturned, IntPtr.Zero); + NativeMethods.CloseHandle(btHandle); + if (!success) + { + if (!NativeMethods.BluetoothFindNextRadio(searchHandle, ref btHandle)) + btHandle = IntPtr.Zero; + } + } + + NativeMethods.BluetoothFindRadioClose(searchHandle); + Console.WriteLine("Disconnect successful: " + success); + } + + success = true; // XXX return value indicates failure, but it still works? + if (success) + { + IsDisconnecting = true; + StopOutputUpdate(); + + if (callRemoval) + { + uiContext.Send(new SendOrPostCallback(delegate (object state) + { + Removal?.Invoke(this, EventArgs.Empty); + }), null); + + //System.Threading.Tasks.Task.Factory.StartNew(() => { Removal?.Invoke(this, EventArgs.Empty); }); + } + } + + return success; + } + + return false; + } + + public bool DisconnectDongle(bool remove = false) + { + bool result = false; + byte[] disconnectReport = new byte[65]; + disconnectReport[0] = 0xe2; + disconnectReport[1] = 0x02; + Array.Clear(disconnectReport, 2, 63); + + lock (outputReport) + { + result = hDevice.WriteFeatureReport(disconnectReport); + } + + if (result && remove) + { + isDisconnecting = true; + StopOutputUpdate(); + + uiContext.Send(new SendOrPostCallback(delegate (object state4) + { + Removal?.Invoke(this, EventArgs.Empty); + }), null); + + //System.Threading.Tasks.Task.Factory.StartNew(() => { Removal?.Invoke(this, EventArgs.Empty); }); + //Removal?.Invoke(this, EventArgs.Empty); + } + else if (result && !remove) + { + isRemoved = true; + } + + return result; + } + + private DS4HapticState testRumble = new DS4HapticState(); + + public void setRumble(byte rightLightFastMotor, byte leftHeavySlowMotor) + { + testRumble.RumbleMotorStrengthRightLightFast = rightLightFastMotor; + testRumble.RumbleMotorStrengthLeftHeavySlow = leftHeavySlowMotor; + testRumble.RumbleMotorsExplicitlyOff = rightLightFastMotor == 0 && leftHeavySlowMotor == 0; + } + + private void setTestRumble() + { + if (testRumble.IsRumbleSet()) + { + pushHapticState(testRumble); + if (testRumble.RumbleMotorsExplicitlyOff) + testRumble.RumbleMotorsExplicitlyOff = false; + } + } + + public DS4State getCurrentState() + { + return cState.Clone(); + } + + public DS4State getPreviousState() + { + return pState.Clone(); + } + + public void getExposedState(DS4StateExposed expState, DS4State state) + { + cState.CopyTo(state); + expState.setAccel(accel); + expState.setGyro(gyro); + } + + public void getCurrentState(DS4State state) + { + cState.CopyTo(state); + } + + public void getPreviousState(DS4State state) + { + pState.CopyTo(state); + } + + private bool isDS4Idle() + { + if (cState.Square || cState.Cross || cState.Circle || cState.Triangle) + return false; + if (cState.DpadUp || cState.DpadLeft || cState.DpadDown || cState.DpadRight) + return false; + if (cState.L3 || cState.R3 || cState.L1 || cState.R1 || cState.Share || cState.Options) + return false; + if (cState.L2 != 0 || cState.R2 != 0) + return false; + // TODO calibrate to get an accurate jitter and center-play range and centered position + const int slop = 64; + if (cState.LX <= 127 - slop || cState.LX >= 128 + slop || cState.LY <= 127 - slop || cState.LY >= 128 + slop) + return false; + if (cState.RX <= 127 - slop || cState.RX >= 128 + slop || cState.RY <= 127 - slop || cState.RY >= 128 + slop) + return false; + if (cState.Touch1 || cState.Touch2 || cState.TouchButton) + return false; + return true; + } + + private DS4HapticState[] hapticState = new DS4HapticState[1]; + private int hapticStackIndex = 0; + private void resetHapticState() + { + hapticStackIndex = 0; + } + + // Use the "most recently set" haptic state for each of light bar/motor. + private void setHapticState() + { + DS4Color lightBarColor = LightBarColor; + byte lightBarFlashDurationOn = LightBarOnDuration, lightBarFlashDurationOff = LightBarOffDuration; + byte rumbleMotorStrengthLeftHeavySlow = LeftHeavySlowRumble, rumbleMotorStrengthRightLightFast = rightLightFastRumble; + int hapticLen = hapticState.Length; + for (int i=0; i < hapticLen; i++) + { + DS4HapticState haptic = hapticState[i]; + if (i == hapticStackIndex) + break; // rest haven't been used this time + + if (haptic.IsLightBarSet()) + { + lightBarColor = haptic.LightBarColor; + lightBarFlashDurationOn = haptic.LightBarFlashDurationOn; + lightBarFlashDurationOff = haptic.LightBarFlashDurationOff; + } + + if (haptic.IsRumbleSet()) + { + rumbleMotorStrengthLeftHeavySlow = haptic.RumbleMotorStrengthLeftHeavySlow; + rumbleMotorStrengthRightLightFast = haptic.RumbleMotorStrengthRightLightFast; + } + } + + LightBarColor = lightBarColor; + LightBarOnDuration = lightBarFlashDurationOn; + LightBarOffDuration = lightBarFlashDurationOff; + LeftHeavySlowRumble = rumbleMotorStrengthLeftHeavySlow; + RightLightFastRumble = rumbleMotorStrengthRightLightFast; + } + + public void pushHapticState(DS4HapticState hs) + { + int hapsLen = hapticState.Length; + if (hapticStackIndex == hapsLen) + { + DS4HapticState[] newHaptics = new DS4HapticState[hapsLen + 1]; + Array.Copy(hapticState, newHaptics, hapsLen); + hapticState = newHaptics; + } + + hapticState[hapticStackIndex++] = hs; + } + + override + public String ToString() + { + return Mac; + } + + public void runRemoval() + { + Removal?.Invoke(this, EventArgs.Empty); + } + + public void removeReportHandlers() + { + this.Report = null; + } + + public void queueEvent(Action act) + { + lock (eventQueueLock) + { + eventQueue.Enqueue(act); + } + } + + public void updateSerial() + { + hDevice.resetSerial(); + string tempMac = hDevice.readSerial(); + if (tempMac != Mac) + { + Mac = tempMac; + SerialChange?.Invoke(this, EventArgs.Empty); + } + } + + public bool isValidSerial() + { + return !Mac.Equals(blankSerial); + } + + public static bool isValidSerial(string test) + { + return !test.Equals(blankSerial); + } + } +} diff --git a/DS4Library/DS4Devices.cs b/DS4Library/DS4Devices.cs new file mode 100644 index 0000000000..b1904f619f --- /dev/null +++ b/DS4Library/DS4Devices.cs @@ -0,0 +1,311 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Principal; + +namespace DS4Windows +{ + public class DS4Devices + { + private static Dictionary Devices = new Dictionary(); + private static HashSet DevicePaths = new HashSet(); + // Keep instance of opened exclusive mode devices not in use (Charging while using BT connection) + private static List DisabledDevices = new List(); + public static bool isExclusiveMode = false; + + private static string devicePathToInstanceId(string devicePath) + { + string deviceInstanceId = devicePath; + deviceInstanceId = deviceInstanceId.Remove(0, deviceInstanceId.LastIndexOf('\\') + 1); + deviceInstanceId = deviceInstanceId.Remove(deviceInstanceId.LastIndexOf('{')); + deviceInstanceId = deviceInstanceId.Replace('#', '\\'); + if (deviceInstanceId.EndsWith("\\")) + { + deviceInstanceId = deviceInstanceId.Remove(deviceInstanceId.Length - 1); + } + + return deviceInstanceId; + } + + // Enumerates ds4 controllers in the system + public static void findControllers() + { + lock (Devices) + { + int[] pid = { 0xBA0, 0x5C4, 0x09CC, 0x1000 }; + IEnumerable hDevices = HidDevices.Enumerate(0x1532, pid); + // Sort Bluetooth first in case USB is also connected on the same controller. + hDevices = hDevices.OrderBy((HidDevice d) => { return DS4Device.HidConnectionType(d); }); + + List tempList = hDevices.ToList(); + purgeHiddenExclusiveDevices(); + tempList.AddRange(DisabledDevices); + int devCount = tempList.Count(); + string devicePlural = "device" + (devCount == 0 || devCount > 1 ? "s" : ""); + //Log.LogToGui("Found " + devCount + " possible " + devicePlural + ". Examining " + devicePlural + ".", false); + + for (int i = 0; i < devCount; i++) + //foreach (HidDevice hDevice in hDevices) + { + HidDevice hDevice = tempList[i]; + if (DevicePaths.Contains(hDevice.DevicePath)) + continue; // BT/USB endpoint already open once + + if (!hDevice.IsOpen) + { + hDevice.OpenDevice(isExclusiveMode); + if (!hDevice.IsOpen && isExclusiveMode) + { + try + { + WindowsIdentity identity = WindowsIdentity.GetCurrent(); + WindowsPrincipal principal = new WindowsPrincipal(identity); + bool elevated = principal.IsInRole(WindowsBuiltInRole.Administrator); + + if (!elevated) + { + // Launches an elevated child process to re-enable device + string exeName = Process.GetCurrentProcess().MainModule.FileName; + ProcessStartInfo startInfo = new ProcessStartInfo(exeName); + startInfo.Verb = "runas"; + startInfo.Arguments = "re-enabledevice " + devicePathToInstanceId(hDevice.DevicePath); + Process child = Process.Start(startInfo); + + if (!child.WaitForExit(5000)) + { + child.Kill(); + } + else if (child.ExitCode == 0) + { + hDevice.OpenDevice(isExclusiveMode); + } + } + else + { + reEnableDevice(devicePathToInstanceId(hDevice.DevicePath)); + hDevice.OpenDevice(isExclusiveMode); + } + } + catch (Exception) { } + } + + // TODO in exclusive mode, try to hold both open when both are connected + if (isExclusiveMode && !hDevice.IsOpen) + hDevice.OpenDevice(false); + } + + if (hDevice.IsOpen) + { + string serial = hDevice.readSerial(); + bool validSerial = !serial.Equals(DS4Device.blankSerial); + if (Devices.ContainsKey(serial)) + { + // happens when the BT endpoint already is open and the USB is plugged into the same host + if (isExclusiveMode && hDevice.IsExclusive && + !DisabledDevices.Contains(hDevice)) + { + // Grab reference to exclusively opened HidDevice so device + // stays hidden to other processes + DisabledDevices.Add(hDevice); + //DevicePaths.Add(hDevice.DevicePath); + } + + continue; + } + else + { + DS4Device ds4Device = new DS4Device(hDevice); + //ds4Device.Removal += On_Removal; + Devices.Add(ds4Device.MacAddress, ds4Device); + DevicePaths.Add(hDevice.DevicePath); + } + } + } + } + } + + //allows to get DS4Device by specifying unique MAC address + //format for MAC address is XX:XX:XX:XX:XX:XX + public static DS4Device getDS4Controller(string mac) + { + lock (Devices) + { + DS4Device device = null; + try + { + Devices.TryGetValue(mac, out device); + } + catch (ArgumentNullException) { } + return device; + } + } + + // Returns DS4 controllers that were found and are running + public static IEnumerable getDS4Controllers() + { + lock (Devices) + { + DS4Device[] controllers = new DS4Device[Devices.Count]; + Devices.Values.CopyTo(controllers, 0); + return controllers; + } + } + + public static void stopControllers() + { + lock (Devices) + { + IEnumerable devices = getDS4Controllers(); + //foreach (DS4Device device in devices) + for (int i = 0, devCount = devices.Count(); i < devCount; i++) + { + DS4Device device = devices.ElementAt(i); + device.StopUpdate(); + //device.runRemoval(); + device.HidDevice.CloseDevice(); + } + + Devices.Clear(); + DevicePaths.Clear(); + DisabledDevices.Clear(); + } + } + + // Called when devices is diconnected, timed out or has input reading failure + public static void On_Removal(object sender, EventArgs e) + { + lock (Devices) + { + DS4Device device = (DS4Device)sender; + if (device != null) + { + device.HidDevice.CloseDevice(); + Devices.Remove(device.MacAddress); + DevicePaths.Remove(device.HidDevice.DevicePath); + //purgeHiddenExclusiveDevices(); + } + } + } + + public static void UpdateSerial(object sender, EventArgs e) + { + lock (Devices) + { + DS4Device device = (DS4Device)sender; + if (device != null) + { + string serial = device.getMacAddress(); + if (Devices.ContainsKey(serial)) + { + Devices.Remove(serial); + device.updateSerial(); + serial = device.getMacAddress(); + Devices.Add(serial, device); + } + } + } + } + + private static void purgeHiddenExclusiveDevices() + { + int disabledDevCount = DisabledDevices.Count; + if (disabledDevCount > 0) + { + List disabledDevList = new List(); + for (int i = 0, arlen = disabledDevCount; i < arlen; i++) + { + HidDevice tempDev = DisabledDevices.ElementAt(i); + if (tempDev != null) + { + if (tempDev.IsOpen && tempDev.IsConnected) + { + disabledDevList.Add(tempDev); + } + else if (tempDev.IsOpen) + { + if (!tempDev.IsConnected) + { + try + { + tempDev.CloseDevice(); + } + catch { } + } + + if (DevicePaths.Contains(tempDev.DevicePath)) + { + DevicePaths.Remove(tempDev.DevicePath); + } + } + } + } + + DisabledDevices.Clear(); + DisabledDevices.AddRange(disabledDevList); + } + } + + public static void reEnableDevice(string deviceInstanceId) + { + Stopwatch sw = new Stopwatch(); + bool success; + Guid hidGuid = new Guid(); + NativeMethods.HidD_GetHidGuid(ref hidGuid); + IntPtr deviceInfoSet = NativeMethods.SetupDiGetClassDevs(ref hidGuid, deviceInstanceId, 0, NativeMethods.DIGCF_PRESENT | NativeMethods.DIGCF_DEVICEINTERFACE); + NativeMethods.SP_DEVINFO_DATA deviceInfoData = new NativeMethods.SP_DEVINFO_DATA(); + deviceInfoData.cbSize = Marshal.SizeOf(deviceInfoData); + success = NativeMethods.SetupDiEnumDeviceInfo(deviceInfoSet, 0, ref deviceInfoData); + if (!success) + { + throw new Exception("Error getting device info data, error code = " + Marshal.GetLastWin32Error()); + } + success = NativeMethods.SetupDiEnumDeviceInfo(deviceInfoSet, 1, ref deviceInfoData); // Checks that we have a unique device + if (success) + { + throw new Exception("Can't find unique device"); + } + + NativeMethods.SP_PROPCHANGE_PARAMS propChangeParams = new NativeMethods.SP_PROPCHANGE_PARAMS(); + propChangeParams.classInstallHeader.cbSize = Marshal.SizeOf(propChangeParams.classInstallHeader); + propChangeParams.classInstallHeader.installFunction = NativeMethods.DIF_PROPERTYCHANGE; + propChangeParams.stateChange = NativeMethods.DICS_DISABLE; + propChangeParams.scope = NativeMethods.DICS_FLAG_GLOBAL; + propChangeParams.hwProfile = 0; + success = NativeMethods.SetupDiSetClassInstallParams(deviceInfoSet, ref deviceInfoData, ref propChangeParams, Marshal.SizeOf(propChangeParams)); + if (!success) + { + throw new Exception("Error setting class install params, error code = " + Marshal.GetLastWin32Error()); + } + success = NativeMethods.SetupDiCallClassInstaller(NativeMethods.DIF_PROPERTYCHANGE, deviceInfoSet, ref deviceInfoData); + if (!success) + { + throw new Exception("Error disabling device, error code = " + Marshal.GetLastWin32Error()); + } + + sw.Start(); + while (sw.ElapsedMilliseconds < 50) + { + // Use SpinWait to keep control of current thread. Using Sleep could potentially + // cause other events to get run out of order + System.Threading.Thread.SpinWait(100); + } + sw.Stop(); + + propChangeParams.stateChange = NativeMethods.DICS_ENABLE; + success = NativeMethods.SetupDiSetClassInstallParams(deviceInfoSet, ref deviceInfoData, ref propChangeParams, Marshal.SizeOf(propChangeParams)); + if (!success) + { + throw new Exception("Error setting class install params, error code = " + Marshal.GetLastWin32Error()); + } + success = NativeMethods.SetupDiCallClassInstaller(NativeMethods.DIF_PROPERTYCHANGE, deviceInfoSet, ref deviceInfoData); + if (!success) + { + throw new Exception("Error enabling device, error code = " + Marshal.GetLastWin32Error()); + } + + NativeMethods.SetupDiDestroyDeviceInfoList(deviceInfoSet); + } + } +} diff --git a/DS4Library/DS4Sixaxis.cs b/DS4Library/DS4Sixaxis.cs new file mode 100644 index 0000000000..bed25c0a25 --- /dev/null +++ b/DS4Library/DS4Sixaxis.cs @@ -0,0 +1,104 @@ +using System; + +namespace DS4Windows +{ + public class SixAxisEventArgs : EventArgs + { + public readonly SixAxis sixAxis; + public readonly System.DateTime timeStamp; + public SixAxisEventArgs(System.DateTime utcTimestamp, SixAxis sa) + { + sixAxis = sa; + this.timeStamp = utcTimestamp; + } + } + + public class SixAxis + { + public readonly int gyroX, gyroY, gyroZ, deltaX, deltaY, deltaZ, accelX, accelY, accelZ; + public readonly byte touchID; + public readonly SixAxis previousAxis; + public SixAxis(int X, int Y, int Z, int aX, int aY, int aZ, SixAxis prevAxis = null) + { + gyroX = X; + gyroY = Y; + gyroZ = Z; + accelX = aX; + accelY = aY; + accelZ = aZ; + previousAxis = prevAxis; + if (previousAxis != null) + { + deltaX = X - previousAxis.gyroX; + deltaY = Y - previousAxis.gyroY; + deltaZ = Z - previousAxis.gyroZ; + } + } + } + + public class DS4SixAxis + { + public event EventHandler SixAxisMoved = null; // deltaX/deltaY are set because one or both fingers were already down on a prior sensor reading + public event EventHandler SixAccelMoved = null; // no status change for the touchpad itself... but other sensors may have changed, or you may just want to do some processing + + internal int lastGyroX, lastGyroY, lastGyroZ, lastAX, lastAY, lastAZ; // tracks 0, 1 or 2 touches; we maintain touch 1 and 2 separately + internal byte[] previousPacket = new byte[8]; + + + public void handleSixaxis(byte[] gyro, byte[] accel, DS4State state) + { + //bool touchPadIsDown = sensors.TouchButton; + /*if (!PacketChanged(data, touchPacketOffset) && touchPadIsDown == lastTouchPadIsDown) + { + if (SixAxisUnchanged != null) + SixAxisUnchanged(this, EventArgs.Empty); + return; + }*/ + /* byte touchID1 = (byte)(data[0 + TOUCHPAD_DATA_OFFSET + touchPacketOffset] & 0x7F); + byte touchID2 = (byte)(data[4 + TOUCHPAD_DATA_OFFSET + touchPacketOffset] & 0x7F);*/ + int currentX = (short)((ushort)(gyro[0] << 8) | gyro[1]) / 64; + int currentY = (short)((ushort)(gyro[2] << 8) | gyro[3]) / 64; + int currentZ = (short)((ushort)(gyro[4] << 8) | gyro[5]) / 64; + int AccelX = (short)((ushort)(accel[2] << 8) | accel[3]) / 256; + int AccelY = (short)((ushort)(accel[0] << 8) | accel[1]) / 256; + int AccelZ = (short)((ushort)(accel[4] << 8) | accel[5]) / 256; + SixAxisEventArgs args; + //if (sensors.Touch1 || sensors.Touch2) + { + /* if (SixAxisMoved != null) + { + SixAxis sPrev, now; + sPrev = new SixAxis(lastGyroX, lastGyroY, lastGyroZ, lastAX,lastAY,lastAZ); + now = new SixAxis(currentX, currentY, currentZ, AccelX, AccelY, AccelZ, sPrev); + args = new SixAxisEventArgs(state.ReportTimeStamp, now); + SixAxisMoved(this, args); + } + + lastGyroX = currentX; + lastGyroY = currentY; + lastGyroZ = currentZ; + lastAX = AccelX; + lastAY = AccelY; + lastAZ = AccelZ;*/ + } + if (AccelX != 0 || AccelY != 0 || AccelZ != 0) + { + if (SixAccelMoved != null) + { + SixAxis sPrev, now; + sPrev = new SixAxis(lastGyroX, lastGyroY, lastGyroZ, lastAX, lastAY, lastAZ); + now = new SixAxis(currentX, currentY, currentZ, AccelX, AccelY, AccelZ, sPrev); + args = new SixAxisEventArgs(state.ReportTimeStamp, now); + SixAccelMoved(this, args); + } + + lastGyroX = currentX; + lastGyroY = currentY; + lastGyroZ = currentZ; + lastAX = AccelX; + lastAY = AccelY; + lastAZ = AccelZ; + } + } + } +} diff --git a/DS4Library/DS4State.cs b/DS4Library/DS4State.cs new file mode 100644 index 0000000000..d62810b463 --- /dev/null +++ b/DS4Library/DS4State.cs @@ -0,0 +1,158 @@ +using System; + +namespace DS4Windows +{ + public class DS4State + { + public DateTime ReportTimeStamp; + public bool Square, Triangle, Circle, Cross; + public bool DpadUp, DpadDown, DpadLeft, DpadRight; + public bool L1, L3, R1, R3; + public bool Share, Options, PS, Touch1, Touch2, TouchButton, TouchRight, TouchLeft; + public byte Touch1Identifier, Touch2Identifier; + public byte LX, RX, LY, RY, L2, R2; + public byte FrameCounter; // 0, 1, 2...62, 63, 0.... + public byte TouchPacketCounter; // we break these out automatically + public byte Battery; // 0 for charging, 10/20/30/40/50/60/70/80/90/100 for percentage of full + public double LSAngle; // Calculated bearing of the LS X,Y coordinates + public double RSAngle; // Calculated bearing of the RS X,Y coordinates + public double LSAngleRad; // Calculated bearing of the LS X,Y coordinates (in radians) + public double RSAngleRad; // Calculated bearing of the RS X,Y coordinates (in radians) + public double LXUnit; + public double LYUnit; + public double RXUnit; + public double RYUnit; + public static readonly int DEFAULT_AXISDIR_VALUE = 127; + + public DS4State() + { + Square = Triangle = Circle = Cross = false; + DpadUp = DpadDown = DpadLeft = DpadRight = false; + L1 = L3 = R1 = R3 = false; + Share = Options = PS = Touch1 = Touch2 = TouchButton = TouchRight = TouchLeft = false; + LX = RX = LY = RY = 127; + L2 = R2 = 0; + FrameCounter = 255; // only actually has 6 bits, so this is a null indicator + TouchPacketCounter = 255; // 8 bits, no great junk value + Battery = 0; + LSAngle = 0.0; + LSAngleRad = 0.0; + RSAngle = 0.0; + RSAngleRad = 0.0; + LXUnit = 0.0; + LYUnit = 0.0; + RXUnit = 0.0; + RYUnit = 0.0; + } + + public DS4State(DS4State state) + { + ReportTimeStamp = state.ReportTimeStamp; + Square = state.Square; + Triangle = state.Triangle; + Circle = state.Circle; + Cross = state.Cross; + DpadUp = state.DpadUp; + DpadDown = state.DpadDown; + DpadLeft = state.DpadLeft; + DpadRight = state.DpadRight; + L1 = state.L1; + L2 = state.L2; + L3 = state.L3; + R1 = state.R1; + R2 = state.R2; + R3 = state.R3; + Share = state.Share; + Options = state.Options; + PS = state.PS; + Touch1 = state.Touch1; + TouchRight = state.TouchRight; + TouchLeft = state.TouchLeft; + Touch1Identifier = state.Touch1Identifier; + Touch2 = state.Touch2; + Touch2Identifier = state.Touch2Identifier; + TouchButton = state.TouchButton; + TouchPacketCounter = state.TouchPacketCounter; + LX = state.LX; + RX = state.RX; + LY = state.LY; + RY = state.RY; + FrameCounter = state.FrameCounter; + Battery = state.Battery; + LSAngle = state.LSAngle; + LSAngleRad = state.LSAngleRad; + RSAngle = state.RSAngle; + RSAngleRad = state.RSAngleRad; + LXUnit = state.LXUnit; + LYUnit = state.LYUnit; + RXUnit = state.RXUnit; + RYUnit = state.RYUnit; + } + + public DS4State Clone() + { + return new DS4State(this); + } + + public void CopyTo(DS4State state) + { + state.ReportTimeStamp = ReportTimeStamp; + state.Square = Square; + state.Triangle = Triangle; + state.Circle = Circle; + state.Cross = Cross; + state.DpadUp = DpadUp; + state.DpadDown = DpadDown; + state.DpadLeft = DpadLeft; + state.DpadRight = DpadRight; + state.L1 = L1; + state.L2 = L2; + state.L3 = L3; + state.R1 = R1; + state.R2 = R2; + state.R3 = R3; + state.Share = Share; + state.Options = Options; + state.PS = PS; + state.Touch1 = Touch1; + state.Touch1Identifier = Touch1Identifier; + state.Touch2 = Touch2; + state.Touch2Identifier = Touch2Identifier; + state.TouchLeft = TouchLeft; + state.TouchRight = TouchRight; + state.TouchButton = TouchButton; + state.TouchPacketCounter = TouchPacketCounter; + state.LX = LX; + state.RX = RX; + state.LY = LY; + state.RY = RY; + state.FrameCounter = FrameCounter; + state.Battery = Battery; + state.LSAngle = LSAngle; + state.LSAngleRad = LSAngleRad; + state.RSAngle = RSAngle; + state.RSAngleRad = RSAngleRad; + state.LXUnit = LXUnit; + state.LYUnit = LYUnit; + state.RXUnit = RXUnit; + state.RYUnit = RYUnit; + } + + public void calculateStickAngles() + { + double lsangle = Math.Atan2((LX - 127), -(LY - 127)); + LSAngleRad = lsangle; + lsangle = (lsangle >= 0 ? lsangle : (2 * Math.PI + lsangle)) * 180 / Math.PI; + LSAngle = lsangle; + LXUnit = Math.Abs(Math.Cos(LSAngleRad)); + LYUnit = Math.Abs(Math.Sin(LSAngleRad)); + + double rsangle = Math.Atan2((RX - 127), -(RY - 127)); + RSAngleRad = rsangle; + rsangle = (rsangle >= 0 ? rsangle : (2 * Math.PI + rsangle)) * 180 / Math.PI; + RSAngle = rsangle; + RXUnit = Math.Abs(Math.Cos(RSAngleRad)); + RYUnit = Math.Abs(Math.Sin(LSAngleRad)); + } + } +} diff --git a/DS4Library/DS4StateExposed.cs b/DS4Library/DS4StateExposed.cs new file mode 100644 index 0000000000..c51cf854d7 --- /dev/null +++ b/DS4Library/DS4StateExposed.cs @@ -0,0 +1,95 @@ + +namespace DS4Windows +{ + public class DS4StateExposed + { + private DS4State _state; + private byte[] accel = new byte[] { 0, 0, 0, 0, 0, 0 }, + gyro = new byte[] { 0, 0, 0, 0, 0, 0 }; + + public DS4StateExposed() + { + _state = new DS4State(); + } + public DS4StateExposed(DS4State state) + { + _state = state; + } + + bool Square { get { return _state.Square; } } + bool Triangle { get { return _state.Triangle; } } + bool Circle { get { return _state.Circle; } } + bool Cross { get { return _state.Cross; } } + bool DpadUp { get { return _state.DpadUp; } } + bool DpadDown { get { return _state.DpadDown; } } + bool DpadLeft { get { return _state.DpadLeft; } } + bool DpadRight { get { return _state.DpadRight; } } + bool L1 { get { return _state.L1; } } + bool L3 { get { return _state.L3; } } + bool R1 { get { return _state.R1; } } + bool R3 { get { return _state.R3; } } + bool Share { get { return _state.Share; } } + bool Options { get { return _state.Options; } } + bool PS { get { return _state.PS; } } + bool Touch1 { get { return _state.Touch1; } } + bool Touch2 { get { return _state.Touch2; } } + bool TouchButton { get { return _state.TouchButton; } } + byte LX { get { return _state.LX; } } + byte RX { get { return _state.RX; } } + byte LY { get { return _state.LY; } } + byte RY { get { return _state.RY; } } + byte L2 { get { return _state.L2; } } + byte R2 { get { return _state.R2; } } + int Battery { get { return _state.Battery; } } + + /// Holds raw DS4 input data from 14 to 19 + public byte[] Accel { set { accel = value; } } + public void setAccel(byte[] value) + { + accel = value; + } + + /// Holds raw DS4 input data from 20 to 25 + public byte[] Gyro { set { gyro = value; } } + public void setGyro(byte[] value) + { + gyro = value; + } + + /// Yaw leftward/counter-clockwise/turn to port or larboard side + /// Add double the previous result to this delta and divide by three. + public int AccelX { get { return (short)((ushort)(accel[2] << 8) | accel[3]) / 256; } } + /// Pitch upward/backward + /// Add double the previous result to this delta and divide by three. + public int AccelY { get { return (short)((ushort)(accel[0] << 8) | accel[1] ) / 256; } } + /// roll left/L side of controller down/starboard raising up + /// Add double the previous result to this delta and divide by three. + public int AccelZ { get { return (short)((ushort)(accel[4] << 8) | accel[5]) / 256; } } + /// R side of controller upward + /// Add double the previous result to this delta and divide by three. + public int GyroX { get { return (short)((ushort)(gyro[0] << 8) | gyro[1]) / 64; } } + + public int getGyroX() + { + return (short)((ushort)(gyro[0] << 8) | gyro[1]) / 64; + } + + /// touchpad and button face side of controller upward + /// Add double the previous result to this delta and divide by three. + public int GyroY { get { return (short)((ushort)(gyro[2] << 8) | gyro[3]) / 64; } } + + public int getGyroY() + { + return (short)((ushort)(gyro[2] << 8) | gyro[3]) / 64; + } + + /// Audio/expansion ports upward and light bar/shoulders/bumpers/USB port downward + /// Add double the previous result to this delta and divide by three. + public int GyroZ { get { return (short)((ushort)(gyro[4] << 8) | gyro[5]) / 64; } } + + public int getGyroZ() + { + return (short)((ushort)(gyro[4] << 8) | gyro[5]) / 64; + } + } +} diff --git a/DS4Library/DS4Touchpad.cs b/DS4Library/DS4Touchpad.cs new file mode 100644 index 0000000000..21f673e337 --- /dev/null +++ b/DS4Library/DS4Touchpad.cs @@ -0,0 +1,215 @@ +using System; + +namespace DS4Windows +{ + public class TouchpadEventArgs : EventArgs + { + public readonly Touch[] touches = null; + public readonly DateTime timeStamp; + public readonly bool touchButtonPressed; + public TouchpadEventArgs(DateTime utcTimestamp, bool tButtonDown, Touch t0, Touch t1 = null) + { + if (t1 != null) + { + touches = new Touch[2]; + touches[0] = t0; + touches[1] = t1; + } + else if (t0 != null) + { + touches = new Touch[1]; + touches[0] = t0; + } + + touchButtonPressed = tButtonDown; + timeStamp = utcTimestamp; + } + } + + public class Touch + { + public readonly int hwX, hwY, deltaX, deltaY; + public readonly byte touchID; + public readonly Touch previousTouch; + public Touch(int X, int Y, byte tID, Touch prevTouch = null) + { + hwX = X; + hwY = Y; + touchID = tID; + previousTouch = prevTouch; + if (previousTouch != null) + { + deltaX = X - previousTouch.hwX; + deltaY = Y - previousTouch.hwY; + } + } + } + + public class DS4Touchpad + { + public event EventHandler TouchesBegan = null; // finger one or two landed (or both, or one then two, or two then one; any touches[] count increase) + public event EventHandler TouchesMoved = null; // deltaX/deltaY are set because one or both fingers were already down on a prior sensor reading + public event EventHandler TouchesEnded = null; // all fingers lifted + public event EventHandler TouchButtonDown = null; // touchpad pushed down until the button clicks + public event EventHandler TouchButtonUp = null; // touchpad button released + public event EventHandler TouchUnchanged = null; // no status change for the touchpad itself... but other sensors may have changed, or you may just want to do some processing + public event EventHandler PreTouchProcess = null; // used to publish that a touch packet is about to be processed + + public readonly static int TOUCHPAD_DATA_OFFSET = 35; + internal int lastTouchPadX1, lastTouchPadY1, + lastTouchPadX2, lastTouchPadY2; // tracks 0, 1 or 2 touches; we maintain touch 1 and 2 separately + internal bool lastTouchPadIsDown; + internal bool lastIsActive1, lastIsActive2; + internal byte lastTouchID1, lastTouchID2; + internal byte[] previousPacket = new byte[8]; + + // We check everything other than the not bothering with not-very-useful TouchPacketCounter. + private bool PacketChanged(byte[] data, int touchPacketOffset) + { + bool changed = false; + for (int i = 0, arLen = previousPacket.Length; i < arLen; i++) + { + byte oldValue = previousPacket[i]; + previousPacket[i] = data[i + TOUCHPAD_DATA_OFFSET + touchPacketOffset]; + if (previousPacket[i] != oldValue) + changed = true; + } + + return changed; + } + + public void handleTouchpad(byte[] data, DS4State sensors, int touchPacketOffset = 0) + { + PreTouchProcess?.Invoke(this, EventArgs.Empty); + + bool touchPadIsDown = sensors.TouchButton; + if (!PacketChanged(data, touchPacketOffset) && touchPadIsDown == lastTouchPadIsDown) + { + if (TouchUnchanged != null) + TouchUnchanged(this, EventArgs.Empty); + return; + } + + byte touchID1 = (byte)(data[0 + TOUCHPAD_DATA_OFFSET + touchPacketOffset] & 0x7F); + byte touchID2 = (byte)(data[4 + TOUCHPAD_DATA_OFFSET + touchPacketOffset] & 0x7F); + int currentX1 = data[1 + TOUCHPAD_DATA_OFFSET + touchPacketOffset] + ((data[2 + TOUCHPAD_DATA_OFFSET + touchPacketOffset] & 0xF) * 255); + int currentY1 = ((data[2 + TOUCHPAD_DATA_OFFSET + touchPacketOffset] & 0xF0) >> 4) + (data[3 + TOUCHPAD_DATA_OFFSET + touchPacketOffset] * 16); + int currentX2 = data[5 + TOUCHPAD_DATA_OFFSET + touchPacketOffset] + ((data[6 + TOUCHPAD_DATA_OFFSET + touchPacketOffset] & 0xF) * 255); + int currentY2 = ((data[6 + TOUCHPAD_DATA_OFFSET + touchPacketOffset] & 0xF0) >> 4) + (data[7 + TOUCHPAD_DATA_OFFSET + touchPacketOffset] * 16); + + TouchpadEventArgs args; + if (sensors.Touch1 || sensors.Touch2) + { + if ((sensors.Touch1 && !lastIsActive1) || (sensors.Touch2 && !lastIsActive2)) + { + if (TouchesBegan != null) + { + if (sensors.Touch1 && sensors.Touch2) + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, new Touch(currentX1, currentY1, touchID1), new Touch(currentX2, currentY2, touchID2)); + else if (sensors.Touch1) + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, new Touch(currentX1, currentY1, touchID1)); + else + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, new Touch(currentX2, currentY2, touchID2)); + + TouchesBegan(this, args); + } + } + else if (sensors.Touch1 == lastIsActive1 && sensors.Touch2 == lastIsActive2 && TouchesMoved != null) + { + Touch tPrev, t0, t1; + + if (sensors.Touch1 && sensors.Touch2) + { + tPrev = new Touch(lastTouchPadX1, lastTouchPadY1, lastTouchID1); + t0 = new Touch(currentX1, currentY1, touchID1, tPrev); + tPrev = new Touch(lastTouchPadX2, lastTouchPadY2, lastTouchID2); + t1 = new Touch(currentX2, currentY2, touchID2, tPrev); + } + else if (sensors.Touch1) + { + tPrev = new Touch(lastTouchPadX1, lastTouchPadY1, lastTouchID1); + t0 = new Touch(currentX1, currentY1, touchID1, tPrev); + t1 = null; + } + else + { + tPrev = new Touch(lastTouchPadX2, lastTouchPadY2, lastTouchID2); + t0 = new Touch(currentX2, currentY2, touchID2, tPrev); + t1 = null; + } + + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, t0, t1); + + TouchesMoved(this, args); + } + + if (!lastTouchPadIsDown && touchPadIsDown && TouchButtonDown != null) + { + if (sensors.Touch1 && sensors.Touch2) + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, new Touch(currentX1, currentY1, touchID1), new Touch(currentX2, currentY2, touchID2)); + else if (sensors.Touch1) + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, new Touch(currentX1, currentY1, touchID1)); + else + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, new Touch(currentX2, currentY2, touchID2)); + + TouchButtonDown(this, args); + } + else if (lastTouchPadIsDown && !touchPadIsDown && TouchButtonUp != null) + { + if (sensors.Touch1 && sensors.Touch2) + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, new Touch(currentX1, currentY1, touchID1), new Touch(currentX2, currentY2, touchID2)); + else if (sensors.Touch1) + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, new Touch(currentX1, currentY1, touchID1)); + else + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, new Touch(currentX2, currentY2, touchID2)); + + TouchButtonUp(this, args); + } + + if (sensors.Touch1) + { + lastTouchPadX1 = currentX1; + lastTouchPadY1 = currentY1; + } + if (sensors.Touch2) + { + lastTouchPadX2 = currentX2; + lastTouchPadY2 = currentY2; + } + + lastTouchPadIsDown = touchPadIsDown; + } + else + { + if (touchPadIsDown && !lastTouchPadIsDown) + { + if (TouchButtonDown != null) + TouchButtonDown(this, new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, null, null)); + } + else if (!touchPadIsDown && lastTouchPadIsDown) + { + if (TouchButtonUp != null) + TouchButtonUp(this, new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, null, null)); + } + + if ((lastIsActive1 || lastIsActive2) && TouchesEnded != null) + { + if (lastIsActive1 && lastIsActive2) + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, new Touch(lastTouchPadX1, lastTouchPadY1, touchID1), new Touch(lastTouchPadX2, lastTouchPadY2, touchID2)); + else if (lastIsActive1) + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, new Touch(lastTouchPadX1, lastTouchPadY1, touchID1)); + else + args = new TouchpadEventArgs(sensors.ReportTimeStamp, sensors.TouchButton, new Touch(lastTouchPadX2, lastTouchPadY2, touchID2)); + + TouchesEnded(this, args); + } + } + + lastIsActive1 = sensors.Touch1; + lastIsActive2 = sensors.Touch2; + lastTouchID1 = touchID1; + lastTouchID2 = touchID2; + lastTouchPadIsDown = touchPadIsDown; + } + } +}