2014-03-28 02:50:40 +01:00
using System ;
using System.Runtime.InteropServices ;
using System.Threading ;
using System.IO ;
using System.Threading.Tasks ;
using Microsoft.Win32.SafeHandles ;
2015-02-08 22:51:52 +01:00
namespace DS4Windows
2014-03-28 02:50:40 +01:00
{
public class HidDevice : IDisposable
{
public enum ReadStatus
{
Success = 0 ,
WaitTimedOut = 1 ,
WaitFail = 2 ,
NoDataRead = 3 ,
ReadError = 4 ,
NotConnected = 5
}
private readonly string _description ;
private readonly string _devicePath ;
private readonly HidDeviceAttributes _deviceAttributes ;
private readonly HidDeviceCapabilities _deviceCapabilities ;
private bool _monitorDeviceEvents ;
private string serial = null ;
2019-10-26 01:17:10 +02:00
private const string BLANK_SERIAL = "00:00:00:00:00:00" ;
2014-03-28 02:50:40 +01:00
internal HidDevice ( string devicePath , string description = null )
{
_devicePath = devicePath ;
_description = description ;
try
{
2020-04-08 10:04:49 +02:00
var hidHandle = OpenHandle ( _devicePath , false , enumerate : true ) ;
2014-03-28 02:50:40 +01:00
_deviceAttributes = GetDeviceAttributes ( hidHandle ) ;
_deviceCapabilities = GetDeviceCapabilities ( hidHandle ) ;
hidHandle . Close ( ) ;
}
catch ( Exception exception )
{
Console . WriteLine ( exception . Message ) ;
throw new Exception ( string . Format ( "Error querying HID device '{0}'." , devicePath ) , exception ) ;
}
}
public SafeFileHandle safeReadHandle { get ; private set ; }
public FileStream fileStream { get ; private set ; }
public bool IsOpen { get ; private set ; }
2014-03-29 06:29:08 +01:00
public bool IsExclusive { get ; private set ; }
2014-03-28 02:50:40 +01:00
public bool IsConnected { get { return HidDevices . IsConnected ( _devicePath ) ; } }
public string Description { get { return _description ; } }
public HidDeviceCapabilities Capabilities { get { return _deviceCapabilities ; } }
public HidDeviceAttributes Attributes { get { return _deviceAttributes ; } }
public string DevicePath { get { return _devicePath ; } }
public override string ToString ( )
{
return string . Format ( "VendorID={0}, ProductID={1}, Version={2}, DevicePath={3}" ,
_deviceAttributes . VendorHexId ,
_deviceAttributes . ProductHexId ,
_deviceAttributes . Version ,
_devicePath ) ;
}
public void OpenDevice ( bool isExclusive )
{
if ( IsOpen ) return ;
try
{
2014-03-29 06:29:08 +01:00
if ( safeReadHandle = = null | | safeReadHandle . IsInvalid )
2020-04-08 10:04:49 +02:00
safeReadHandle = OpenHandle ( _devicePath , isExclusive , enumerate : false ) ;
2014-03-28 02:50:40 +01:00
}
catch ( Exception exception )
{
IsOpen = false ;
throw new Exception ( "Error opening HID device." , exception ) ;
}
IsOpen = ! safeReadHandle . IsInvalid ;
2014-03-29 06:29:08 +01:00
IsExclusive = isExclusive ;
2014-03-28 02:50:40 +01:00
}
2017-05-04 11:02:38 +02:00
public void OpenFileStream ( int reportSize )
{
if ( fileStream = = null & & ! safeReadHandle . IsInvalid )
{
fileStream = new FileStream ( safeReadHandle , FileAccess . ReadWrite , reportSize , true ) ;
}
}
public bool IsFileStreamOpen ( )
{
bool result = false ;
if ( fileStream ! = null )
{
result = ! fileStream . SafeFileHandle . IsInvalid & & ! fileStream . SafeFileHandle . IsClosed ;
}
return result ;
}
2014-03-28 02:50:40 +01:00
public void CloseDevice ( )
{
if ( ! IsOpen ) return ;
closeFileStreamIO ( ) ;
IsOpen = false ;
}
2014-04-27 21:32:09 +02:00
public void Dispose ( )
{
CancelIO ( ) ;
CloseDevice ( ) ;
}
public void CancelIO ( )
{
if ( IsOpen )
NativeMethods . CancelIoEx ( safeReadHandle . DangerousGetHandle ( ) , IntPtr . Zero ) ;
}
2014-03-28 02:50:40 +01:00
public bool ReadInputReport ( byte [ ] data )
{
if ( safeReadHandle = = null )
2020-04-08 10:04:49 +02:00
safeReadHandle = OpenHandle ( _devicePath , true , enumerate : false ) ;
2014-03-28 02:50:40 +01:00
return NativeMethods . HidD_GetInputReport ( safeReadHandle , data , data . Length ) ;
}
2017-04-06 03:37:38 +02:00
public bool WriteFeatureReport ( byte [ ] data )
{
bool result = false ;
if ( IsOpen & & safeReadHandle ! = null )
{
result = NativeMethods . HidD_SetFeature ( safeReadHandle , data , data . Length ) ;
}
return result ;
}
2014-03-28 02:50:40 +01:00
private static HidDeviceAttributes GetDeviceAttributes ( SafeFileHandle hidHandle )
{
var deviceAttributes = default ( NativeMethods . HIDD_ATTRIBUTES ) ;
deviceAttributes . Size = Marshal . SizeOf ( deviceAttributes ) ;
NativeMethods . HidD_GetAttributes ( hidHandle . DangerousGetHandle ( ) , ref deviceAttributes ) ;
return new HidDeviceAttributes ( deviceAttributes ) ;
}
private static HidDeviceCapabilities GetDeviceCapabilities ( SafeFileHandle hidHandle )
{
var capabilities = default ( NativeMethods . HIDP_CAPS ) ;
var preparsedDataPointer = default ( IntPtr ) ;
if ( NativeMethods . HidD_GetPreparsedData ( hidHandle . DangerousGetHandle ( ) , ref preparsedDataPointer ) )
{
NativeMethods . HidP_GetCaps ( preparsedDataPointer , ref capabilities ) ;
NativeMethods . HidD_FreePreparsedData ( preparsedDataPointer ) ;
}
2017-05-04 11:02:38 +02:00
2014-03-28 02:50:40 +01:00
return new HidDeviceCapabilities ( capabilities ) ;
}
private void closeFileStreamIO ( )
{
if ( fileStream ! = null )
2017-03-25 16:49:03 +01:00
{
try
{
fileStream . Close ( ) ;
}
2017-05-04 11:02:38 +02:00
catch ( IOException ) { }
catch ( OperationCanceledException ) { }
2017-03-25 16:49:03 +01:00
}
2014-03-28 02:50:40 +01:00
fileStream = null ;
Console . WriteLine ( "Close fs" ) ;
if ( safeReadHandle ! = null & & ! safeReadHandle . IsInvalid )
{
2017-05-04 11:02:38 +02:00
try
{
if ( ! safeReadHandle . IsClosed )
{
safeReadHandle . Close ( ) ;
Console . WriteLine ( "Close sh" ) ;
}
}
catch ( IOException ) { }
2014-03-28 02:50:40 +01:00
}
2017-05-04 11:02:38 +02:00
2014-03-28 02:50:40 +01:00
safeReadHandle = null ;
}
public void flush_Queue ( )
{
if ( safeReadHandle ! = null )
{
NativeMethods . HidD_FlushQueue ( safeReadHandle ) ;
}
}
private ReadStatus ReadWithFileStreamTask ( byte [ ] inputBuffer )
{
try
{
if ( fileStream . Read ( inputBuffer , 0 , inputBuffer . Length ) > 0 )
{
return ReadStatus . Success ;
}
else
{
return ReadStatus . NoDataRead ;
}
}
catch ( Exception )
{
return ReadStatus . ReadError ;
}
}
2017-05-04 11:02:38 +02:00
2014-03-28 02:50:40 +01:00
public ReadStatus ReadFile ( byte [ ] inputBuffer )
{
if ( safeReadHandle = = null )
2020-04-08 10:04:49 +02:00
safeReadHandle = OpenHandle ( _devicePath , true , enumerate : false ) ;
2014-03-28 02:50:40 +01:00
try
{
uint bytesRead ;
if ( NativeMethods . ReadFile ( safeReadHandle . DangerousGetHandle ( ) , inputBuffer , ( uint ) inputBuffer . Length , out bytesRead , IntPtr . Zero ) )
{
return ReadStatus . Success ;
}
else
{
return ReadStatus . NoDataRead ;
}
}
catch ( Exception )
{
return ReadStatus . ReadError ;
}
}
2017-05-14 00:01:43 +02:00
public ReadStatus ReadWithFileStream ( byte [ ] inputBuffer )
{
try
{
if ( fileStream . Read ( inputBuffer , 0 , inputBuffer . Length ) > 0 )
{
return ReadStatus . Success ;
}
else
{
return ReadStatus . NoDataRead ;
}
}
catch ( Exception )
{
return ReadStatus . ReadError ;
}
}
2014-03-28 02:50:40 +01:00
public ReadStatus ReadWithFileStream ( byte [ ] inputBuffer , int timeout )
{
try
{
if ( safeReadHandle = = null )
2020-04-08 10:04:49 +02:00
safeReadHandle = OpenHandle ( _devicePath , true , enumerate : false ) ;
2014-03-28 02:50:40 +01:00
if ( fileStream = = null & & ! safeReadHandle . IsInvalid )
2017-03-22 08:52:54 +01:00
fileStream = new FileStream ( safeReadHandle , FileAccess . ReadWrite , inputBuffer . Length , true ) ;
2017-05-04 11:02:38 +02:00
2014-03-28 02:50:40 +01:00
if ( ! safeReadHandle . IsInvalid & & fileStream . CanRead )
{
Task < ReadStatus > readFileTask = new Task < ReadStatus > ( ( ) = > ReadWithFileStreamTask ( inputBuffer ) ) ;
readFileTask . Start ( ) ;
bool success = readFileTask . Wait ( timeout ) ;
if ( success )
{
if ( readFileTask . Result = = ReadStatus . Success )
{
return ReadStatus . Success ;
}
else if ( readFileTask . Result = = ReadStatus . ReadError )
{
return ReadStatus . ReadError ;
}
else if ( readFileTask . Result = = ReadStatus . NoDataRead )
{
return ReadStatus . NoDataRead ;
}
}
else
return ReadStatus . WaitTimedOut ;
}
}
catch ( Exception e )
{
if ( e is AggregateException )
{
Console . WriteLine ( e . Message ) ;
return ReadStatus . WaitFail ;
}
else
{
return ReadStatus . ReadError ;
}
}
2017-03-22 08:52:54 +01:00
return ReadStatus . ReadError ;
}
public ReadStatus ReadAsyncWithFileStream ( byte [ ] inputBuffer , int timeout )
{
try
{
if ( safeReadHandle = = null )
2020-04-08 10:04:49 +02:00
safeReadHandle = OpenHandle ( _devicePath , true , enumerate : false ) ;
2017-03-22 08:52:54 +01:00
if ( fileStream = = null & & ! safeReadHandle . IsInvalid )
fileStream = new FileStream ( safeReadHandle , FileAccess . ReadWrite , inputBuffer . Length , true ) ;
2017-05-11 17:13:51 +02:00
2017-03-22 08:52:54 +01:00
if ( ! safeReadHandle . IsInvalid & & fileStream . CanRead )
{
Task < int > readTask = fileStream . ReadAsync ( inputBuffer , 0 , inputBuffer . Length ) ;
2017-05-12 16:48:58 +02:00
bool success = readTask . Wait ( timeout ) ;
if ( success )
2017-03-22 08:52:54 +01:00
{
2017-05-12 16:48:58 +02:00
if ( readTask . Result > 0 )
{
return ReadStatus . Success ;
}
else
{
return ReadStatus . NoDataRead ;
}
2017-03-22 08:52:54 +01:00
}
else
{
2017-05-12 16:48:58 +02:00
return ReadStatus . WaitTimedOut ;
2017-03-22 08:52:54 +01:00
}
}
}
catch ( Exception e )
{
if ( e is AggregateException )
{
Console . WriteLine ( e . Message ) ;
return ReadStatus . WaitFail ;
}
else
{
return ReadStatus . ReadError ;
}
}
2014-03-28 02:50:40 +01:00
return ReadStatus . ReadError ;
}
public bool WriteOutputReportViaControl ( byte [ ] outputBuffer )
{
if ( safeReadHandle = = null )
{
2020-04-08 10:04:49 +02:00
safeReadHandle = OpenHandle ( _devicePath , true , enumerate : false ) ;
2014-03-28 02:50:40 +01:00
}
if ( NativeMethods . HidD_SetOutputReport ( safeReadHandle , outputBuffer , outputBuffer . Length ) )
return true ;
else
return false ;
}
private bool WriteOutputReportViaInterruptTask ( byte [ ] outputBuffer )
{
try
{
fileStream . Write ( outputBuffer , 0 , outputBuffer . Length ) ;
return true ;
}
catch ( Exception e )
{
Console . WriteLine ( e . Message ) ;
return false ;
}
}
2014-03-29 06:29:08 +01:00
public bool WriteOutputReportViaInterrupt ( byte [ ] outputBuffer , int timeout )
2014-03-28 02:50:40 +01:00
{
try
{
if ( safeReadHandle = = null )
{
2020-04-08 10:04:49 +02:00
safeReadHandle = OpenHandle ( _devicePath , true , enumerate : false ) ;
2014-03-28 02:50:40 +01:00
}
if ( fileStream = = null & & ! safeReadHandle . IsInvalid )
{
2017-03-22 08:52:54 +01:00
fileStream = new FileStream ( safeReadHandle , FileAccess . ReadWrite , outputBuffer . Length , true ) ;
2014-03-28 02:50:40 +01:00
}
if ( fileStream ! = null & & fileStream . CanWrite & & ! safeReadHandle . IsInvalid )
{
fileStream . Write ( outputBuffer , 0 , outputBuffer . Length ) ;
return true ;
}
else
{
return false ;
}
}
catch ( Exception )
{
return false ;
}
}
2017-03-22 08:52:54 +01:00
public bool WriteAsyncOutputReportViaInterrupt ( byte [ ] outputBuffer )
{
try
{
if ( safeReadHandle = = null )
{
2020-04-08 10:04:49 +02:00
safeReadHandle = OpenHandle ( _devicePath , true , enumerate : false ) ;
2017-03-22 08:52:54 +01:00
}
if ( fileStream = = null & & ! safeReadHandle . IsInvalid )
{
fileStream = new FileStream ( safeReadHandle , FileAccess . ReadWrite , outputBuffer . Length , true ) ;
}
if ( fileStream ! = null & & fileStream . CanWrite & & ! safeReadHandle . IsInvalid )
{
Task writeTask = fileStream . WriteAsync ( outputBuffer , 0 , outputBuffer . Length ) ;
//fileStream.Write(outputBuffer, 0, outputBuffer.Length);
return true ;
}
else
{
return false ;
}
}
catch ( Exception )
{
return false ;
}
}
2020-04-08 10:04:49 +02:00
private SafeFileHandle OpenHandle ( String devicePathName , Boolean isExclusive , bool enumerate )
2014-03-28 02:50:40 +01:00
{
SafeFileHandle hidHandle ;
2020-04-08 10:04:49 +02:00
uint access = enumerate ? 0 : NativeMethods . GENERIC_READ | NativeMethods . GENERIC_WRITE ;
2014-03-28 02:50:40 +01:00
2018-01-12 05:31:49 +01:00
if ( isExclusive )
2014-03-28 02:50:40 +01:00
{
2020-04-08 10:04:49 +02:00
hidHandle = NativeMethods . CreateFile ( devicePathName , access , 0 , IntPtr . Zero , NativeMethods . OpenExisting , 0x20000000 | 0x80000000 | 0x100 | NativeMethods . FILE_FLAG_OVERLAPPED , 0 ) ;
2014-03-28 02:50:40 +01:00
}
2018-01-12 05:31:49 +01:00
else
2014-03-28 02:50:40 +01:00
{
2020-04-08 10:04:49 +02:00
hidHandle = NativeMethods . CreateFile ( devicePathName , access , NativeMethods . FILE_SHARE_READ | NativeMethods . FILE_SHARE_WRITE , IntPtr . Zero , NativeMethods . OpenExisting , 0x20000000 | 0x80000000 | 0x100 | NativeMethods . FILE_FLAG_OVERLAPPED , 0 ) ;
2014-03-28 02:50:40 +01:00
}
2018-01-12 05:31:49 +01:00
2014-03-28 02:50:40 +01:00
return hidHandle ;
}
public bool readFeatureData ( byte [ ] inputBuffer )
{
return NativeMethods . HidD_GetFeature ( safeReadHandle . DangerousGetHandle ( ) , inputBuffer , inputBuffer . Length ) ;
}
2017-05-25 11:51:28 +02:00
public void resetSerial ( )
{
serial = null ;
}
2014-03-28 02:50:40 +01:00
public string readSerial ( )
{
if ( serial ! = null )
return serial ;
2019-10-20 16:25:18 +02:00
// Some devices don't have MAC address (especially gamepads with USB only suports in PC). If the serial number reading fails
// then use dummy zero MAC address, because there is a good chance the gamepad stll works in DS4Windows app (the code would throw
// an index out of bounds exception anyway without IF-THEN-ELSE checks after trying to read a serial number).
2014-03-28 02:50:40 +01:00
if ( Capabilities . InputReportByteLength = = 64 )
{
byte [ ] buffer = new byte [ 16 ] ;
buffer [ 0 ] = 18 ;
2019-10-20 16:25:18 +02:00
if ( readFeatureData ( buffer ) )
2019-10-26 01:17:10 +02:00
serial = String . Format ( "{0:X02}:{1:X02}:{2:X02}:{3:X02}:{4:X02}:{5:X02}" ,
buffer [ 6 ] , buffer [ 5 ] , buffer [ 4 ] , buffer [ 3 ] , buffer [ 2 ] , buffer [ 1 ] ) ;
2014-03-28 02:50:40 +01:00
}
else
{
byte [ ] buffer = new byte [ 126 ] ;
2017-11-17 08:50:18 +01:00
#if WIN64
2017-11-17 08:33:24 +01:00
ulong bufferLen = 126 ;
#else
uint bufferLen = 126 ;
#endif
2019-10-20 16:25:18 +02:00
if ( NativeMethods . HidD_GetSerialNumberString ( safeReadHandle . DangerousGetHandle ( ) , buffer , bufferLen ) )
{
string MACAddr = System . Text . Encoding . Unicode . GetString ( buffer ) . Replace ( "\0" , string . Empty ) . ToUpper ( ) ;
MACAddr = $"{MACAddr[0]}{MACAddr[1]}:{MACAddr[2]}{MACAddr[3]}:{MACAddr[4]}{MACAddr[5]}:{MACAddr[6]}{MACAddr[7]}:{MACAddr[8]}{MACAddr[9]}:{MACAddr[10]}{MACAddr[11]}" ;
serial = MACAddr ;
}
2019-11-02 20:01:54 +01:00
}
2019-10-20 16:25:18 +02:00
2019-11-02 20:01:54 +01:00
// If serial# reading failed then generate a dummy MAC address based on HID device path (WinOS generated runtime unique value based on connected usb port and hub or BT channel).
// The device path remains the same as long the gamepad is always connected to the same usb/BT port, but may be different in other usb ports. Therefore this value is unique
// as long the same device is always connected to the same usb port.
if ( serial = = null )
{
string MACAddr = string . Empty ;
AppLogger . LogToGui ( $"WARNING: Failed to read serial# from a gamepad ({this._deviceAttributes.VendorHexId}/{this._deviceAttributes.ProductHexId}). Generating MAC address from a device path. From now on you should connect this gamepad always into the same USB port or BT pairing host to keep the same device path." , true ) ;
try
{
// Substring: \\?\hid#vid_054c&pid_09cc&mi_03#7&1f882A25&0&0001#{4d1e55b2-f16f-11cf-88cb-001111000030} -> \\?\hid#vid_054c&pid_09cc&mi_03#7&1f882A25&0&0001#
int endPos = this . DevicePath . LastIndexOf ( '{' ) ;
if ( endPos < 0 )
endPos = this . DevicePath . Length ;
// String array: \\?\hid#vid_054c&pid_09cc&mi_03#7&1f882A25&0&0001# -> [0]=\\?\hidvid_054c, [1]=pid_09cc, [2]=mi_037, [3]=1f882A25, [4]=0, [5]=0001
string [ ] devPathItems = this . DevicePath . Substring ( 0 , endPos ) . Replace ( "#" , "" ) . Replace ( "-" , "" ) . Replace ( "{" , "" ) . Replace ( "}" , "" ) . Split ( '&' ) ;
if ( devPathItems . Length > = 3 )
MACAddr = devPathItems [ devPathItems . Length - 3 ] . ToUpper ( ) // 1f882A25
+ devPathItems [ devPathItems . Length - 2 ] . ToUpper ( ) // 0
+ devPathItems [ devPathItems . Length - 1 ] . TrimStart ( '0' ) . ToUpper ( ) ; // 0001 -> 1
else if ( devPathItems . Length > = 1 )
// Device and usb hub and port identifiers missing in devicePath string. Fallback to use vendor and product ID values and
// take a number from the last part of the devicePath. Hopefully the last part is a usb port number as it usually should be.
MACAddr = this . _deviceAttributes . VendorId . ToString ( "X4" )
+ this . _deviceAttributes . ProductId . ToString ( "X4" )
+ devPathItems [ devPathItems . Length - 1 ] . TrimStart ( '0' ) . ToUpper ( ) ;
if ( ! string . IsNullOrEmpty ( MACAddr ) )
{
MACAddr = MACAddr . PadRight ( 12 , '0' ) ;
serial = $"{MACAddr[0]}{MACAddr[1]}:{MACAddr[2]}{MACAddr[3]}:{MACAddr[4]}{MACAddr[5]}:{MACAddr[6]}{MACAddr[7]}:{MACAddr[8]}{MACAddr[9]}:{MACAddr[10]}{MACAddr[11]}" ;
}
else
// Hmm... Shold never come here. Strange format in devicePath because all identifier items of devicePath string are missing.
serial = BLANK_SERIAL ;
}
catch ( Exception e )
{
AppLogger . LogToGui ( $"ERROR: Failed to generate runtime MAC address from device path {this.DevicePath}. {e.Message}" , true ) ;
serial = BLANK_SERIAL ;
}
2014-03-28 02:50:40 +01:00
}
2019-11-02 20:01:54 +01:00
return serial ;
2014-03-28 02:50:40 +01:00
}
}
}