1 /* 2 DualSenseWindows API 3 https://github.com/Ohjurot/DualSense-Windows 4 5 MIT License 6 7 Copyright (c) 2020 Ludwig Füchsl 8 9 Permission is hereby granted, free of charge, to any person obtaining a copy 10 of this software and associated documentation files (the "Software"), to deal 11 in the Software without restriction, including without limitation the rights 12 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 copies of the Software, and to permit persons to whom the Software is 14 furnished to do so, subject to the following conditions: 15 16 The above copyright notice and this permission notice shall be included in all 17 copies or substantial portions of the Software. 18 19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 SOFTWARE. 26 27 */ 28 module ds5w.io; 29 30 import ds5w.dswapi; 31 import ds5w.device; 32 import ds5w.ds5state; 33 import ds5w.dscrc32; 34 import ds5w.winhelper; 35 import ds5w.ds5input; 36 import ds5w.ds5output; 37 38 import core.stdc.stddef : wchar_t; 39 import core.stdc.stdlib : alloca; 40 41 import core.sys.windows.windows; 42 import core.sys.windows.setupapi; 43 44 /// <summary> 45 /// Enumerate all ds5 deviced connected to the computer 46 /// </summary> 47 /// <param name="devInfoArr">A slice of DeviceEnumInfo objects / DeviceEnumInfo pointers</param> 48 /// <param name="requiredLength">pointer to uint witch recives the required total length</param> 49 /// <returns>DS5W Return value</returns> 50 DS5W_ReturnValue enumDevices(DeviceEnumInfo[] devInfoArr, size_t* requiredLength) 51 { 52 size_t inArrLength = devInfoArr.length; 53 // Check for invalid non expected buffer 54 if (inArrLength && !devInfoArr) 55 { 56 inArrLength = 0; 57 } 58 59 // Get all hid devices from devs 60 HANDLE hidDiHandle = SetupDiGetClassDevs(&GUID_DEVINTERFACE_HID, null, null, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT); 61 if (!hidDiHandle || (hidDiHandle == INVALID_HANDLE_VALUE)) 62 { 63 return DS5W_E_EXTERNAL_WINAPI; 64 } 65 66 // Index into input array 67 uint inputArrIndex = 0; 68 bool inputArrOverflow = false; 69 70 // Enumerate over hid device 71 DWORD devIndex = 0; 72 SP_DEVINFO_DATA hidDiInfo; 73 hidDiInfo.cbSize = SP_DEVINFO_DATA.sizeof; 74 75 while (SetupDiEnumDeviceInfo(hidDiHandle, devIndex, &hidDiInfo)) 76 { 77 // Enumerate over all hid device interfaces 78 DWORD ifIndex = 0; 79 SP_DEVICE_INTERFACE_DATA ifDiInfo; 80 ifDiInfo.cbSize = SP_DEVICE_INTERFACE_DATA.sizeof; 81 82 while (SetupDiEnumDeviceInterfaces(hidDiHandle, &hidDiInfo, &GUID_DEVINTERFACE_HID, ifIndex, &ifDiInfo)) 83 { 84 85 // Query device path size 86 DWORD requiredSize = 0; 87 SetupDiGetDeviceInterfaceDetailW(hidDiHandle, &ifDiInfo, null, 0, &requiredSize, null); 88 89 // Check size 90 if (requiredSize > (260 * wchar_t.sizeof)) 91 { 92 SetupDiDestroyDeviceInfoList(hidDiHandle); 93 return DS5W_E_EXTERNAL_WINAPI; 94 } 95 96 // Allocate memory for path on the stack 97 SP_DEVICE_INTERFACE_DETAIL_DATA_W* devicePath = cast( 98 SP_DEVICE_INTERFACE_DETAIL_DATA_W*) alloca(requiredSize); 99 100 if (!devicePath) 101 { 102 SetupDiDestroyDeviceInfoList(hidDiHandle); 103 return DS5W_E_STACK_OVERFLOW; 104 } 105 106 // Get device path 107 devicePath.cbSize = SP_DEVICE_INTERFACE_DETAIL_DATA_W.sizeof; 108 SetupDiGetDeviceInterfaceDetailW(hidDiHandle, &ifDiInfo, devicePath, requiredSize, NULL, NULL); 109 110 // Check if input array has space 111 // Check if device is reachable 112 HANDLE deviceHandle = CreateFileW(devicePath.DevicePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); 113 114 // Check if device is reachable 115 if (deviceHandle && (deviceHandle != INVALID_HANDLE_VALUE)) 116 { 117 118 // Get vendor and product id 119 uint vendorId = 0; 120 uint productId = 0; 121 HIDD_ATTRIBUTES deviceAttributes; 122 if (HidD_GetAttributes(deviceHandle, &deviceAttributes)) 123 { 124 vendorId = deviceAttributes.VendorID; 125 productId = deviceAttributes.ProductID; 126 } 127 128 // Check if ids match 129 if (vendorId == PSVENDOR && 130 (productId == DEVICE_VARIANT.DS_REGULAR || productId == DEVICE_VARIANT.DS_EDGE)) 131 { 132 debug 133 { 134 import std.stdio; 135 136 writeln("vendorId ", vendorId, " productId ", productId); 137 } 138 // Get pointer to target 139 DeviceEnumInfo* ptrInfo = null; 140 if (inputArrIndex < inArrLength) 141 { 142 ptrInfo = &devInfoArr[inputArrIndex]; 143 } 144 145 // Copy path 146 if (ptrInfo) 147 { 148 wcscpy_s(ptrInfo._internal.path.ptr, 260, cast( 149 const(wchar_t)*) devicePath.DevicePath); 150 } 151 152 // Get preparsed data 153 PHIDP_PREPARSED_DATA ppd; 154 if (HidD_GetPreparsedData(deviceHandle, &ppd)) 155 { 156 157 // Get device capcbilitys 158 HIDP_CAPS deviceCaps; 159 160 if (HidP_GetCaps(ppd, &deviceCaps) == HIDP_STATUS_SUCCESS) 161 { 162 debug 163 { 164 import core.stdc.stdio : printf; 165 import std.stdio : writeln; 166 167 writeln(deviceCaps); 168 printf("%ls\n", ptrInfo._internal.path.ptr); 169 } 170 // Check for device connection type 171 if (ptrInfo) 172 { 173 // Check if controller matches USB specifications 174 if (deviceCaps.InputReportByteLength == 64) 175 { 176 ptrInfo._internal.connection = DeviceConnection.USB; 177 178 // Device found and valid -> Inrement index 179 inputArrIndex++; 180 } 181 // Check if controler matches BT specifications 182 else if ( 183 deviceCaps.InputReportByteLength == 78) 184 { 185 ptrInfo._internal.connection = DeviceConnection.BT; 186 187 // Device found and valid -> Inrement index 188 inputArrIndex++; 189 } 190 191 ptrInfo.variant = cast(DEVICE_VARIANT) productId; 192 } 193 } 194 195 // Free preparsed data 196 HidD_FreePreparsedData(ppd); 197 } 198 } 199 200 // Close device 201 CloseHandle(deviceHandle); 202 } 203 204 // Increment index 205 ifIndex++; 206 } 207 208 // Increment index 209 devIndex++; 210 } 211 212 // Close device enum list 213 SetupDiDestroyDeviceInfoList(hidDiHandle); 214 215 // Set required size if exists 216 if (requiredLength) 217 { 218 *requiredLength = inputArrIndex; 219 } 220 221 // Check if array was suficient 222 if (inputArrIndex <= inArrLength) 223 { 224 return DS5W_OK; 225 } 226 // Else return error 227 else 228 { 229 return DS5W_E_INSUFFICIENT_BUFFER; 230 } 231 } 232 233 /// <summary> 234 /// Initializes a DeviceContext from its enum infos 235 /// </summary> 236 /// <param name="ptrEnumInfo">Pointer to enum object to create device from</param> 237 /// <param name="ptrContext">Pointer to context to create to</param> 238 /// <returns>If creation was successfull</returns> 239 DS5W_ReturnValue initDeviceContext(DeviceEnumInfo* ptrEnumInfo, DeviceContext* ptrContext) 240 { 241 // Check if pointers are valid 242 if (!ptrEnumInfo || !ptrContext) 243 { 244 return DS5W_E_INVALID_ARGS; 245 } 246 247 // Check len 248 if (wcslen(ptrEnumInfo._internal.path.ptr) == 0) 249 { 250 return DS5W_E_INVALID_ARGS; 251 } 252 253 // Connect to device 254 HANDLE deviceHandle = CreateFileW(ptrEnumInfo._internal.path.ptr, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, null, OPEN_EXISTING, 0, null); 255 if (!deviceHandle || (deviceHandle == INVALID_HANDLE_VALUE)) 256 { 257 return DS5W_E_DEVICE_REMOVED; 258 } 259 260 // Write to conext 261 ptrContext._internal.connected = true; 262 ptrContext._internal.connection = ptrEnumInfo._internal.connection; 263 ptrContext._internal.deviceHandle = deviceHandle; 264 //wcscpy_s(ptrContext._internal.devicePath.ptr, 260, ptrEnumInfo._internal.path.ptr); 265 ptrContext._internal.devicePath[] = ptrEnumInfo._internal.path[]; 266 ptrContext.variant = ptrEnumInfo.variant; // Regular or Edge 267 // Get input report length 268 ushort reportLength = 0; 269 if (ptrContext._internal.connection == DeviceConnection.BT) 270 { 271 // Start BT by reading feature report 5 272 ubyte[64] fBuffer; 273 fBuffer[0] = 0x05; 274 if (!HidD_GetFeature(deviceHandle, fBuffer.ptr, 64)) 275 { 276 return DS5W_E_BT_COM; 277 } 278 279 // The bluetooth input report is 78 Bytes long 280 reportLength = 547; 281 } 282 else 283 { 284 // The usb input report is 64 Bytes long 285 reportLength = 64; 286 } 287 288 // Return OK 289 return DS5W_OK; 290 } 291 292 /// <summary> 293 /// Free the device conntext 294 /// </summary> 295 /// <param name="ptrContext">Pointer to context</param> 296 void freeDeviceContext(DeviceContext* ptrContext) 297 { 298 // Check if handle is existing 299 if (ptrContext._internal.deviceHandle) 300 { 301 // Send zero output report to disable all onging outputs 302 DS5OutputState os; 303 304 os.leftTriggerEffect.effectType = TriggerEffectType.NoResitance; 305 os.rightTriggerEffect.effectType = TriggerEffectType.NoResitance; 306 os.disableLeds = true; 307 308 setDeviceOutputState(ptrContext, &os); 309 310 // Close handle 311 CloseHandle(ptrContext._internal.deviceHandle); 312 ptrContext._internal.deviceHandle = null; 313 } 314 315 // Unset bool 316 ptrContext._internal.connected = false; 317 318 // Unset string 319 ptrContext._internal.devicePath[0] = 0x0; 320 } 321 322 /// <summary> 323 /// Try to reconnect a removed device 324 /// </summary> 325 /// <param name="ptrContext">Context to reconnect on</param> 326 /// <returns>Result</returns> 327 DS5W_ReturnValue reconnectDevice(DeviceContext* ptrContext) 328 { 329 // Check len 330 if (wcslen(ptrContext._internal.devicePath.ptr) == 0) 331 { 332 return DS5W_E_INVALID_ARGS; 333 } 334 335 // Connect to device 336 HANDLE deviceHandle = CreateFileW(ptrContext._internal.devicePath.ptr, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, null, OPEN_EXISTING, 0, null); 337 if (!deviceHandle || (deviceHandle == INVALID_HANDLE_VALUE)) 338 { 339 return DS5W_E_DEVICE_REMOVED; 340 } 341 342 // Write to conext 343 ptrContext._internal.connected = true; 344 ptrContext._internal.deviceHandle = deviceHandle; 345 346 // Return ok 347 return DS5W_OK; 348 } 349 350 /// <summary> 351 /// Get device input state 352 /// </summary> 353 /// <param name="ptrContext">Pointer to context</param> 354 /// <param name="ptrInputState">Pointer to input state</param> 355 /// <returns>Result of call</returns> 356 DS5W_ReturnValue getDeviceInputState(DeviceContext* ptrContext, DS5InputState* ptrInputState) 357 { 358 // Check pointer 359 if (!ptrContext || !ptrInputState) 360 { 361 return DS5W_E_INVALID_ARGS; 362 } 363 364 // Check for connection 365 if (!ptrContext._internal.connected) 366 { 367 return DS5W_E_DEVICE_REMOVED; 368 } 369 370 // Get the most recent package 371 HidD_FlushQueue(ptrContext._internal.deviceHandle); 372 373 // Get input report length 374 uint inputReportLength = 0; 375 if (ptrContext._internal.connection == DeviceConnection.BT) 376 { 377 // The bluetooth input report is 78 Bytes long 378 inputReportLength = 78; 379 ptrContext._internal.hidBuffer[0] = 0x31; 380 } 381 else 382 { 383 // The usb input report is 64 Bytes long 384 inputReportLength = 64; 385 ptrContext._internal.hidBuffer[0] = 0x01; 386 } 387 388 // Get device input 389 if (ReadFile(ptrContext._internal.deviceHandle, ptrContext._internal.hidBuffer.ptr, inputReportLength, NULL, NULL) == FALSE) 390 { 391 // Close handle and set error state 392 CloseHandle(ptrContext._internal.deviceHandle); 393 ptrContext._internal.deviceHandle = null; 394 ptrContext._internal.connected = false; 395 396 // Return error 397 return DS5W_E_DEVICE_REMOVED; 398 } 399 400 // Evaluete input buffer 401 if (ptrContext._internal.connection == DeviceConnection.BT) 402 { 403 // Call bluetooth evaluator if connection is qual to BT 404 evaluateHidInputBuffer(&ptrContext._internal.hidBuffer[2], ptrInputState); 405 } 406 else 407 { 408 // Else it is USB so call its evaluator 409 evaluateHidInputBuffer(&ptrContext._internal.hidBuffer[1], ptrInputState); 410 } 411 412 // Return ok 413 return DS5W_OK; 414 } 415 416 /// <summary> 417 /// Set the device output state 418 /// </summary> 419 /// <param name="ptrContext">Pointer to context</param> 420 /// <param name="ptrOutputState">Pointer to output state to be set</param> 421 /// <returns>Result of call</returns> 422 DS5W_ReturnValue setDeviceOutputState(DeviceContext* ptrContext, DS5OutputState* ptrOutputState) 423 { 424 // Check pointer 425 if (!ptrContext || !ptrOutputState) 426 { 427 return DS5W_E_INVALID_ARGS; 428 } 429 430 // Check for connection 431 if (!ptrContext._internal.connected) 432 { 433 return DS5W_E_DEVICE_REMOVED; 434 } 435 436 // Get otuput report length 437 ushort outputReportLength = 0; 438 if (ptrContext._internal.connection == DeviceConnection.BT) 439 { 440 // The bluetooth input report is 547 Bytes long 441 outputReportLength = 547; 442 } 443 else 444 { 445 // The usb input report is 48 or 64 Bytes long for DS_REGULAR and DS_EDGE 446 if (ptrContext.variant == DEVICE_VARIANT.DS_EDGE) 447 outputReportLength = 64; 448 else 449 outputReportLength = 48; 450 } 451 452 // Cleat all input data 453 ptrContext._internal.hidBuffer[0 .. outputReportLength] = 0; 454 // Build output buffer 455 if (ptrContext._internal.connection == DeviceConnection.BT) 456 { 457 //return DS5W_E_CURRENTLY_NOT_SUPPORTED; 458 // Report type 459 ptrContext._internal.hidBuffer[0x00] = 0x31; 460 ptrContext._internal.hidBuffer[0x01] = 0x02; 461 createHidOutputBuffer(&ptrContext._internal.hidBuffer[2], ptrOutputState); 462 463 // Hash 464 const crcChecksum = CRC32.compute(ptrContext._internal.hidBuffer.ptr, 74); 465 466 ptrContext._internal.hidBuffer[0x4A] = cast(ubyte)( 467 (crcChecksum & 0x000000FF) >> 0UL); 468 ptrContext._internal.hidBuffer[0x4B] = cast(ubyte)( 469 (crcChecksum & 0x0000FF00) >> 8UL); 470 ptrContext._internal.hidBuffer[0x4C] = cast(ubyte)( 471 (crcChecksum & 0x00FF0000) >> 16UL); 472 ptrContext._internal.hidBuffer[0x4D] = cast(ubyte)( 473 (crcChecksum & 0xFF000000) >> 24UL); 474 475 } 476 else 477 { 478 // Report type 479 ptrContext._internal.hidBuffer[0x00] = 0x02; 480 481 // Else it is USB so call its evaluator 482 createHidOutputBuffer(&ptrContext._internal.hidBuffer[1], ptrOutputState); 483 } 484 485 // Write to controller 486 if (!WriteFile(ptrContext._internal.deviceHandle, ptrContext._internal.hidBuffer.ptr, outputReportLength, NULL, NULL)) 487 { 488 debug 489 { 490 import std.stdio : writeln; 491 492 writeln(GetLastErrorAsString()); 493 } 494 // Close handle and set error state 495 CloseHandle(ptrContext._internal.deviceHandle); 496 ptrContext._internal.deviceHandle = null; 497 ptrContext._internal.connected = false; 498 499 // Return error 500 return DS5W_E_DEVICE_REMOVED; 501 } 502 503 // OK 504 return DS5W_OK; 505 }