2014-03-28 02:50:40 +01:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.ComponentModel;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
|
|
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
using Microsoft.Win32.SafeHandles;
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
using HidLibrary;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Collections;
|
|
|
|
|
namespace DS4Library
|
|
|
|
|
{
|
|
|
|
|
public struct DS4Color
|
|
|
|
|
{
|
|
|
|
|
public byte red;
|
|
|
|
|
public byte green;
|
|
|
|
|
public byte blue;
|
|
|
|
|
}
|
|
|
|
|
|
2014-03-29 06:29:08 +01:00
|
|
|
|
public enum ConnectionType : byte { BT, USB }; // Prioritize Bluetooth when both 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2014-03-28 02:50:40 +01:00
|
|
|
|
|
|
|
|
|
public class DS4Device
|
|
|
|
|
{
|
|
|
|
|
private const int BT_OUTPUT_REPORT_LENGTH = 78;
|
|
|
|
|
private const int BT_INPUT_REPORT_LENGTH = 547;
|
|
|
|
|
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];
|
2014-03-29 06:29:08 +01:00
|
|
|
|
private byte[] inputReport;
|
2014-03-28 02:50:40 +01:00
|
|
|
|
private byte[] btInputReport = null;
|
2014-03-29 06:29:08 +01:00
|
|
|
|
private byte[] outputReportBuffer, outputReport;
|
2014-03-28 02:50:40 +01:00
|
|
|
|
private readonly DS4Touchpad touchpad = null;
|
|
|
|
|
private byte rightLightFastRumble;
|
|
|
|
|
private byte leftHeavySlowRumble;
|
|
|
|
|
private DS4Color ligtBarColor;
|
|
|
|
|
private byte ledFlashOn, ledFlashOff;
|
2014-03-29 06:29:08 +01:00
|
|
|
|
private Thread ds4Input, ds4Output;
|
2014-03-28 02:50:40 +01:00
|
|
|
|
private int battery;
|
2014-05-22 21:13:38 +02:00
|
|
|
|
public DateTime lastActive = DateTime.UtcNow;
|
2014-03-29 06:29:08 +01:00
|
|
|
|
private bool charging;
|
2014-03-28 02:50:40 +01:00
|
|
|
|
public event EventHandler<EventArgs> Report = null;
|
|
|
|
|
public event EventHandler<EventArgs> Removal = null;
|
|
|
|
|
|
|
|
|
|
public HidDevice HidDevice { get { return hDevice; } }
|
2014-03-29 06:29:08 +01:00
|
|
|
|
public bool IsExclusive { get { return HidDevice.IsExclusive; } }
|
|
|
|
|
public bool IsDisconnecting { get; private set; }
|
2014-03-28 02:50:40 +01:00
|
|
|
|
|
|
|
|
|
public string MacAddress { get { return Mac; } }
|
|
|
|
|
|
|
|
|
|
public ConnectionType ConnectionType { get { return conType; } }
|
2014-04-27 21:32:09 +02:00
|
|
|
|
public int IdleTimeout { get; set; } // behavior only active when > 0
|
2014-03-28 02:50:40 +01:00
|
|
|
|
|
|
|
|
|
public int Battery { get { return battery; } }
|
2014-03-29 06:29:08 +01:00
|
|
|
|
public bool Charging { get { return charging; } }
|
2014-03-28 02:50:40 +01:00
|
|
|
|
|
|
|
|
|
public byte RightLightFastRumble
|
|
|
|
|
{
|
|
|
|
|
get { return rightLightFastRumble; }
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (value == rightLightFastRumble) return;
|
|
|
|
|
rightLightFastRumble = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public byte LeftHeavySlowRumble
|
|
|
|
|
{
|
|
|
|
|
get { return leftHeavySlowRumble; }
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (value == leftHeavySlowRumble) return;
|
|
|
|
|
leftHeavySlowRumble = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 LightBarOffDuration
|
|
|
|
|
{
|
|
|
|
|
get { return ledFlashOff; }
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (ledFlashOff != value)
|
|
|
|
|
{
|
|
|
|
|
ledFlashOff = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public DS4Touchpad Touchpad { get { return touchpad; } }
|
|
|
|
|
|
2014-03-29 06:29:08 +01:00
|
|
|
|
public static ConnectionType HidConnectionType(HidDevice hidDevice)
|
|
|
|
|
{
|
|
|
|
|
return hidDevice.Capabilities.InputReportByteLength == 64 ? ConnectionType.USB : ConnectionType.BT;
|
|
|
|
|
}
|
|
|
|
|
|
2014-03-28 02:50:40 +01:00
|
|
|
|
public DS4Device(HidDevice hidDevice)
|
|
|
|
|
{
|
|
|
|
|
hDevice = hidDevice;
|
2014-03-29 06:29:08 +01:00
|
|
|
|
conType = HidConnectionType(hDevice);
|
2014-03-28 02:50:40 +01:00
|
|
|
|
Mac = hDevice.readSerial();
|
|
|
|
|
if (conType == ConnectionType.USB)
|
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
inputReport = new byte[64];
|
2014-03-28 02:50:40 +01:00
|
|
|
|
outputReport = new byte[hDevice.Capabilities.OutputReportByteLength];
|
2014-03-29 06:29:08 +01:00
|
|
|
|
outputReportBuffer = new byte[hDevice.Capabilities.OutputReportByteLength];
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
btInputReport = new byte[BT_INPUT_REPORT_LENGTH];
|
2014-03-29 06:29:08 +01:00
|
|
|
|
inputReport = new byte[btInputReport.Length - 2];
|
2014-03-28 02:50:40 +01:00
|
|
|
|
outputReport = new byte[BT_OUTPUT_REPORT_LENGTH];
|
2014-03-29 06:29:08 +01:00
|
|
|
|
outputReportBuffer = new byte[BT_OUTPUT_REPORT_LENGTH];
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
touchpad = new DS4Touchpad();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void StartUpdate()
|
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
if (ds4Input == null)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
Console.WriteLine(MacAddress.ToString() + " " + System.DateTime.UtcNow.ToString("o") + "> start");
|
2014-04-27 21:32:09 +02:00
|
|
|
|
sendOutputReport(true); // initialize the output report
|
2014-03-29 06:29:08 +01:00
|
|
|
|
ds4Output = new Thread(performDs4Output);
|
|
|
|
|
ds4Output.Name = "DS4 Output thread: " + Mac;
|
|
|
|
|
ds4Output.Start();
|
|
|
|
|
ds4Input = new Thread(performDs4Input);
|
|
|
|
|
ds4Input.Name = "DS4 Input thread: " + Mac;
|
|
|
|
|
ds4Input.Start();
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
Console.WriteLine("Thread already running for DS4: " + Mac);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void StopUpdate()
|
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
if (ds4Input.ThreadState != System.Threading.ThreadState.Stopped || ds4Input.ThreadState != System.Threading.ThreadState.Aborted)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
ds4Input.Abort();
|
|
|
|
|
ds4Input.Join();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine(e.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
StopOutputUpdate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void StopOutputUpdate()
|
|
|
|
|
{
|
|
|
|
|
if (ds4Output.ThreadState != System.Threading.ThreadState.Stopped || ds4Output.ThreadState != System.Threading.ThreadState.Aborted)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
ds4Output.Abort();
|
|
|
|
|
ds4Output.Join();
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine(e.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-03-29 06:29:08 +01:00
|
|
|
|
private bool writeOutput()
|
|
|
|
|
{
|
|
|
|
|
if (conType == ConnectionType.BT)
|
|
|
|
|
{
|
|
|
|
|
return hDevice.WriteOutputReportViaControl(outputReport);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return hDevice.WriteOutputReportViaInterrupt(outputReport, 8);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void performDs4Output()
|
|
|
|
|
{
|
|
|
|
|
lock (outputReport)
|
|
|
|
|
{
|
2014-04-27 21:32:09 +02:00
|
|
|
|
int lastError = 0;
|
|
|
|
|
while (true)
|
2014-03-29 06:29:08 +01:00
|
|
|
|
{
|
2014-04-27 21:32:09 +02:00
|
|
|
|
if (writeOutput())
|
|
|
|
|
{
|
|
|
|
|
lastError = 0;
|
|
|
|
|
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);
|
|
|
|
|
}
|
2014-03-29 06:29:08 +01:00
|
|
|
|
else
|
2014-04-27 21:32:09 +02:00
|
|
|
|
{
|
|
|
|
|
int thisError = Marshal.GetLastWin32Error();
|
|
|
|
|
if (lastError != thisError)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine(MacAddress.ToString() + " " + System.DateTime.UtcNow.ToString("o") + "> encountered write failure: " + thisError);
|
|
|
|
|
lastError = thisError;
|
|
|
|
|
}
|
|
|
|
|
}
|
2014-03-29 06:29:08 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Is the device alive and receiving valid sensor input reports? */
|
|
|
|
|
public bool IsAlive()
|
|
|
|
|
{
|
|
|
|
|
return priorInputReport30 != 0xff;
|
|
|
|
|
}
|
|
|
|
|
private byte priorInputReport30 = 0xff;
|
Shift modifier: Hold an action to use another set of controls, if nothing is set to the shifted control, in falls back to the default action
View input of controls in profiles, see exactly when a deadzone is passed and check the input delay for controllers (special thanks to jhebbel), click the on sixaxis panel
Click the Empty text on in the lightbar box to copy the lightbar color from full to empty.
While opened, option to keep the window size after closing the profile's settings
Old profiles are automatically upgraded if it's missing new settings, such as how colors are now saved, sixaxis deadzones, and shift controls
Other UI changes for profile settings, flipped touchpad and other settings boxes
Others:
Fix for when clicking the semicolon in the select an action screen
Fix assigning Sixaxis action to a key
minor UI changes and bug fixes, such as auto resize of the log listview
DS4Updater: Also now works for the new numbering system, can read the version number right from the exe instead of in profiles.xml, UI additions to better notify users of errors, Bug fixes for non-portable users
2014-07-07 21:22:42 +02:00
|
|
|
|
public double Latency = 0;
|
|
|
|
|
bool warn;
|
2014-03-29 06:29:08 +01:00
|
|
|
|
private void performDs4Input()
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-04-27 21:32:09 +02:00
|
|
|
|
System.Timers.Timer readTimeout = new System.Timers.Timer(); // Await 30 seconds for the initial packet, then 3 seconds thereafter.
|
|
|
|
|
readTimeout.Elapsed += delegate { HidDevice.CancelIO(); };
|
Shift modifier: Hold an action to use another set of controls, if nothing is set to the shifted control, in falls back to the default action
View input of controls in profiles, see exactly when a deadzone is passed and check the input delay for controllers (special thanks to jhebbel), click the on sixaxis panel
Click the Empty text on in the lightbar box to copy the lightbar color from full to empty.
While opened, option to keep the window size after closing the profile's settings
Old profiles are automatically upgraded if it's missing new settings, such as how colors are now saved, sixaxis deadzones, and shift controls
Other UI changes for profile settings, flipped touchpad and other settings boxes
Others:
Fix for when clicking the semicolon in the select an action screen
Fix assigning Sixaxis action to a key
minor UI changes and bug fixes, such as auto resize of the log listview
DS4Updater: Also now works for the new numbering system, can read the version number right from the exe instead of in profiles.xml, UI additions to better notify users of errors, Bug fixes for non-portable users
2014-07-07 21:22:42 +02:00
|
|
|
|
List<long> Latency = new List<long>();
|
|
|
|
|
long oldtime = 0;
|
|
|
|
|
Stopwatch sw = new Stopwatch();
|
|
|
|
|
sw.Start();
|
2014-03-28 02:50:40 +01:00
|
|
|
|
while (true)
|
|
|
|
|
{
|
Shift modifier: Hold an action to use another set of controls, if nothing is set to the shifted control, in falls back to the default action
View input of controls in profiles, see exactly when a deadzone is passed and check the input delay for controllers (special thanks to jhebbel), click the on sixaxis panel
Click the Empty text on in the lightbar box to copy the lightbar color from full to empty.
While opened, option to keep the window size after closing the profile's settings
Old profiles are automatically upgraded if it's missing new settings, such as how colors are now saved, sixaxis deadzones, and shift controls
Other UI changes for profile settings, flipped touchpad and other settings boxes
Others:
Fix for when clicking the semicolon in the select an action screen
Fix assigning Sixaxis action to a key
minor UI changes and bug fixes, such as auto resize of the log listview
DS4Updater: Also now works for the new numbering system, can read the version number right from the exe instead of in profiles.xml, UI additions to better notify users of errors, Bug fixes for non-portable users
2014-07-07 21:22:42 +02:00
|
|
|
|
Latency.Add(sw.ElapsedMilliseconds - oldtime);
|
|
|
|
|
oldtime = sw.ElapsedMilliseconds;
|
|
|
|
|
|
|
|
|
|
if (Latency.Count > 100)
|
|
|
|
|
Latency.RemoveAt(0);
|
|
|
|
|
|
|
|
|
|
this.Latency = Latency.Average();
|
|
|
|
|
|
|
|
|
|
if (this.Latency > 10 && !warn && sw.ElapsedMilliseconds > 4000)
|
|
|
|
|
{
|
|
|
|
|
warn = true;
|
|
|
|
|
//System.Diagnostics.Trace.WriteLine(System.DateTime.UtcNow.ToString("o") + "> " + "Controller " + /*this.DeviceNum*/ + 1 + " (" + this.MacAddress + ") is experiencing latency issues. Currently at " + Math.Round(this.Latency, 2).ToString() + "ms of recomended maximum 10ms");
|
|
|
|
|
}
|
|
|
|
|
else if (this.Latency <= 10 && warn) warn = false;
|
|
|
|
|
|
2014-04-27 21:32:09 +02:00
|
|
|
|
if (readTimeout.Interval != 3000.0)
|
|
|
|
|
{
|
|
|
|
|
if (readTimeout.Interval != 30000.0)
|
|
|
|
|
readTimeout.Interval = 30000.0;
|
|
|
|
|
else
|
|
|
|
|
readTimeout.Interval = 3000.0;
|
|
|
|
|
}
|
|
|
|
|
readTimeout.Enabled = true;
|
2014-03-28 02:50:40 +01:00
|
|
|
|
if (conType != ConnectionType.USB)
|
2014-04-27 21:32:09 +02:00
|
|
|
|
{
|
|
|
|
|
HidDevice.ReadStatus res = hDevice.ReadFile(btInputReport);
|
|
|
|
|
readTimeout.Enabled = false;
|
|
|
|
|
if (res == HidDevice.ReadStatus.Success)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
Array.Copy(btInputReport, 2, inputReport, 0, inputReport.Length);
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2014-04-27 21:32:09 +02:00
|
|
|
|
Console.WriteLine(MacAddress.ToString() + " " + System.DateTime.UtcNow.ToString("o") + "> disconnect due to read failure: " + Marshal.GetLastWin32Error());
|
2014-03-29 06:29:08 +01:00
|
|
|
|
sendOutputReport(true); // Kick Windows into noticing the disconnection.
|
|
|
|
|
StopOutputUpdate();
|
2014-04-27 21:32:09 +02:00
|
|
|
|
IsDisconnecting = true;
|
|
|
|
|
if (Removal != null)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
Removal(this, EventArgs.Empty);
|
2014-04-27 21:32:09 +02:00
|
|
|
|
return;
|
|
|
|
|
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
2014-04-27 21:32:09 +02:00
|
|
|
|
}
|
|
|
|
|
else
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-04-27 21:32:09 +02:00
|
|
|
|
HidDevice.ReadStatus res = hDevice.ReadFile(inputReport);
|
|
|
|
|
readTimeout.Enabled = false;
|
|
|
|
|
if (res != HidDevice.ReadStatus.Success)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine(MacAddress.ToString() + " " + System.DateTime.UtcNow.ToString("o") + "> disconnect due to read failure: " + Marshal.GetLastWin32Error());
|
|
|
|
|
StopOutputUpdate();
|
|
|
|
|
IsDisconnecting = true;
|
|
|
|
|
if (Removal != null)
|
|
|
|
|
Removal(this, EventArgs.Empty);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
if (ConnectionType == ConnectionType.BT && btInputReport[0] != 0x11)
|
2014-03-29 06:29:08 +01:00
|
|
|
|
{
|
|
|
|
|
//Received incorrect report, skip it
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
DateTime utcNow = System.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 = ((byte)inputReport[5] & (1 << 7)) != 0;
|
|
|
|
|
cState.Circle = ((byte)inputReport[5] & (1 << 6)) != 0;
|
|
|
|
|
cState.Cross = ((byte)inputReport[5] & (1 << 5)) != 0;
|
|
|
|
|
cState.Square = ((byte)inputReport[5] & (1 << 4)) != 0;
|
|
|
|
|
cState.DpadUp = ((byte)inputReport[5] & (1 << 3)) != 0;
|
|
|
|
|
cState.DpadDown = ((byte)inputReport[5] & (1 << 2)) != 0;
|
|
|
|
|
cState.DpadLeft = ((byte)inputReport[5] & (1 << 1)) != 0;
|
|
|
|
|
cState.DpadRight = ((byte)inputReport[5] & (1 << 0)) != 0;
|
|
|
|
|
|
|
|
|
|
//Convert dpad into individual On/Off bits instead of a clock representation
|
|
|
|
|
byte dpad_state = 0;
|
|
|
|
|
|
|
|
|
|
dpad_state = (byte)(
|
|
|
|
|
((cState.DpadRight ? 1 : 0) << 0) |
|
|
|
|
|
((cState.DpadLeft ? 1 : 0) << 1) |
|
|
|
|
|
((cState.DpadDown ? 1 : 0) << 2) |
|
|
|
|
|
((cState.DpadUp ? 1 : 0) << 3));
|
|
|
|
|
|
|
|
|
|
switch (dpad_state)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
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: cState.DpadUp = false; cState.DpadDown = false; cState.DpadLeft = false; cState.DpadRight = false; break;
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2014-03-29 06:29:08 +01:00
|
|
|
|
cState.R3 = ((byte)inputReport[6] & (1 << 7)) != 0;
|
|
|
|
|
cState.L3 = ((byte)inputReport[6] & (1 << 6)) != 0;
|
|
|
|
|
cState.Options = ((byte)inputReport[6] & (1 << 5)) != 0;
|
|
|
|
|
cState.Share = ((byte)inputReport[6] & (1 << 4)) != 0;
|
|
|
|
|
cState.R1 = ((byte)inputReport[6] & (1 << 1)) != 0;
|
|
|
|
|
cState.L1 = ((byte)inputReport[6] & (1 << 0)) != 0;
|
|
|
|
|
|
|
|
|
|
cState.PS = ((byte)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);
|
|
|
|
|
|
|
|
|
|
charging = (inputReport[30] & 0x10) != 0;
|
|
|
|
|
battery = (inputReport[30] & 0x0f) * 10;
|
|
|
|
|
cState.Battery = (byte)battery;
|
|
|
|
|
if (inputReport[30] != priorInputReport30)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
priorInputReport30 = inputReport[30];
|
|
|
|
|
Console.WriteLine(MacAddress.ToString() + " " + System.DateTime.UtcNow.ToString("o") + "> power subsystem octet: 0x" + inputReport[30].ToString("x02"));
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
2014-03-29 06:29:08 +01:00
|
|
|
|
|
|
|
|
|
// XXX DS4State mapping needs fixup, turn touches into an array[4] of structs. And include the touchpad details there instead.
|
|
|
|
|
for (int touches = inputReport[-1 + DS4Touchpad.TOUCHPAD_DATA_OFFSET - 1], touchOffset = 0; touches > 0; touches--, touchOffset += 9)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
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);
|
2014-04-27 21:32:09 +02:00
|
|
|
|
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
|
2014-03-29 06:29:08 +01:00
|
|
|
|
touchpad.handleTouchpad(inputReport, cState, touchOffset);
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
2014-03-29 06:29:08 +01:00
|
|
|
|
|
|
|
|
|
/* Debug output of incoming HID data:
|
|
|
|
|
if (cState.L2 == 0xff && cState.R2 == 0xff)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
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();
|
|
|
|
|
} */
|
2014-05-28 04:49:58 +02:00
|
|
|
|
if (!isNonSixaxisIdle())
|
|
|
|
|
lastActive = utcNow;
|
2014-04-27 21:32:09 +02:00
|
|
|
|
if (conType == ConnectionType.BT)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-04-27 21:32:09 +02:00
|
|
|
|
bool shouldDisconnect = false;
|
2014-05-28 04:49:58 +02:00
|
|
|
|
/*if ((!pState.PS || !pState.Options) && cState.PS && cState.Options)
|
2014-04-27 21:32:09 +02:00
|
|
|
|
{
|
|
|
|
|
shouldDisconnect = true;
|
2014-05-28 04:49:58 +02:00
|
|
|
|
for (int i = 0; i < 255; i++)
|
|
|
|
|
ReleaseKeys(i);
|
|
|
|
|
}*/
|
|
|
|
|
if (IdleTimeout > 0)
|
2014-04-27 21:32:09 +02:00
|
|
|
|
{
|
2014-05-28 04:49:58 +02:00
|
|
|
|
if (isNonSixaxisIdle())
|
2014-04-27 21:32:09 +02:00
|
|
|
|
{
|
|
|
|
|
DateTime timeout = lastActive + TimeSpan.FromSeconds(IdleTimeout);
|
2014-05-28 04:49:58 +02:00
|
|
|
|
if (!Charging)
|
|
|
|
|
shouldDisconnect = utcNow >= timeout;
|
2014-04-27 21:32:09 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (shouldDisconnect && DisconnectBT())
|
2014-03-29 06:29:08 +01:00
|
|
|
|
return; // all done
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
2014-03-29 06:29:08 +01:00
|
|
|
|
// XXX fix initialization ordering so the null checks all go away
|
2014-03-28 02:50:40 +01:00
|
|
|
|
if (Report != null)
|
|
|
|
|
Report(this, EventArgs.Empty);
|
2014-03-29 06:29:08 +01:00
|
|
|
|
sendOutputReport(false);
|
|
|
|
|
|
2014-04-27 21:32:09 +02:00
|
|
|
|
cState.CopyTo(pState);
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-03-29 06:29:08 +01:00
|
|
|
|
private void sendOutputReport(bool synchronous)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
setTestRumble();
|
|
|
|
|
setHapticState();
|
|
|
|
|
if (conType == ConnectionType.BT)
|
|
|
|
|
{
|
|
|
|
|
outputReportBuffer[0] = 0x11;
|
|
|
|
|
outputReportBuffer[1] = 0x80;
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
lock (outputReport)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
if (synchronous)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
outputReportBuffer.CopyTo(outputReport, 0);
|
|
|
|
|
try
|
|
|
|
|
{
|
2014-04-27 21:32:09 +02:00
|
|
|
|
if (!writeOutput())
|
|
|
|
|
Console.WriteLine(MacAddress.ToString() + " " + System.DateTime.UtcNow.ToString("o") + "> encountered synchronous write failure: " + Marshal.GetLastWin32Error());
|
2014-03-29 06:29:08 +01:00
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// If it's dead already, don't worry about it.
|
|
|
|
|
}
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
bool output = false;
|
|
|
|
|
for (int i = 0; !output && i < outputReport.Length; i++)
|
|
|
|
|
output = outputReport[i] != outputReportBuffer[i];
|
|
|
|
|
if (output)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
outputReportBuffer.CopyTo(outputReport, 0);
|
|
|
|
|
Monitor.Pulse(outputReport);
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool DisconnectBT()
|
|
|
|
|
{
|
|
|
|
|
if (Mac != null)
|
|
|
|
|
{
|
2014-04-27 21:32:09 +02:00
|
|
|
|
Console.WriteLine("Trying to disconnect BT device " + Mac);
|
2014-03-28 02:50:40 +01:00
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
bool success = false;
|
|
|
|
|
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);
|
2014-03-29 06:29:08 +01:00
|
|
|
|
Console.WriteLine("Disconnect successful: " + success);
|
|
|
|
|
success = true; // XXX return value indicates failure, but it still works?
|
2014-03-28 02:50:40 +01:00
|
|
|
|
if(success)
|
2014-03-29 06:29:08 +01:00
|
|
|
|
{
|
|
|
|
|
IsDisconnecting = true;
|
|
|
|
|
StopOutputUpdate();
|
2014-03-28 02:50:40 +01:00
|
|
|
|
if (Removal != null)
|
|
|
|
|
Removal(this, EventArgs.Empty);
|
2014-03-29 06:29:08 +01:00
|
|
|
|
}
|
2014-03-28 02:50:40 +01:00
|
|
|
|
return success;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2014-03-29 06:29:08 +01:00
|
|
|
|
private DS4HapticState testRumble = new DS4HapticState();
|
|
|
|
|
public void setRumble(byte rightLightFastMotor, byte leftHeavySlowMotor)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
testRumble.RumbleMotorStrengthRightLightFast = rightLightFastMotor;
|
|
|
|
|
testRumble.RumbleMotorStrengthLeftHeavySlow = leftHeavySlowMotor;
|
|
|
|
|
testRumble.RumbleMotorsExplicitlyOff = rightLightFastMotor == 0 && leftHeavySlowMotor == 0;
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2014-03-29 06:29:08 +01:00
|
|
|
|
private void setTestRumble()
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
if (testRumble.IsRumbleSet())
|
|
|
|
|
{
|
|
|
|
|
pushHapticState(testRumble);
|
|
|
|
|
if (testRumble.RumbleMotorsExplicitlyOff)
|
|
|
|
|
testRumble.RumbleMotorsExplicitlyOff = false;
|
|
|
|
|
}
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public DS4State getCurrentState()
|
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
return cState.Clone();
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public DS4State getPreviousState()
|
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
return pState.Clone();
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void getExposedState(DS4StateExposed expState, DS4State state)
|
|
|
|
|
{
|
2014-04-27 21:32:09 +02:00
|
|
|
|
cState.CopyTo(state);
|
2014-03-28 02:50:40 +01:00
|
|
|
|
expState.Accel = accel;
|
|
|
|
|
expState.Gyro = gyro;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void getCurrentState(DS4State state)
|
|
|
|
|
{
|
2014-04-27 21:32:09 +02:00
|
|
|
|
cState.CopyTo(state);
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void getPreviousState(DS4State state)
|
|
|
|
|
{
|
2014-04-27 21:32:09 +02:00
|
|
|
|
pState.CopyTo(state);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool isNonSixaxisIdle()
|
|
|
|
|
{
|
|
|
|
|
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;
|
2014-03-29 06:29:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
{
|
|
|
|
|
int i = 0;
|
|
|
|
|
DS4Color lightBarColor = LightBarColor;
|
|
|
|
|
byte lightBarFlashDurationOn = LightBarOnDuration, lightBarFlashDurationOff = LightBarOffDuration;
|
|
|
|
|
byte rumbleMotorStrengthLeftHeavySlow = LeftHeavySlowRumble, rumbleMotorStrengthRightLightFast = rightLightFastRumble;
|
|
|
|
|
foreach (DS4HapticState haptic in hapticState)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
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;
|
|
|
|
|
}
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
2014-03-29 06:29:08 +01:00
|
|
|
|
LightBarColor = lightBarColor;
|
|
|
|
|
LightBarOnDuration = lightBarFlashDurationOn;
|
|
|
|
|
LightBarOffDuration = lightBarFlashDurationOff;
|
|
|
|
|
LeftHeavySlowRumble = rumbleMotorStrengthLeftHeavySlow;
|
|
|
|
|
RightLightFastRumble = rumbleMotorStrengthRightLightFast;
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2014-03-29 06:29:08 +01:00
|
|
|
|
public void pushHapticState(DS4HapticState hs)
|
2014-03-28 02:50:40 +01:00
|
|
|
|
{
|
2014-03-29 06:29:08 +01:00
|
|
|
|
if (hapticStackIndex == hapticState.Length)
|
|
|
|
|
{
|
|
|
|
|
DS4HapticState[] newHaptics = new DS4HapticState[hapticState.Length + 1];
|
|
|
|
|
Array.Copy(hapticState, newHaptics, hapticState.Length);
|
|
|
|
|
hapticState = newHaptics;
|
|
|
|
|
}
|
|
|
|
|
hapticState[hapticStackIndex++] = hs;
|
2014-03-28 02:50:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override
|
|
|
|
|
public String ToString()
|
|
|
|
|
{
|
|
|
|
|
return Mac;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|