scmd.c 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989
  1. // scmd.c — Steam Controller / Puck feature-report command tool (macOS IOKit).
  2. // Talks to the VENDOR HID interface (usage page 0xFF00) which opens WITHOUT
  3. // Input Monitoring permission. Implements the 64-byte SET_REPORT(command) plus
  4. // GET_REPORT(reply) command channel observed on the Triton copycat/real puck.
  5. //
  6. // clang -framework IOKit -framework CoreFoundation -o scmd scmd.c
  7. // ./scmd list [pidHex] # list Valve HID nodes and serials
  8. // ./scmd <pidHex> [--serial S] [--loc HEX] # run the SAFE read battery (no writes)
  9. // ./scmd <pidHex> [--serial S] [--loc HEX] <cmdHex> [payloadHexBytes...] # one custom command
  10. // ./scmd <pidHex> [--serial S] [--loc HEX] rid2 <cmdHex> [payloadHexBytes...] # custom command on one report id
  11. // ./scmd <pidHex> [--serial S] [--loc HEX] seq-noget rid2 <cmd> [bytes...] -- rid2 <cmd> [bytes...]
  12. // ./scmd <pidHex> [--serial S] [--loc HEX] a3watch [count] [delayMs] # watch/decode Triton A3 bond events
  13. // ./scmd <pidHex> [--serial S] [--loc HEX] a3variants # read-only A3 framing tests
  14. // ./scmd <pidHex> [--serial S] [--loc HEX] triton-bond <0|1> <keyHex16> <peerSerial>
  15. // ./scmd <pidHex> [--serial S] [--loc HEX] triton-transport <slot0|slot1|slot2|rawByte>
  16. //
  17. // SAFE reads used by default: 0xAE GET_STRING_ATTRIBUTE (serials), 0x83
  18. // GET_ATTRIBUTES_VALUES, 0xA1 GET_DEVICE_INFO, 0xB4 DONGLE_GET_WIRELESS_STATE.
  19. //
  20. // Command labels below are only labels. 0xA2/0xA3 are Triton-confirmed from our
  21. // copycat logs; the 0xAF..0xC4 names come from SDL's shared Steam constants and
  22. // are not proof of Triton pairing semantics. Destructive opcodes (0x86 reset,
  23. // 0x9F off, 0xAD pair, 0xB2/B3 dongle candidates, 0xAF/B0 radio-record
  24. // candidates) are NEVER sent unless you pass them explicitly.
  25. #include <IOKit/hid/IOHIDManager.h>
  26. #include <IOKit/IOKitLib.h>
  27. #include <CoreFoundation/CoreFoundation.h>
  28. #include <stdio.h>
  29. #include <stdlib.h>
  30. #include <string.h>
  31. #include <unistd.h>
  32. #define VALVE_VID 0x28DE
  33. static void dump(const char *label, const uint8_t *p, int n)
  34. {
  35. printf("%s [%d]:\n ", label, n);
  36. for (int i = 0; i < n; i++) {
  37. if (i && i % 32 == 0)
  38. printf("\n ");
  39. printf("%02X ", p[i]);
  40. }
  41. printf("\n ascii: ");
  42. for (int i = 0; i < n; i++)
  43. putchar((p[i] >= 32 && p[i] < 127) ? p[i] : '.');
  44. printf("\n");
  45. }
  46. static void print_payload_inline(const uint8_t *p, int n)
  47. {
  48. if (n <= 0) {
  49. printf("-");
  50. return;
  51. }
  52. for (int i = 0; i < n; i++) {
  53. if (i)
  54. printf(" ");
  55. printf("%02X", p[i]);
  56. }
  57. }
  58. static const char *cmd_name(uint8_t cmd)
  59. {
  60. switch (cmd) {
  61. case 0x83:
  62. return "GET_ATTRIBUTES_VALUES";
  63. case 0xA1:
  64. return "GET_DEVICE_INFO";
  65. case 0xA2:
  66. return "TRITON_A2_OBSERVED_PAIRING_RECORD";
  67. case 0xA3:
  68. return "TRITON_A3_BOND_EVENT_OR_STATUS";
  69. case 0xAD:
  70. return "ENABLE_PAIRING";
  71. case 0xAE:
  72. return "GET_STRING_ATTRIBUTE";
  73. case 0xAF:
  74. return "SDL_SHARED_RADIO_ERASE_RECORDS";
  75. case 0xB0:
  76. return "SDL_SHARED_RADIO_WRITE_RECORD";
  77. case 0xB2:
  78. return "SDL_SHARED_DONGLE_DISCONNECT_DEVICE";
  79. case 0xB3:
  80. return "SDL_SHARED_DONGLE_COMMIT_DEVICE_CANDIDATE";
  81. case 0xB4:
  82. return "SDL_SHARED_DONGLE_GET_WIRELESS_STATE";
  83. case 0xC4:
  84. return "SDL_SHARED_DONGLE_GET_CONNECTED_SLOTS";
  85. case 0xEE:
  86. return "TRITON_KEYED_VALUE_WRITE";
  87. case 0xEF:
  88. return "TRITON_KEYED_VALUE_COMMIT";
  89. default:
  90. return NULL;
  91. }
  92. }
  93. static int hid_long(IOHIDDeviceRef d, CFStringRef key, long *out)
  94. {
  95. CFTypeRef v = IOHIDDeviceGetProperty(d, key);
  96. if (!v || CFGetTypeID(v) != CFNumberGetTypeID())
  97. return 0;
  98. return CFNumberGetValue((CFNumberRef)v, kCFNumberLongType, out);
  99. }
  100. static void hid_string(IOHIDDeviceRef d, CFStringRef key, char *out,
  101. size_t outn)
  102. {
  103. if (!outn)
  104. return;
  105. out[0] = 0;
  106. CFTypeRef v = IOHIDDeviceGetProperty(d, key);
  107. if (!v)
  108. return;
  109. if (CFGetTypeID(v) == CFStringGetTypeID()) {
  110. CFStringGetCString((CFStringRef)v, out, outn,
  111. kCFStringEncodingUTF8);
  112. } else if (CFGetTypeID(v) == CFNumberGetTypeID()) {
  113. long x = 0;
  114. if (CFNumberGetValue((CFNumberRef)v, kCFNumberLongType, &x))
  115. snprintf(out, outn, "%ld", x);
  116. }
  117. }
  118. static int reg_long(io_object_t svc, const char *key, long *out)
  119. {
  120. CFStringRef k = CFStringCreateWithCString(kCFAllocatorDefault, key,
  121. kCFStringEncodingUTF8);
  122. if (!k)
  123. return 0;
  124. CFTypeRef v =
  125. IORegistryEntryCreateCFProperty(svc, k, kCFAllocatorDefault, 0);
  126. CFRelease(k);
  127. if (!v)
  128. return 0;
  129. int ok = 0;
  130. if (CFGetTypeID(v) == CFNumberGetTypeID())
  131. ok = CFNumberGetValue((CFNumberRef)v, kCFNumberLongType, out);
  132. CFRelease(v);
  133. return ok;
  134. }
  135. static void reg_string(io_object_t svc, const char *key, char *out, size_t outn)
  136. {
  137. if (!outn)
  138. return;
  139. out[0] = 0;
  140. CFStringRef k = CFStringCreateWithCString(kCFAllocatorDefault, key,
  141. kCFStringEncodingUTF8);
  142. if (!k)
  143. return;
  144. CFTypeRef v =
  145. IORegistryEntryCreateCFProperty(svc, k, kCFAllocatorDefault, 0);
  146. CFRelease(k);
  147. if (!v)
  148. return;
  149. if (CFGetTypeID(v) == CFStringGetTypeID()) {
  150. CFStringGetCString((CFStringRef)v, out, outn,
  151. kCFStringEncodingUTF8);
  152. } else if (CFGetTypeID(v) == CFNumberGetTypeID()) {
  153. long x = 0;
  154. if (CFNumberGetValue((CFNumberRef)v, kCFNumberLongType, &x))
  155. snprintf(out, outn, "%ld", x);
  156. }
  157. CFRelease(v);
  158. }
  159. static void print_device_line(io_object_t svc, IOHIDDeviceRef d)
  160. {
  161. long vid = -1, pid = -1, up = -1, usage = -1, loc = -1, maxF = -1;
  162. char serial[128], product[128], manufacturer[128];
  163. serial[0] = product[0] = manufacturer[0] = 0;
  164. reg_long(svc, "VendorID", &vid);
  165. if (vid < 0)
  166. hid_long(d, CFSTR(kIOHIDVendorIDKey), &vid);
  167. reg_long(svc, "ProductID", &pid);
  168. if (pid < 0)
  169. hid_long(d, CFSTR(kIOHIDProductIDKey), &pid);
  170. reg_long(svc, "PrimaryUsagePage", &up);
  171. if (up < 0)
  172. hid_long(d, CFSTR(kIOHIDPrimaryUsagePageKey), &up);
  173. reg_long(svc, "PrimaryUsage", &usage);
  174. if (usage < 0)
  175. hid_long(d, CFSTR(kIOHIDPrimaryUsageKey), &usage);
  176. reg_long(svc, "LocationID", &loc);
  177. if (loc < 0)
  178. hid_long(d, CFSTR(kIOHIDLocationIDKey), &loc);
  179. reg_long(svc, "MaxFeatureReportSize", &maxF);
  180. reg_string(svc, "SerialNumber", serial, sizeof serial);
  181. if (!serial[0])
  182. hid_string(d, CFSTR(kIOHIDSerialNumberKey), serial,
  183. sizeof serial);
  184. reg_string(svc, "Product", product, sizeof product);
  185. if (!product[0])
  186. hid_string(d, CFSTR(kIOHIDProductKey), product, sizeof product);
  187. reg_string(svc, "Manufacturer", manufacturer, sizeof manufacturer);
  188. if (!manufacturer[0])
  189. hid_string(d, CFSTR(kIOHIDManufacturerKey), manufacturer,
  190. sizeof manufacturer);
  191. printf("Valve %04lX:%04lX usagePage=0x%lX usage=0x%lX maxFeature=%ld loc=0x%lX serial=%s product=%s manufacturer=%s\n",
  192. vid, pid, up, usage, maxF, loc, serial[0] ? serial : "-",
  193. product[0] ? product : "-",
  194. manufacturer[0] ? manufacturer : "-");
  195. }
  196. static int list_devices(long wantPid)
  197. {
  198. CFMutableDictionaryRef m = IOServiceMatching("IOHIDDevice");
  199. io_iterator_t it;
  200. if (IOServiceGetMatchingServices(kIOMainPortDefault, m, &it) !=
  201. KERN_SUCCESS) {
  202. fprintf(stderr, "match fail\n");
  203. return 1;
  204. }
  205. int n = 0;
  206. io_object_t svc;
  207. while ((svc = IOIteratorNext(it))) {
  208. IOHIDDeviceRef d = IOHIDDeviceCreate(kCFAllocatorDefault, svc);
  209. if (d) {
  210. long vid = -1, pid = -1;
  211. reg_long(svc, "VendorID", &vid);
  212. if (vid < 0)
  213. hid_long(d, CFSTR(kIOHIDVendorIDKey), &vid);
  214. reg_long(svc, "ProductID", &pid);
  215. if (pid < 0)
  216. hid_long(d, CFSTR(kIOHIDProductIDKey), &pid);
  217. if (vid == VALVE_VID &&
  218. (wantPid < 0 || pid == wantPid)) {
  219. print_device_line(svc, d);
  220. n++;
  221. }
  222. CFRelease(d);
  223. }
  224. IOObjectRelease(svc);
  225. }
  226. IOObjectRelease(it);
  227. if (!n)
  228. printf("no Valve HID devices found%s\n",
  229. wantPid < 0 ? "" : " for requested PID");
  230. return n ? 0 : 1;
  231. }
  232. // Issue one command on report id `rid`: [rid][cmd][len][payload...].
  233. static void sc_cmd_ex(IOHIDDeviceRef d, int rid, uint8_t cmd, const uint8_t *pl,
  234. int pln, int do_get)
  235. {
  236. uint8_t tx[64];
  237. memset(tx, 0, sizeof tx);
  238. tx[0] = (uint8_t)rid;
  239. tx[1] = cmd;
  240. tx[2] = (uint8_t)pln;
  241. if (pln > 0)
  242. memcpy(tx + 3, pl, pln);
  243. IOReturn rs =
  244. IOHIDDeviceSetReport(d, kIOHIDReportTypeFeature, rid, tx, 64);
  245. const char *name = cmd_name(cmd);
  246. if (rs != kIOReturnSuccess) {
  247. printf("\n=== cmd 0x%02X%s%s (payload %d: ", cmd,
  248. name ? " " : "", name ? name : "", pln);
  249. print_payload_inline(pl, pln);
  250. printf(") on report id %d [set=0x%08X get=skipped-set-failed] ===\n",
  251. rid, rs);
  252. return;
  253. }
  254. if (!do_get) {
  255. printf("\n=== cmd 0x%02X%s%s (payload %d: ", cmd,
  256. name ? " " : "", name ? name : "", pln);
  257. print_payload_inline(pl, pln);
  258. printf(") on report id %d [set=0x%08X get=skipped] ===\n", rid,
  259. rs);
  260. return;
  261. }
  262. usleep(20000);
  263. uint8_t rx[64];
  264. memset(rx, 0, sizeof rx);
  265. rx[0] = (uint8_t)rid;
  266. CFIndex rl = 64;
  267. IOReturn rg =
  268. IOHIDDeviceGetReport(d, kIOHIDReportTypeFeature, rid, rx, &rl);
  269. printf("\n=== cmd 0x%02X%s%s (payload %d: ", cmd, name ? " " : "",
  270. name ? name : "", pln);
  271. print_payload_inline(pl, pln);
  272. printf(") on report id %d [set=0x%08X get=0x%08X] ===\n", rid, rs, rg);
  273. if (rg == kIOReturnSuccess)
  274. dump("reply", rx, (int)rl);
  275. else
  276. printf(" (GET failed)\n");
  277. }
  278. static void sc_cmd(IOHIDDeviceRef d, int rid, uint8_t cmd, const uint8_t *pl,
  279. int pln)
  280. {
  281. sc_cmd_ex(d, rid, cmd, pl, pln, 1);
  282. }
  283. static int hex_value(char c)
  284. {
  285. if (c >= '0' && c <= '9')
  286. return c - '0';
  287. if (c >= 'a' && c <= 'f')
  288. return c - 'a' + 10;
  289. if (c >= 'A' && c <= 'F')
  290. return c - 'A' + 10;
  291. return -1;
  292. }
  293. static int parse_hex_bytes(const char *hex, uint8_t *out, int outn)
  294. {
  295. int n = 0;
  296. int hi = -1;
  297. for (const char *p = hex; *p; p++) {
  298. if (*p == ':' || *p == '-' || *p == ' ' || *p == '_')
  299. continue;
  300. int v = hex_value(*p);
  301. if (v < 0)
  302. return -1;
  303. if (hi < 0) {
  304. hi = v;
  305. } else {
  306. if (n >= outn)
  307. return -1;
  308. out[n++] = (uint8_t)((hi << 4) | v);
  309. hi = -1;
  310. }
  311. }
  312. if (hi >= 0)
  313. return -1;
  314. return n;
  315. }
  316. static int make_serial16(const char *serial, uint8_t out[16])
  317. {
  318. memset(out, 0, 16);
  319. size_t n = strlen(serial);
  320. if (n > 16)
  321. return 0;
  322. memcpy(out, serial, n);
  323. return 1;
  324. }
  325. static int triton_keyed_write(IOHIDDeviceRef dev, const char *key,
  326. const uint8_t *payload, int payload_len)
  327. {
  328. size_t key_len = strlen(key) + 1;
  329. if (key_len + (size_t)payload_len > 61 || key_len > 61) {
  330. fprintf(stderr,
  331. "keyed payload too long for 64-byte feature report\n");
  332. return 2;
  333. }
  334. uint8_t ee[61];
  335. memcpy(ee, key, key_len);
  336. if (payload_len > 0)
  337. memcpy(ee + key_len, payload, payload_len);
  338. uint8_t ef[61];
  339. memcpy(ef, key, key_len);
  340. printf("Triton keyed write key=\"%s\" payload=", key);
  341. print_payload_inline(payload, payload_len);
  342. printf("\n");
  343. sc_cmd_ex(dev, 2, 0xEE, ee, (int)key_len + payload_len, 0);
  344. usleep(20000);
  345. sc_cmd_ex(dev, 2, 0xEF, ef, (int)key_len, 0);
  346. return 0;
  347. }
  348. static int triton_bond_write(IOHIDDeviceRef dev, int slot, const char *key_hex,
  349. const char *serial)
  350. {
  351. if (slot < 0 || slot > 1) {
  352. fprintf(stderr, "triton-bond slot must be 0 or 1\n");
  353. return 2;
  354. }
  355. uint8_t payload[24];
  356. int keyn = parse_hex_bytes(key_hex, payload, 8);
  357. if (keyn != 8) {
  358. fprintf(stderr,
  359. "triton-bond key must be exactly 8 bytes / 16 hex digits\n");
  360. return 2;
  361. }
  362. if (!make_serial16(serial, payload + 8)) {
  363. fprintf(stderr,
  364. "controller serial must be at most 16 ASCII bytes\n");
  365. return 2;
  366. }
  367. const char *key = slot == 0 ? "esb/bond" : "esb/bond_2";
  368. printf("note: Steam uses this keyed path for controller-side bond storage; real pucks reject it.\n");
  369. return triton_keyed_write(dev, key, payload, sizeof payload);
  370. }
  371. static int parse_triton_transport(const char *arg, uint8_t *out)
  372. {
  373. if (strcmp(arg, "slot0") == 0 || strcmp(arg, "0") == 0) {
  374. *out = 2;
  375. return 1;
  376. }
  377. if (strcmp(arg, "slot1") == 0 || strcmp(arg, "1") == 0) {
  378. *out = 3;
  379. return 1;
  380. }
  381. if (strcmp(arg, "slot2") == 0 || strcmp(arg, "2") == 0) {
  382. *out = 0;
  383. return 1;
  384. }
  385. char *end = NULL;
  386. long raw = strtol(arg, &end, 0);
  387. if (end && *end == 0 && raw >= 0 && raw <= 255) {
  388. *out = (uint8_t)raw;
  389. return 1;
  390. }
  391. return 0;
  392. }
  393. static int triton_transport_write(IOHIDDeviceRef dev, const char *arg)
  394. {
  395. uint8_t value = 0;
  396. if (!parse_triton_transport(arg, &value)) {
  397. fprintf(stderr,
  398. "triton-transport expects slot0, slot1, slot2, or raw byte 0..255\n");
  399. return 2;
  400. }
  401. return triton_keyed_write(dev, "user/wireless_transport", &value, 1);
  402. }
  403. static void print_a3_decode(const uint8_t *rx, int rl)
  404. {
  405. if (rl < 3 || rx[1] != 0xA3) {
  406. printf(" not an A3 reply\n");
  407. return;
  408. }
  409. int len = rx[2];
  410. if (len != 0x18 || rl < 27) {
  411. printf(" A3 len=0x%02X (no bond payload)\n", len);
  412. return;
  413. }
  414. const uint8_t *key = rx + 3;
  415. const uint8_t *serial = rx + 11;
  416. uint32_t w0 = (uint32_t)key[0] | ((uint32_t)key[1] << 8) |
  417. ((uint32_t)key[2] << 16) | ((uint32_t)key[3] << 24);
  418. uint32_t w1 = (uint32_t)key[4] | ((uint32_t)key[5] << 8) |
  419. ((uint32_t)key[6] << 16) | ((uint32_t)key[7] << 24);
  420. printf(" A3 bond key=");
  421. for (int i = 0; i < 8; i++)
  422. printf("%s%02X", i ? " " : "", key[i]);
  423. printf(" steam_words=%X_%X serial=", w0, w1);
  424. for (int i = 0; i < 16 && serial[i]; i++)
  425. putchar((serial[i] >= 32 && serial[i] < 127) ? serial[i] : '.');
  426. printf("\n");
  427. }
  428. static void print_a3_decode_any(const uint8_t *rx, int rl)
  429. {
  430. if (rl >= 2 && rx[0] == 0xA3) {
  431. uint8_t shifted[64];
  432. int n = rl + 1;
  433. if (n > (int)sizeof shifted)
  434. n = (int)sizeof shifted;
  435. memset(shifted, 0, sizeof shifted);
  436. shifted[0] = 2;
  437. memcpy(shifted + 1, rx, n - 1);
  438. print_a3_decode(shifted, n);
  439. return;
  440. }
  441. print_a3_decode(rx, rl);
  442. }
  443. static int a3_watch(IOHIDDeviceRef dev, int count, int delay_ms)
  444. {
  445. if (count <= 0)
  446. count = 20;
  447. if (delay_ms < 0)
  448. delay_ms = 100;
  449. for (int i = 0; i < count; i++) {
  450. uint8_t tx[64];
  451. memset(tx, 0, sizeof tx);
  452. tx[0] = 2;
  453. tx[1] = 0xA3;
  454. tx[2] = 0;
  455. IOReturn rs = IOHIDDeviceSetReport(dev, kIOHIDReportTypeFeature,
  456. 2, tx, 64);
  457. usleep(20000);
  458. uint8_t rx[64];
  459. memset(rx, 0, sizeof rx);
  460. rx[0] = 2;
  461. CFIndex rl = 64;
  462. IOReturn rg = kIOReturnError;
  463. if (rs == kIOReturnSuccess)
  464. rg = IOHIDDeviceGetReport(dev, kIOHIDReportTypeFeature,
  465. 2, rx, &rl);
  466. printf("[%02d] A3 set=0x%08X get=0x%08X", i + 1, rs, rg);
  467. if (rg == kIOReturnSuccess && rl >= 3)
  468. printf(" reply=%02X %02X %02X", rx[0], rx[1], rx[2]);
  469. printf("\n");
  470. if (rg == kIOReturnSuccess)
  471. print_a3_decode(rx, (int)rl);
  472. if (delay_ms > 0 && i + 1 < count)
  473. usleep((useconds_t)delay_ms * 1000);
  474. }
  475. return 0;
  476. }
  477. static void a3_variant(IOHIDDeviceRef dev, const char *label, int include_rid,
  478. int set_len, int get_len, int seed_rid)
  479. {
  480. uint8_t tx[64];
  481. memset(tx, 0, sizeof tx);
  482. if (include_rid) {
  483. tx[0] = 2;
  484. tx[1] = 0xA3;
  485. tx[2] = 0;
  486. } else {
  487. tx[0] = 0xA3;
  488. tx[1] = 0;
  489. }
  490. IOReturn rs = IOHIDDeviceSetReport(dev, kIOHIDReportTypeFeature, 2, tx,
  491. set_len);
  492. usleep(20000);
  493. uint8_t rx[64];
  494. memset(rx, 0, sizeof rx);
  495. if (seed_rid)
  496. rx[0] = 2;
  497. CFIndex rl = get_len;
  498. IOReturn rg = kIOReturnError;
  499. if (rs == kIOReturnSuccess)
  500. rg = IOHIDDeviceGetReport(dev, kIOHIDReportTypeFeature, 2, rx,
  501. &rl);
  502. printf("\n=== A3 variant %s setLen=%d getLen=%d includeRid=%d seedRid=%d set=0x%08X get=0x%08X rl=%ld ===\n",
  503. label, set_len, get_len, include_rid, seed_rid, rs, rg,
  504. (long)rl);
  505. if (rg == kIOReturnSuccess) {
  506. dump("reply", rx, (int)rl);
  507. print_a3_decode_any(rx, (int)rl);
  508. }
  509. }
  510. static int a3_variants(IOHIDDeviceRef dev)
  511. {
  512. a3_variant(dev, "steam-short", 0, 2, 0x1a, 0);
  513. a3_variant(dev, "steam-short-seeded", 0, 2, 0x1a, 1);
  514. a3_variant(dev, "rid-short", 1, 3, 0x1b, 1);
  515. a3_variant(dev, "rid-64", 1, 64, 64, 1);
  516. return 0;
  517. }
  518. static int run_seq(IOHIDDeviceRef dev, int argc, char **argv, int start,
  519. int do_get)
  520. {
  521. int i = start;
  522. while (i < argc) {
  523. if (strcmp(argv[i], "--") == 0) {
  524. i++;
  525. continue;
  526. }
  527. if (strncmp(argv[i], "rid", 3) != 0) {
  528. fprintf(stderr,
  529. "seq expects ridN before each command, got: %s\n",
  530. argv[i]);
  531. return 2;
  532. }
  533. int rid = atoi(argv[i] + 3);
  534. if (rid < 1 || rid > 255) {
  535. fprintf(stderr, "bad report id: %s\n", argv[i]);
  536. return 2;
  537. }
  538. i++;
  539. if (i >= argc) {
  540. fprintf(stderr, "seq missing cmd after rid%d\n", rid);
  541. return 2;
  542. }
  543. uint8_t cmd = (uint8_t)strtol(argv[i++], NULL, 16);
  544. uint8_t pl[62];
  545. int pln = 0;
  546. while (i < argc && strcmp(argv[i], "--") != 0) {
  547. if (strncmp(argv[i], "rid", 3) == 0)
  548. break;
  549. if (pln >= 62) {
  550. fprintf(stderr,
  551. "payload too long for cmd 0x%02X\n",
  552. cmd);
  553. return 2;
  554. }
  555. pl[pln++] = (uint8_t)strtol(argv[i++], NULL, 16);
  556. }
  557. sc_cmd_ex(dev, rid, cmd, pl, pln, do_get);
  558. usleep(20000);
  559. }
  560. return 0;
  561. }
  562. int main(int argc, char **argv)
  563. {
  564. if (argc < 2) {
  565. fprintf(stderr,
  566. "usage: %s list [pidHex] | <pidHex> [--serial SERIAL] [cmdHex payloadBytes...]\n",
  567. argv[0]);
  568. return 2;
  569. }
  570. if (strcmp(argv[1], "list") == 0) {
  571. long wantPid = argc >= 3 ? strtol(argv[2], NULL, 16) : -1;
  572. return list_devices(wantPid);
  573. }
  574. long wantPid = strtol(argv[1], NULL, 16);
  575. const char *wantSerial = NULL;
  576. long wantLoc = -1;
  577. long wantUp = -1;
  578. int wantIdx = -1;
  579. int matchIdx = 0;
  580. int arg = 2;
  581. while (arg < argc) {
  582. if (strcmp(argv[arg], "--serial") == 0) {
  583. if (arg + 1 >= argc) {
  584. fprintf(stderr, "--serial needs a value\n");
  585. return 2;
  586. }
  587. wantSerial = argv[arg + 1];
  588. arg += 2;
  589. } else if (strncmp(argv[arg], "--serial=", 9) == 0) {
  590. wantSerial = argv[arg] + 9;
  591. arg++;
  592. } else if (strcmp(argv[arg], "--loc") == 0) {
  593. if (arg + 1 >= argc) {
  594. fprintf(stderr, "--loc needs a value\n");
  595. return 2;
  596. }
  597. wantLoc = strtol(argv[arg + 1], NULL, 0);
  598. arg += 2;
  599. } else if (strncmp(argv[arg], "--loc=", 6) == 0) {
  600. wantLoc = strtol(argv[arg] + 6, NULL, 0);
  601. arg++;
  602. } else if (strcmp(argv[arg], "--up") == 0) {
  603. if (arg + 1 >= argc) {
  604. fprintf(stderr, "--up needs a value\n");
  605. return 2;
  606. }
  607. wantUp = strtol(argv[arg + 1], NULL, 0);
  608. arg += 2;
  609. } else if (strcmp(argv[arg], "--idx") == 0) {
  610. if (arg + 1 >= argc) {
  611. fprintf(stderr, "--idx needs a value\n");
  612. return 2;
  613. }
  614. wantIdx = strtol(argv[arg + 1], NULL, 0);
  615. arg += 2;
  616. } else {
  617. break;
  618. }
  619. }
  620. CFMutableDictionaryRef m = IOServiceMatching("IOHIDDevice");
  621. io_iterator_t it;
  622. if (IOServiceGetMatchingServices(kIOMainPortDefault, m, &it) !=
  623. KERN_SUCCESS) {
  624. fprintf(stderr, "match fail\n");
  625. return 1;
  626. }
  627. IOHIDDeviceRef dev = NULL;
  628. io_object_t svc;
  629. while ((svc = IOIteratorNext(it))) {
  630. CFTypeRef vid = IORegistryEntryCreateCFProperty(
  631. svc, CFSTR("VendorID"), kCFAllocatorDefault, 0);
  632. CFTypeRef pid = IORegistryEntryCreateCFProperty(
  633. svc, CFSTR("ProductID"), kCFAllocatorDefault, 0);
  634. long v = -1, p = -1;
  635. if (vid) {
  636. CFNumberGetValue((CFNumberRef)vid, kCFNumberLongType,
  637. &v);
  638. CFRelease(vid);
  639. }
  640. if (pid) {
  641. CFNumberGetValue((CFNumberRef)pid, kCFNumberLongType,
  642. &p);
  643. CFRelease(pid);
  644. }
  645. if (v == VALVE_VID && p == wantPid) {
  646. IOHIDDeviceRef d =
  647. IOHIDDeviceCreate(kCFAllocatorDefault, svc);
  648. long up = -1, maxF = -1, loc = -1;
  649. if (d) {
  650. reg_long(svc, "PrimaryUsagePage", &up);
  651. if (up < 0) {
  652. CFTypeRef u = IOHIDDeviceGetProperty(
  653. d,
  654. CFSTR(kIOHIDPrimaryUsagePageKey));
  655. if (u)
  656. CFNumberGetValue(
  657. (CFNumberRef)u,
  658. kCFNumberLongType, &up);
  659. }
  660. reg_long(svc, "MaxFeatureReportSize", &maxF);
  661. reg_long(svc, "LocationID", &loc);
  662. }
  663. char serial[128] = "";
  664. char product[128] = "";
  665. if (d) {
  666. reg_string(svc, "SerialNumber", serial,
  667. sizeof serial);
  668. if (!serial[0])
  669. hid_string(d,
  670. CFSTR(kIOHIDSerialNumberKey),
  671. serial, sizeof serial);
  672. reg_string(svc, "Product", product,
  673. sizeof product);
  674. if (!product[0])
  675. hid_string(d, CFSTR(kIOHIDProductKey),
  676. product, sizeof product);
  677. }
  678. if (wantSerial && strcmp(wantSerial, serial) != 0) {
  679. if (d)
  680. CFRelease(d);
  681. IOObjectRelease(svc);
  682. continue;
  683. }
  684. if (wantLoc >= 0 && loc != wantLoc) {
  685. if (d)
  686. CFRelease(d);
  687. IOObjectRelease(svc);
  688. continue;
  689. }
  690. if (wantUp >= 0 && up != wantUp) {
  691. if (d)
  692. CFRelease(d);
  693. IOObjectRelease(svc);
  694. continue;
  695. }
  696. if (wantIdx >= 0 && matchIdx++ != wantIdx) {
  697. if (d)
  698. CFRelease(d);
  699. IOObjectRelease(svc);
  700. continue;
  701. }
  702. int command_capable = (up == 0xFF00) || (maxF >= 64);
  703. IOReturn ro =
  704. d && command_capable ?
  705. IOHIDDeviceOpen(d,
  706. kIOHIDOptionsTypeNone) :
  707. kIOReturnNotFound;
  708. if (d && command_capable && ro == kIOReturnSuccess) {
  709. printf("opened Valve %04lX:%04lX node (usagePage=0x%lX maxFeature=%ld serial=%s product=%s)\n",
  710. (long)VALVE_VID, wantPid, up, maxF,
  711. serial[0] ? serial : "-",
  712. product[0] ? product : "-");
  713. dev = d;
  714. IOObjectRelease(svc);
  715. break;
  716. }
  717. if (d && command_capable &&
  718. (wantSerial || wantLoc >= 0)) {
  719. fprintf(stderr,
  720. "matched Valve %04lX:%04lX usagePage=0x%lX maxFeature=%ld loc=0x%lX serial=%s but open failed: 0x%08X\n",
  721. (long)VALVE_VID, wantPid, up, maxF, loc,
  722. serial[0] ? serial : "-", ro);
  723. }
  724. if (d)
  725. CFRelease(d);
  726. }
  727. IOObjectRelease(svc);
  728. }
  729. IOObjectRelease(it);
  730. if (!dev) {
  731. if (wantSerial || wantLoc >= 0)
  732. fprintf(stderr,
  733. "no openable Valve %04lX command-capable interface matching serial=%s loc=0x%lX (quit Steam? wrong device?)\n",
  734. wantPid, wantSerial ? wantSerial : "-",
  735. wantLoc);
  736. else
  737. fprintf(stderr,
  738. "no openable Valve %04lX command-capable interface (quit Steam? add --serial/--loc?)\n",
  739. wantPid);
  740. return 1;
  741. }
  742. if (arg < argc && strcmp(argv[arg], "slotwrite") == 0) {
  743. // Single-session dongle slot write: 0xAD 01 (enter pairing mode) -> 0xA2 <24-byte record>
  744. // -> 0xAD 00, all on the SAME open handle (Steam keeps one session; pairing mode resets on
  745. // close). record = [8-byte uuids][16-byte serial]; zeros-serial clears the matching slot.
  746. // first arg = report id (the ALT channel is report id 3); rest = 24-byte record
  747. int rid = (arg + 1 < argc) ? atoi(argv[arg + 1]) : 3;
  748. uint8_t rec[24];
  749. memset(rec, 0, sizeof rec);
  750. int n = 0;
  751. for (int i = arg + 2; i < argc && n < 24; i++)
  752. rec[n++] = (uint8_t)strtol(argv[i], NULL, 16);
  753. uint8_t on[2] = { 0x01, 0x00 }, off[1] = { 0x00 };
  754. printf("# slotwrite rid%d: AD 01 -> A2 <24> -> AD 00 (one session)\n",
  755. rid);
  756. sc_cmd(dev, rid, 0xAD, on, 2);
  757. sc_cmd(dev, rid, 0xA2, rec, 24);
  758. sc_cmd(dev, rid, 0xAD, off, 1);
  759. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  760. CFRelease(dev);
  761. return 0;
  762. } else if (arg < argc && strcmp(argv[arg], "pair") == 0) {
  763. // Steamless pairing (firmware-derived): 0xAD [mode=1][channel] enters RF pairing mode;
  764. // the puck then runs the pairing handshake autonomously. Channel 0 => firmware default
  765. // (0x3c). Put the controller in pairing via its key-chord, then we poll 0xA3 for the bond.
  766. uint8_t ch = (arg + 1 < argc) ?
  767. (uint8_t)strtol(argv[arg + 1], NULL, 16) :
  768. 0x00;
  769. uint8_t start[2] = { 0x01, ch };
  770. printf("# pair: entering pairing mode (0xAD 01 %02X).\n", ch);
  771. printf("# >>> Now put the controller into pairing via its key-chord. Polling 0xA3...\n");
  772. sc_cmd(dev, 2, 0xAD, start, 2);
  773. int rc = a3_watch(
  774. dev, 120,
  775. 200); // ~24s: watch status for the controller to bond
  776. uint8_t stop[1] = { 0x00 };
  777. printf("# pair: exiting pairing mode (0xAD 00).\n");
  778. sc_cmd(dev, 2, 0xAD, stop, 1);
  779. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  780. CFRelease(dev);
  781. return rc;
  782. } else if (arg < argc && strcmp(argv[arg], "stop") == 0) {
  783. uint8_t stop[1] = { 0x00 }; // exit pairing mode
  784. sc_cmd(dev, 2, 0xAD, stop, 1);
  785. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  786. CFRelease(dev);
  787. return 0;
  788. } else if (arg < argc && strcmp(argv[arg], "info") == 0) {
  789. // Read identity + bond status without changing anything.
  790. printf("\n##### 0x83 attributes #####\n");
  791. sc_cmd(dev, 2, 0x83, NULL, 0);
  792. printf("\n##### 0xAE strings (idx 0..7) #####\n");
  793. for (int a = 0; a < 8; a++) {
  794. uint8_t aa = (uint8_t)a;
  795. sc_cmd(dev, 2, 0xAE, &aa, 1);
  796. }
  797. printf("\n##### 0xB4 wireless state #####\n");
  798. sc_cmd(dev, 2, 0xB4, NULL, 0);
  799. printf("\n##### 0xA3 status / bond #####\n");
  800. sc_cmd(dev, 2, 0xA3, NULL, 0);
  801. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  802. CFRelease(dev);
  803. return 0;
  804. } else if (arg < argc && strcmp(argv[arg], "a3watch") == 0) {
  805. int count = (arg + 1 < argc) ? atoi(argv[arg + 1]) : 20;
  806. int delay_ms = (arg + 2 < argc) ? atoi(argv[arg + 2]) : 100;
  807. int rc = a3_watch(dev, count, delay_ms);
  808. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  809. CFRelease(dev);
  810. return rc;
  811. } else if (arg < argc && strcmp(argv[arg], "a3variants") == 0) {
  812. int rc = a3_variants(dev);
  813. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  814. CFRelease(dev);
  815. return rc;
  816. } else if (arg < argc && strcmp(argv[arg], "triton-bond") == 0) {
  817. if (arg + 3 >= argc) {
  818. fprintf(stderr,
  819. "usage: %s <pidHex> [--serial S] triton-bond <0|1> <keyHex16> <controllerSerial>\n",
  820. argv[0]);
  821. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  822. CFRelease(dev);
  823. return 2;
  824. }
  825. int slot = atoi(argv[arg + 1]);
  826. int rc = triton_bond_write(dev, slot, argv[arg + 2],
  827. argv[arg + 3]);
  828. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  829. CFRelease(dev);
  830. return rc;
  831. } else if (arg < argc && strcmp(argv[arg], "triton-transport") == 0) {
  832. if (arg + 1 >= argc) {
  833. fprintf(stderr,
  834. "usage: %s <pidHex> [--serial S] triton-transport <slot0|slot1|slot2|rawByte>\n",
  835. argv[0]);
  836. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  837. CFRelease(dev);
  838. return 2;
  839. }
  840. int rc = triton_transport_write(dev, argv[arg + 1]);
  841. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  842. CFRelease(dev);
  843. return rc;
  844. }
  845. // probe all non-destructive opcodes for bond/serial data
  846. else if (arg < argc && strcmp(argv[arg], "find") == 0) {
  847. // BLOCKLIST = known state-changing/destructive opcodes; never sent here.
  848. static const int block[] = { 0x80, 0x81, 0x85, 0x86, 0x87, 0x88,
  849. 0x8D, 0x8E, 0x8F, 0x9F, 0xAD, 0xAF,
  850. 0xB0, 0xB2, 0xB3, 0xEB, -1 };
  851. int rid = 2;
  852. for (int cmd = 0x82; cmd <= 0xFE; cmd++) {
  853. int blocked = 0;
  854. for (const int *b = block; *b >= 0; b++)
  855. if (*b == cmd)
  856. blocked = 1;
  857. if (blocked)
  858. continue;
  859. // send with empty payload; report only if SET succeeded (supported opcode)
  860. uint8_t tx[64];
  861. memset(tx, 0, sizeof tx);
  862. tx[0] = rid;
  863. tx[1] = (uint8_t)cmd;
  864. tx[2] = 0;
  865. IOReturn rs = IOHIDDeviceSetReport(
  866. dev, kIOHIDReportTypeFeature, rid, tx, 64);
  867. if (rs != kIOReturnSuccess)
  868. continue; // unsupported -> stall, skip silently
  869. usleep(15000);
  870. uint8_t rx[64];
  871. memset(rx, 0, sizeof rx);
  872. rx[0] = rid;
  873. CFIndex rl = 64;
  874. IOReturn rg = IOHIDDeviceGetReport(
  875. dev, kIOHIDReportTypeFeature, rid, rx, &rl);
  876. if (rg == kIOReturnSuccess &&
  877. rx[1] == cmd) { // echoed -> real reply
  878. char lbl[24];
  879. snprintf(lbl, sizeof lbl,
  880. "SUPPORTED cmd 0x%02X", cmd);
  881. dump(lbl, rx, (int)rl);
  882. }
  883. }
  884. } else if (arg < argc &&
  885. strcmp(argv[arg], "labels") ==
  886. 0) { // SAFE read-only label/settings sweep
  887. int rid = 2;
  888. printf("\n##### 0x84 GET_ATTRIBUTE_LABEL idx 0..15 #####\n");
  889. for (int i = 0; i < 16; i++) {
  890. uint8_t p[2] = { (uint8_t)i, 0 };
  891. sc_cmd(dev, rid, 0x84, p, 2);
  892. }
  893. printf("\n##### 0x8A GET_SETTING_LABEL idx 0..47 #####\n");
  894. for (int i = 0; i < 48; i++) {
  895. uint8_t p[2] = { (uint8_t)i, 0 };
  896. sc_cmd(dev, rid, 0x8A, p, 2);
  897. }
  898. printf("\n##### 0x89 GET_SETTINGS_VALUES idx 0..47 #####\n");
  899. for (int i = 0; i < 48; i++) {
  900. uint8_t p[2] = { (uint8_t)i, 0 };
  901. sc_cmd(dev, rid, 0x89, p, 2);
  902. }
  903. } else if (arg < argc && strcmp(argv[arg], "seq") == 0) {
  904. int rc = run_seq(dev, argc, argv, arg + 1, 1);
  905. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  906. CFRelease(dev);
  907. return rc;
  908. } else if (arg < argc && strcmp(argv[arg], "seq-noget") == 0) {
  909. int rc = run_seq(dev, argc, argv, arg + 1, 0);
  910. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  911. CFRelease(dev);
  912. return rc;
  913. } else if (arg + 1 < argc &&
  914. strncmp(argv[arg], "rid", 3) ==
  915. 0) { // custom command on one report id
  916. int rid = atoi(argv[arg] + 3);
  917. if (rid < 1 || rid > 255) {
  918. fprintf(stderr, "bad report id: %s\n", argv[arg]);
  919. return 2;
  920. }
  921. uint8_t cmd = (uint8_t)strtol(argv[arg + 1], NULL, 16);
  922. uint8_t pl[62];
  923. int pln = 0;
  924. for (int i = arg + 2; i < argc && pln < 62; i++)
  925. pl[pln++] = (uint8_t)strtol(argv[i], NULL, 16);
  926. sc_cmd(dev, rid, cmd, pl, pln);
  927. } else if (arg <
  928. argc) { // custom single command on both command report ids
  929. uint8_t cmd = (uint8_t)strtol(argv[arg], NULL, 16);
  930. uint8_t pl[62];
  931. int pln = 0;
  932. for (int i = arg + 1; i < argc && pln < 62; i++)
  933. pl[pln++] = (uint8_t)strtol(argv[i], NULL, 16);
  934. for (int rid = 1; rid <= 2; rid++)
  935. sc_cmd(dev, rid, cmd, pl, pln);
  936. } else { // safe read battery on the working channel (report id 2)
  937. int rid = 2;
  938. printf("\n########## string attributes 0xAE (idx 0..15) ##########\n");
  939. for (int a = 0; a < 16; a++) {
  940. uint8_t aa = (uint8_t)a;
  941. sc_cmd(dev, rid, 0xAE, &aa, 1);
  942. }
  943. printf("\n########## 0x83 GET_ATTRIBUTES_VALUES ##########\n");
  944. sc_cmd(dev, rid, 0x83, NULL, 0);
  945. printf("\n########## 0xB4 DONGLE_GET_WIRELESS_STATE (no idx, then idx 0..3) ##########\n");
  946. sc_cmd(dev, rid, 0xB4, NULL, 0);
  947. for (int i = 0; i < 4; i++) {
  948. uint8_t ii = (uint8_t)i;
  949. sc_cmd(dev, rid, 0xB4, &ii, 1);
  950. }
  951. printf("\n########## 0x82 GET_DIGITAL_MAPPINGS (read) ##########\n");
  952. sc_cmd(dev, rid, 0x82, NULL, 0);
  953. }
  954. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  955. CFRelease(dev);
  956. return 0;
  957. }