scmd.c 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  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. printf("%s [%d]:\n ", label, n);
  35. for (int i = 0; i < n; i++) { if (i && i % 32 == 0) printf("\n "); printf("%02X ", p[i]); }
  36. printf("\n ascii: ");
  37. for (int i = 0; i < n; i++) putchar((p[i] >= 32 && p[i] < 127) ? p[i] : '.');
  38. printf("\n");
  39. }
  40. static void print_payload_inline(const uint8_t *p, int n) {
  41. if (n <= 0) {
  42. printf("-");
  43. return;
  44. }
  45. for (int i = 0; i < n; i++) {
  46. if (i) printf(" ");
  47. printf("%02X", p[i]);
  48. }
  49. }
  50. static const char *cmd_name(uint8_t cmd) {
  51. switch (cmd) {
  52. case 0x83: return "GET_ATTRIBUTES_VALUES";
  53. case 0xA1: return "GET_DEVICE_INFO";
  54. case 0xA2: return "TRITON_A2_OBSERVED_PAIRING_RECORD";
  55. case 0xA3: return "TRITON_A3_BOND_EVENT_OR_STATUS";
  56. case 0xAD: return "ENABLE_PAIRING";
  57. case 0xAE: return "GET_STRING_ATTRIBUTE";
  58. case 0xAF: return "SDL_SHARED_RADIO_ERASE_RECORDS";
  59. case 0xB0: return "SDL_SHARED_RADIO_WRITE_RECORD";
  60. case 0xB2: return "SDL_SHARED_DONGLE_DISCONNECT_DEVICE";
  61. case 0xB3: return "SDL_SHARED_DONGLE_COMMIT_DEVICE_CANDIDATE";
  62. case 0xB4: return "SDL_SHARED_DONGLE_GET_WIRELESS_STATE";
  63. case 0xC4: return "SDL_SHARED_DONGLE_GET_CONNECTED_SLOTS";
  64. case 0xEE: return "TRITON_KEYED_VALUE_WRITE";
  65. case 0xEF: return "TRITON_KEYED_VALUE_COMMIT";
  66. default: return NULL;
  67. }
  68. }
  69. static int hid_long(IOHIDDeviceRef d, CFStringRef key, long *out) {
  70. CFTypeRef v = IOHIDDeviceGetProperty(d, key);
  71. if (!v || CFGetTypeID(v) != CFNumberGetTypeID()) return 0;
  72. return CFNumberGetValue((CFNumberRef)v, kCFNumberLongType, out);
  73. }
  74. static void hid_string(IOHIDDeviceRef d, CFStringRef key, char *out, size_t outn) {
  75. if (!outn) return;
  76. out[0] = 0;
  77. CFTypeRef v = IOHIDDeviceGetProperty(d, key);
  78. if (!v) return;
  79. if (CFGetTypeID(v) == CFStringGetTypeID()) {
  80. CFStringGetCString((CFStringRef)v, out, outn, kCFStringEncodingUTF8);
  81. } else if (CFGetTypeID(v) == CFNumberGetTypeID()) {
  82. long x = 0;
  83. if (CFNumberGetValue((CFNumberRef)v, kCFNumberLongType, &x)) snprintf(out, outn, "%ld", x);
  84. }
  85. }
  86. static int reg_long(io_object_t svc, const char *key, long *out) {
  87. CFStringRef k = CFStringCreateWithCString(kCFAllocatorDefault, key, kCFStringEncodingUTF8);
  88. if (!k) return 0;
  89. CFTypeRef v = IORegistryEntryCreateCFProperty(svc, k, kCFAllocatorDefault, 0);
  90. CFRelease(k);
  91. if (!v) return 0;
  92. int ok = 0;
  93. if (CFGetTypeID(v) == CFNumberGetTypeID()) ok = CFNumberGetValue((CFNumberRef)v, kCFNumberLongType, out);
  94. CFRelease(v);
  95. return ok;
  96. }
  97. static void reg_string(io_object_t svc, const char *key, char *out, size_t outn) {
  98. if (!outn) return;
  99. out[0] = 0;
  100. CFStringRef k = CFStringCreateWithCString(kCFAllocatorDefault, key, kCFStringEncodingUTF8);
  101. if (!k) return;
  102. CFTypeRef v = IORegistryEntryCreateCFProperty(svc, k, kCFAllocatorDefault, 0);
  103. CFRelease(k);
  104. if (!v) return;
  105. if (CFGetTypeID(v) == CFStringGetTypeID()) {
  106. CFStringGetCString((CFStringRef)v, out, outn, kCFStringEncodingUTF8);
  107. } else if (CFGetTypeID(v) == CFNumberGetTypeID()) {
  108. long x = 0;
  109. if (CFNumberGetValue((CFNumberRef)v, kCFNumberLongType, &x)) snprintf(out, outn, "%ld", x);
  110. }
  111. CFRelease(v);
  112. }
  113. static void print_device_line(io_object_t svc, IOHIDDeviceRef d) {
  114. long vid = -1, pid = -1, up = -1, usage = -1, loc = -1, maxF = -1;
  115. char serial[128], product[128], manufacturer[128];
  116. serial[0] = product[0] = manufacturer[0] = 0;
  117. reg_long(svc, "VendorID", &vid); if (vid < 0) hid_long(d, CFSTR(kIOHIDVendorIDKey), &vid);
  118. reg_long(svc, "ProductID", &pid); if (pid < 0) hid_long(d, CFSTR(kIOHIDProductIDKey), &pid);
  119. reg_long(svc, "PrimaryUsagePage", &up); if (up < 0) hid_long(d, CFSTR(kIOHIDPrimaryUsagePageKey), &up);
  120. reg_long(svc, "PrimaryUsage", &usage); if (usage < 0) hid_long(d, CFSTR(kIOHIDPrimaryUsageKey), &usage);
  121. reg_long(svc, "LocationID", &loc); if (loc < 0) hid_long(d, CFSTR(kIOHIDLocationIDKey), &loc);
  122. reg_long(svc, "MaxFeatureReportSize", &maxF);
  123. reg_string(svc, "SerialNumber", serial, sizeof serial); if (!serial[0]) hid_string(d, CFSTR(kIOHIDSerialNumberKey), serial, sizeof serial);
  124. reg_string(svc, "Product", product, sizeof product); if (!product[0]) hid_string(d, CFSTR(kIOHIDProductKey), product, sizeof product);
  125. reg_string(svc, "Manufacturer", manufacturer, sizeof manufacturer); if (!manufacturer[0]) hid_string(d, CFSTR(kIOHIDManufacturerKey), manufacturer, sizeof manufacturer);
  126. printf("Valve %04lX:%04lX usagePage=0x%lX usage=0x%lX maxFeature=%ld loc=0x%lX serial=%s product=%s manufacturer=%s\n",
  127. vid, pid, up, usage, maxF, loc, serial[0] ? serial : "-", product[0] ? product : "-",
  128. manufacturer[0] ? manufacturer : "-");
  129. }
  130. static int list_devices(long wantPid) {
  131. CFMutableDictionaryRef m = IOServiceMatching("IOHIDDevice");
  132. io_iterator_t it;
  133. if (IOServiceGetMatchingServices(kIOMainPortDefault, m, &it) != KERN_SUCCESS) {
  134. fprintf(stderr, "match fail\n");
  135. return 1;
  136. }
  137. int n = 0;
  138. io_object_t svc;
  139. while ((svc = IOIteratorNext(it))) {
  140. IOHIDDeviceRef d = IOHIDDeviceCreate(kCFAllocatorDefault, svc);
  141. if (d) {
  142. long vid = -1, pid = -1;
  143. reg_long(svc, "VendorID", &vid); if (vid < 0) hid_long(d, CFSTR(kIOHIDVendorIDKey), &vid);
  144. reg_long(svc, "ProductID", &pid); if (pid < 0) hid_long(d, CFSTR(kIOHIDProductIDKey), &pid);
  145. if (vid == VALVE_VID && (wantPid < 0 || pid == wantPid)) {
  146. print_device_line(svc, d);
  147. n++;
  148. }
  149. CFRelease(d);
  150. }
  151. IOObjectRelease(svc);
  152. }
  153. IOObjectRelease(it);
  154. if (!n) printf("no Valve HID devices found%s\n", wantPid < 0 ? "" : " for requested PID");
  155. return n ? 0 : 1;
  156. }
  157. // Issue one command on report id `rid`: [rid][cmd][len][payload...].
  158. static void sc_cmd_ex(IOHIDDeviceRef d, int rid, uint8_t cmd, const uint8_t *pl, int pln, int do_get) {
  159. uint8_t tx[64]; memset(tx, 0, sizeof tx);
  160. tx[0] = (uint8_t)rid; tx[1] = cmd; tx[2] = (uint8_t)pln;
  161. if (pln > 0) memcpy(tx + 3, pl, pln);
  162. IOReturn rs = IOHIDDeviceSetReport(d, kIOHIDReportTypeFeature, rid, tx, 64);
  163. const char *name = cmd_name(cmd);
  164. if (rs != kIOReturnSuccess) {
  165. printf("\n=== cmd 0x%02X%s%s (payload %d: ", cmd, name ? " " : "", name ? name : "", pln);
  166. print_payload_inline(pl, pln);
  167. printf(") on report id %d [set=0x%08X get=skipped-set-failed] ===\n", rid, rs);
  168. return;
  169. }
  170. if (!do_get) {
  171. printf("\n=== cmd 0x%02X%s%s (payload %d: ", cmd, name ? " " : "", name ? name : "", pln);
  172. print_payload_inline(pl, pln);
  173. printf(") on report id %d [set=0x%08X get=skipped] ===\n", rid, rs);
  174. return;
  175. }
  176. usleep(20000);
  177. uint8_t rx[64]; memset(rx, 0, sizeof rx); rx[0] = (uint8_t)rid;
  178. CFIndex rl = 64;
  179. IOReturn rg = IOHIDDeviceGetReport(d, kIOHIDReportTypeFeature, rid, rx, &rl);
  180. printf("\n=== cmd 0x%02X%s%s (payload %d: ", cmd, name ? " " : "", name ? name : "", pln);
  181. print_payload_inline(pl, pln);
  182. printf(") on report id %d [set=0x%08X get=0x%08X] ===\n", rid, rs, rg);
  183. if (rg == kIOReturnSuccess) dump("reply", rx, (int)rl);
  184. else printf(" (GET failed)\n");
  185. }
  186. static void sc_cmd(IOHIDDeviceRef d, int rid, uint8_t cmd, const uint8_t *pl, int pln) {
  187. sc_cmd_ex(d, rid, cmd, pl, pln, 1);
  188. }
  189. static int hex_value(char c) {
  190. if (c >= '0' && c <= '9') return c - '0';
  191. if (c >= 'a' && c <= 'f') return c - 'a' + 10;
  192. if (c >= 'A' && c <= 'F') return c - 'A' + 10;
  193. return -1;
  194. }
  195. static int parse_hex_bytes(const char *hex, uint8_t *out, int outn) {
  196. int n = 0;
  197. int hi = -1;
  198. for (const char *p = hex; *p; p++) {
  199. if (*p == ':' || *p == '-' || *p == ' ' || *p == '_') continue;
  200. int v = hex_value(*p);
  201. if (v < 0) return -1;
  202. if (hi < 0) {
  203. hi = v;
  204. } else {
  205. if (n >= outn) return -1;
  206. out[n++] = (uint8_t)((hi << 4) | v);
  207. hi = -1;
  208. }
  209. }
  210. if (hi >= 0) return -1;
  211. return n;
  212. }
  213. static int make_serial16(const char *serial, uint8_t out[16]) {
  214. memset(out, 0, 16);
  215. size_t n = strlen(serial);
  216. if (n > 16) return 0;
  217. memcpy(out, serial, n);
  218. return 1;
  219. }
  220. static int triton_keyed_write(IOHIDDeviceRef dev, const char *key, const uint8_t *payload, int payload_len) {
  221. size_t key_len = strlen(key) + 1;
  222. if (key_len + (size_t)payload_len > 61 || key_len > 61) {
  223. fprintf(stderr, "keyed payload too long for 64-byte feature report\n");
  224. return 2;
  225. }
  226. uint8_t ee[61];
  227. memcpy(ee, key, key_len);
  228. if (payload_len > 0) memcpy(ee + key_len, payload, payload_len);
  229. uint8_t ef[61];
  230. memcpy(ef, key, key_len);
  231. printf("Triton keyed write key=\"%s\" payload=", key);
  232. print_payload_inline(payload, payload_len);
  233. printf("\n");
  234. sc_cmd_ex(dev, 2, 0xEE, ee, (int)key_len + payload_len, 0);
  235. usleep(20000);
  236. sc_cmd_ex(dev, 2, 0xEF, ef, (int)key_len, 0);
  237. return 0;
  238. }
  239. static int triton_bond_write(IOHIDDeviceRef dev, int slot, const char *key_hex, const char *serial) {
  240. if (slot < 0 || slot > 1) {
  241. fprintf(stderr, "triton-bond slot must be 0 or 1\n");
  242. return 2;
  243. }
  244. uint8_t payload[24];
  245. int keyn = parse_hex_bytes(key_hex, payload, 8);
  246. if (keyn != 8) {
  247. fprintf(stderr, "triton-bond key must be exactly 8 bytes / 16 hex digits\n");
  248. return 2;
  249. }
  250. if (!make_serial16(serial, payload + 8)) {
  251. fprintf(stderr, "controller serial must be at most 16 ASCII bytes\n");
  252. return 2;
  253. }
  254. const char *key = slot == 0 ? "esb/bond" : "esb/bond_2";
  255. printf("note: Steam uses this keyed path for controller-side bond storage; real pucks reject it.\n");
  256. return triton_keyed_write(dev, key, payload, sizeof payload);
  257. }
  258. static int parse_triton_transport(const char *arg, uint8_t *out) {
  259. if (strcmp(arg, "slot0") == 0 || strcmp(arg, "0") == 0) { *out = 2; return 1; }
  260. if (strcmp(arg, "slot1") == 0 || strcmp(arg, "1") == 0) { *out = 3; return 1; }
  261. if (strcmp(arg, "slot2") == 0 || strcmp(arg, "2") == 0) { *out = 0; return 1; }
  262. char *end = NULL;
  263. long raw = strtol(arg, &end, 0);
  264. if (end && *end == 0 && raw >= 0 && raw <= 255) { *out = (uint8_t)raw; return 1; }
  265. return 0;
  266. }
  267. static int triton_transport_write(IOHIDDeviceRef dev, const char *arg) {
  268. uint8_t value = 0;
  269. if (!parse_triton_transport(arg, &value)) {
  270. fprintf(stderr, "triton-transport expects slot0, slot1, slot2, or raw byte 0..255\n");
  271. return 2;
  272. }
  273. return triton_keyed_write(dev, "user/wireless_transport", &value, 1);
  274. }
  275. static void print_a3_decode(const uint8_t *rx, int rl) {
  276. if (rl < 3 || rx[1] != 0xA3) {
  277. printf(" not an A3 reply\n");
  278. return;
  279. }
  280. int len = rx[2];
  281. if (len != 0x18 || rl < 27) {
  282. printf(" A3 len=0x%02X (no bond payload)\n", len);
  283. return;
  284. }
  285. const uint8_t *key = rx + 3;
  286. const uint8_t *serial = rx + 11;
  287. uint32_t w0 = (uint32_t)key[0] | ((uint32_t)key[1] << 8) | ((uint32_t)key[2] << 16) | ((uint32_t)key[3] << 24);
  288. uint32_t w1 = (uint32_t)key[4] | ((uint32_t)key[5] << 8) | ((uint32_t)key[6] << 16) | ((uint32_t)key[7] << 24);
  289. printf(" A3 bond key=");
  290. for (int i = 0; i < 8; i++) printf("%s%02X", i ? " " : "", key[i]);
  291. printf(" steam_words=%X_%X serial=", w0, w1);
  292. for (int i = 0; i < 16 && serial[i]; i++) putchar((serial[i] >= 32 && serial[i] < 127) ? serial[i] : '.');
  293. printf("\n");
  294. }
  295. static void print_a3_decode_any(const uint8_t *rx, int rl) {
  296. if (rl >= 2 && rx[0] == 0xA3) {
  297. uint8_t shifted[64];
  298. int n = rl + 1;
  299. if (n > (int)sizeof shifted) n = (int)sizeof shifted;
  300. memset(shifted, 0, sizeof shifted);
  301. shifted[0] = 2;
  302. memcpy(shifted + 1, rx, n - 1);
  303. print_a3_decode(shifted, n);
  304. return;
  305. }
  306. print_a3_decode(rx, rl);
  307. }
  308. static int a3_watch(IOHIDDeviceRef dev, int count, int delay_ms) {
  309. if (count <= 0) count = 20;
  310. if (delay_ms < 0) delay_ms = 100;
  311. for (int i = 0; i < count; i++) {
  312. uint8_t tx[64]; memset(tx, 0, sizeof tx);
  313. tx[0] = 2; tx[1] = 0xA3; tx[2] = 0;
  314. IOReturn rs = IOHIDDeviceSetReport(dev, kIOHIDReportTypeFeature, 2, tx, 64);
  315. usleep(20000);
  316. uint8_t rx[64]; memset(rx, 0, sizeof rx); rx[0] = 2;
  317. CFIndex rl = 64;
  318. IOReturn rg = kIOReturnError;
  319. if (rs == kIOReturnSuccess) rg = IOHIDDeviceGetReport(dev, kIOHIDReportTypeFeature, 2, rx, &rl);
  320. printf("[%02d] A3 set=0x%08X get=0x%08X", i + 1, rs, rg);
  321. if (rg == kIOReturnSuccess && rl >= 3) printf(" reply=%02X %02X %02X", rx[0], rx[1], rx[2]);
  322. printf("\n");
  323. if (rg == kIOReturnSuccess) print_a3_decode(rx, (int)rl);
  324. if (delay_ms > 0 && i + 1 < count) usleep((useconds_t)delay_ms * 1000);
  325. }
  326. return 0;
  327. }
  328. static void a3_variant(IOHIDDeviceRef dev, const char *label, int include_rid, int set_len, int get_len, int seed_rid) {
  329. uint8_t tx[64]; memset(tx, 0, sizeof tx);
  330. if (include_rid) {
  331. tx[0] = 2;
  332. tx[1] = 0xA3;
  333. tx[2] = 0;
  334. } else {
  335. tx[0] = 0xA3;
  336. tx[1] = 0;
  337. }
  338. IOReturn rs = IOHIDDeviceSetReport(dev, kIOHIDReportTypeFeature, 2, tx, set_len);
  339. usleep(20000);
  340. uint8_t rx[64]; memset(rx, 0, sizeof rx);
  341. if (seed_rid) rx[0] = 2;
  342. CFIndex rl = get_len;
  343. IOReturn rg = kIOReturnError;
  344. if (rs == kIOReturnSuccess) rg = IOHIDDeviceGetReport(dev, kIOHIDReportTypeFeature, 2, rx, &rl);
  345. printf("\n=== A3 variant %s setLen=%d getLen=%d includeRid=%d seedRid=%d set=0x%08X get=0x%08X rl=%ld ===\n",
  346. label, set_len, get_len, include_rid, seed_rid, rs, rg, (long)rl);
  347. if (rg == kIOReturnSuccess) {
  348. dump("reply", rx, (int)rl);
  349. print_a3_decode_any(rx, (int)rl);
  350. }
  351. }
  352. static int a3_variants(IOHIDDeviceRef dev) {
  353. a3_variant(dev, "steam-short", 0, 2, 0x1a, 0);
  354. a3_variant(dev, "steam-short-seeded", 0, 2, 0x1a, 1);
  355. a3_variant(dev, "rid-short", 1, 3, 0x1b, 1);
  356. a3_variant(dev, "rid-64", 1, 64, 64, 1);
  357. return 0;
  358. }
  359. static int run_seq(IOHIDDeviceRef dev, int argc, char **argv, int start, int do_get) {
  360. int i = start;
  361. while (i < argc) {
  362. if (strcmp(argv[i], "--") == 0) { i++; continue; }
  363. if (strncmp(argv[i], "rid", 3) != 0) {
  364. fprintf(stderr, "seq expects ridN before each command, got: %s\n", argv[i]);
  365. return 2;
  366. }
  367. int rid = atoi(argv[i] + 3);
  368. if (rid < 1 || rid > 255) { fprintf(stderr, "bad report id: %s\n", argv[i]); return 2; }
  369. i++;
  370. if (i >= argc) { fprintf(stderr, "seq missing cmd after rid%d\n", rid); return 2; }
  371. uint8_t cmd = (uint8_t)strtol(argv[i++], NULL, 16);
  372. uint8_t pl[62]; int pln = 0;
  373. while (i < argc && strcmp(argv[i], "--") != 0) {
  374. if (strncmp(argv[i], "rid", 3) == 0) break;
  375. if (pln >= 62) { fprintf(stderr, "payload too long for cmd 0x%02X\n", cmd); return 2; }
  376. pl[pln++] = (uint8_t)strtol(argv[i++], NULL, 16);
  377. }
  378. sc_cmd_ex(dev, rid, cmd, pl, pln, do_get);
  379. usleep(20000);
  380. }
  381. return 0;
  382. }
  383. int main(int argc, char **argv) {
  384. if (argc < 2) { fprintf(stderr, "usage: %s list [pidHex] | <pidHex> [--serial SERIAL] [cmdHex payloadBytes...]\n", argv[0]); return 2; }
  385. if (strcmp(argv[1], "list") == 0) {
  386. long wantPid = argc >= 3 ? strtol(argv[2], NULL, 16) : -1;
  387. return list_devices(wantPid);
  388. }
  389. long wantPid = strtol(argv[1], NULL, 16);
  390. const char *wantSerial = NULL;
  391. long wantLoc = -1;
  392. long wantUp = -1;
  393. int wantIdx = -1;
  394. int matchIdx = 0;
  395. int arg = 2;
  396. while (arg < argc) {
  397. if (strcmp(argv[arg], "--serial") == 0) {
  398. if (arg + 1 >= argc) { fprintf(stderr, "--serial needs a value\n"); return 2; }
  399. wantSerial = argv[arg + 1];
  400. arg += 2;
  401. } else if (strncmp(argv[arg], "--serial=", 9) == 0) {
  402. wantSerial = argv[arg] + 9;
  403. arg++;
  404. } else if (strcmp(argv[arg], "--loc") == 0) {
  405. if (arg + 1 >= argc) { fprintf(stderr, "--loc needs a value\n"); return 2; }
  406. wantLoc = strtol(argv[arg + 1], NULL, 0);
  407. arg += 2;
  408. } else if (strncmp(argv[arg], "--loc=", 6) == 0) {
  409. wantLoc = strtol(argv[arg] + 6, NULL, 0);
  410. arg++;
  411. } else if (strcmp(argv[arg], "--up") == 0) {
  412. if (arg + 1 >= argc) { fprintf(stderr, "--up needs a value\n"); return 2; }
  413. wantUp = strtol(argv[arg + 1], NULL, 0);
  414. arg += 2;
  415. } else if (strcmp(argv[arg], "--idx") == 0) {
  416. if (arg + 1 >= argc) { fprintf(stderr, "--idx needs a value\n"); return 2; }
  417. wantIdx = strtol(argv[arg + 1], NULL, 0);
  418. arg += 2;
  419. } else {
  420. break;
  421. }
  422. }
  423. CFMutableDictionaryRef m = IOServiceMatching("IOHIDDevice");
  424. io_iterator_t it;
  425. if (IOServiceGetMatchingServices(kIOMainPortDefault, m, &it) != KERN_SUCCESS) { fprintf(stderr, "match fail\n"); return 1; }
  426. IOHIDDeviceRef dev = NULL; io_object_t svc;
  427. while ((svc = IOIteratorNext(it))) {
  428. CFTypeRef vid = IORegistryEntryCreateCFProperty(svc, CFSTR("VendorID"), kCFAllocatorDefault, 0);
  429. CFTypeRef pid = IORegistryEntryCreateCFProperty(svc, CFSTR("ProductID"), kCFAllocatorDefault, 0);
  430. long v = -1, p = -1;
  431. if (vid) { CFNumberGetValue((CFNumberRef)vid, kCFNumberLongType, &v); CFRelease(vid); }
  432. if (pid) { CFNumberGetValue((CFNumberRef)pid, kCFNumberLongType, &p); CFRelease(pid); }
  433. if (v == VALVE_VID && p == wantPid) {
  434. IOHIDDeviceRef d = IOHIDDeviceCreate(kCFAllocatorDefault, svc);
  435. long up = -1, maxF = -1, loc = -1;
  436. if (d) {
  437. reg_long(svc, "PrimaryUsagePage", &up);
  438. if (up < 0) {
  439. CFTypeRef u = IOHIDDeviceGetProperty(d, CFSTR(kIOHIDPrimaryUsagePageKey));
  440. if (u) CFNumberGetValue((CFNumberRef)u, kCFNumberLongType, &up);
  441. }
  442. reg_long(svc, "MaxFeatureReportSize", &maxF);
  443. reg_long(svc, "LocationID", &loc);
  444. }
  445. char serial[128] = "";
  446. char product[128] = "";
  447. if (d) {
  448. reg_string(svc, "SerialNumber", serial, sizeof serial);
  449. if (!serial[0]) hid_string(d, CFSTR(kIOHIDSerialNumberKey), serial, sizeof serial);
  450. reg_string(svc, "Product", product, sizeof product);
  451. if (!product[0]) hid_string(d, CFSTR(kIOHIDProductKey), product, sizeof product);
  452. }
  453. if (wantSerial && strcmp(wantSerial, serial) != 0) {
  454. if (d) CFRelease(d);
  455. IOObjectRelease(svc);
  456. continue;
  457. }
  458. if (wantLoc >= 0 && loc != wantLoc) {
  459. if (d) CFRelease(d);
  460. IOObjectRelease(svc);
  461. continue;
  462. }
  463. if (wantUp >= 0 && up != wantUp) {
  464. if (d) CFRelease(d);
  465. IOObjectRelease(svc);
  466. continue;
  467. }
  468. if (wantIdx >= 0 && matchIdx++ != wantIdx) {
  469. if (d) CFRelease(d);
  470. IOObjectRelease(svc);
  471. continue;
  472. }
  473. int command_capable = (up == 0xFF00) || (maxF >= 64);
  474. IOReturn ro = d && command_capable ? IOHIDDeviceOpen(d, kIOHIDOptionsTypeNone) : kIOReturnNotFound;
  475. if (d && command_capable && ro == kIOReturnSuccess) {
  476. printf("opened Valve %04lX:%04lX node (usagePage=0x%lX maxFeature=%ld serial=%s product=%s)\n",
  477. (long)VALVE_VID, wantPid, up, maxF, serial[0] ? serial : "-", product[0] ? product : "-");
  478. dev = d; IOObjectRelease(svc); break;
  479. }
  480. if (d && command_capable && (wantSerial || wantLoc >= 0)) {
  481. fprintf(stderr, "matched Valve %04lX:%04lX usagePage=0x%lX maxFeature=%ld loc=0x%lX serial=%s but open failed: 0x%08X\n",
  482. (long)VALVE_VID, wantPid, up, maxF, loc, serial[0] ? serial : "-", ro);
  483. }
  484. if (d) CFRelease(d);
  485. }
  486. IOObjectRelease(svc);
  487. }
  488. IOObjectRelease(it);
  489. if (!dev) {
  490. if (wantSerial || wantLoc >= 0) fprintf(stderr, "no openable Valve %04lX command-capable interface matching serial=%s loc=0x%lX (quit Steam? wrong device?)\n", wantPid, wantSerial ? wantSerial : "-", wantLoc);
  491. else fprintf(stderr, "no openable Valve %04lX command-capable interface (quit Steam? add --serial/--loc?)\n", wantPid);
  492. return 1;
  493. }
  494. if (arg < argc && strcmp(argv[arg], "slotwrite") == 0) {
  495. // Single-session dongle slot write: 0xAD 01 (enter pairing mode) -> 0xA2 <24-byte record>
  496. // -> 0xAD 00, all on the SAME open handle (Steam keeps one session; pairing mode resets on
  497. // close). record = [8-byte uuids][16-byte serial]; zeros-serial clears the matching slot.
  498. // first arg = report id (the ALT channel is report id 3); rest = 24-byte record
  499. int rid = (arg + 1 < argc) ? atoi(argv[arg + 1]) : 3;
  500. uint8_t rec[24]; memset(rec, 0, sizeof rec);
  501. int n = 0;
  502. for (int i = arg + 2; i < argc && n < 24; i++) rec[n++] = (uint8_t)strtol(argv[i], NULL, 16);
  503. uint8_t on[2] = {0x01, 0x00}, off[1] = {0x00};
  504. printf("# slotwrite rid%d: AD 01 -> A2 <24> -> AD 00 (one session)\n", rid);
  505. sc_cmd(dev, rid, 0xAD, on, 2);
  506. sc_cmd(dev, rid, 0xA2, rec, 24);
  507. sc_cmd(dev, rid, 0xAD, off, 1);
  508. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone); CFRelease(dev); return 0;
  509. } else if (arg < argc && strcmp(argv[arg], "pair") == 0) {
  510. // Steamless pairing (firmware-derived): 0xAD [mode=1][channel] enters RF pairing mode;
  511. // the puck then runs the pairing handshake autonomously. Channel 0 => firmware default
  512. // (0x3c). Put the controller in pairing via its key-chord, then we poll 0xA3 for the bond.
  513. uint8_t ch = (arg + 1 < argc) ? (uint8_t)strtol(argv[arg + 1], NULL, 16) : 0x00;
  514. uint8_t start[2] = { 0x01, ch };
  515. printf("# pair: entering pairing mode (0xAD 01 %02X).\n", ch);
  516. printf("# >>> Now put the controller into pairing via its key-chord. Polling 0xA3...\n");
  517. sc_cmd(dev, 2, 0xAD, start, 2);
  518. int rc = a3_watch(dev, 120, 200); // ~24s: watch status for the controller to bond
  519. uint8_t stop[1] = { 0x00 };
  520. printf("# pair: exiting pairing mode (0xAD 00).\n");
  521. sc_cmd(dev, 2, 0xAD, stop, 1);
  522. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone); CFRelease(dev); return rc;
  523. } else if (arg < argc && strcmp(argv[arg], "stop") == 0) {
  524. uint8_t stop[1] = { 0x00 }; // exit pairing mode
  525. sc_cmd(dev, 2, 0xAD, stop, 1);
  526. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone); CFRelease(dev); return 0;
  527. } else if (arg < argc && strcmp(argv[arg], "info") == 0) {
  528. // Read identity + bond status without changing anything.
  529. printf("\n##### 0x83 attributes #####\n"); sc_cmd(dev, 2, 0x83, NULL, 0);
  530. printf("\n##### 0xAE strings (idx 0..7) #####\n"); for (int a = 0; a < 8; a++) { uint8_t aa = (uint8_t)a; sc_cmd(dev, 2, 0xAE, &aa, 1); }
  531. printf("\n##### 0xB4 wireless state #####\n"); sc_cmd(dev, 2, 0xB4, NULL, 0);
  532. printf("\n##### 0xA3 status / bond #####\n"); sc_cmd(dev, 2, 0xA3, NULL, 0);
  533. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone); CFRelease(dev); return 0;
  534. } else if (arg < argc && strcmp(argv[arg], "a3watch") == 0) {
  535. int count = (arg + 1 < argc) ? atoi(argv[arg + 1]) : 20;
  536. int delay_ms = (arg + 2 < argc) ? atoi(argv[arg + 2]) : 100;
  537. int rc = a3_watch(dev, count, delay_ms);
  538. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  539. CFRelease(dev);
  540. return rc;
  541. } else if (arg < argc && strcmp(argv[arg], "a3variants") == 0) {
  542. int rc = a3_variants(dev);
  543. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  544. CFRelease(dev);
  545. return rc;
  546. } else if (arg < argc && strcmp(argv[arg], "triton-bond") == 0) {
  547. if (arg + 3 >= argc) {
  548. fprintf(stderr, "usage: %s <pidHex> [--serial S] triton-bond <0|1> <keyHex16> <controllerSerial>\n", argv[0]);
  549. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  550. CFRelease(dev);
  551. return 2;
  552. }
  553. int slot = atoi(argv[arg + 1]);
  554. int rc = triton_bond_write(dev, slot, argv[arg + 2], argv[arg + 3]);
  555. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  556. CFRelease(dev);
  557. return rc;
  558. } else if (arg < argc && strcmp(argv[arg], "triton-transport") == 0) {
  559. if (arg + 1 >= argc) {
  560. fprintf(stderr, "usage: %s <pidHex> [--serial S] triton-transport <slot0|slot1|slot2|rawByte>\n", argv[0]);
  561. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  562. CFRelease(dev);
  563. return 2;
  564. }
  565. int rc = triton_transport_write(dev, argv[arg + 1]);
  566. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  567. CFRelease(dev);
  568. return rc;
  569. } else if (arg < argc && strcmp(argv[arg], "find") == 0) { // probe all non-destructive opcodes for bond/serial data
  570. // BLOCKLIST = known state-changing/destructive opcodes; never sent here.
  571. static const int block[] = {0x80,0x81,0x85,0x86,0x87,0x88,0x8D,0x8E,0x8F,0x9F,
  572. 0xAD,0xAF,0xB0,0xB2,0xB3,0xEB,-1};
  573. int rid = 2;
  574. for (int cmd = 0x82; cmd <= 0xFE; cmd++) {
  575. int blocked = 0; for (const int *b = block; *b >= 0; b++) if (*b == cmd) blocked = 1;
  576. if (blocked) continue;
  577. // send with empty payload; report only if SET succeeded (supported opcode)
  578. uint8_t tx[64]; memset(tx, 0, sizeof tx); tx[0] = rid; tx[1] = (uint8_t)cmd; tx[2] = 0;
  579. IOReturn rs = IOHIDDeviceSetReport(dev, kIOHIDReportTypeFeature, rid, tx, 64);
  580. if (rs != kIOReturnSuccess) continue; // unsupported -> stall, skip silently
  581. usleep(15000);
  582. uint8_t rx[64]; memset(rx, 0, sizeof rx); rx[0] = rid; CFIndex rl = 64;
  583. IOReturn rg = IOHIDDeviceGetReport(dev, kIOHIDReportTypeFeature, rid, rx, &rl);
  584. if (rg == kIOReturnSuccess && rx[1] == cmd) { // echoed -> real reply
  585. char lbl[24]; snprintf(lbl, sizeof lbl, "SUPPORTED cmd 0x%02X", cmd);
  586. dump(lbl, rx, (int)rl);
  587. }
  588. }
  589. } else if (arg < argc && strcmp(argv[arg], "labels") == 0) { // SAFE read-only label/settings sweep
  590. int rid = 2;
  591. printf("\n##### 0x84 GET_ATTRIBUTE_LABEL idx 0..15 #####\n");
  592. for (int i = 0; i < 16; i++) { uint8_t p[2] = {(uint8_t)i, 0}; sc_cmd(dev, rid, 0x84, p, 2); }
  593. printf("\n##### 0x8A GET_SETTING_LABEL idx 0..47 #####\n");
  594. for (int i = 0; i < 48; i++) { uint8_t p[2] = {(uint8_t)i, 0}; sc_cmd(dev, rid, 0x8A, p, 2); }
  595. printf("\n##### 0x89 GET_SETTINGS_VALUES idx 0..47 #####\n");
  596. for (int i = 0; i < 48; i++) { uint8_t p[2] = {(uint8_t)i, 0}; sc_cmd(dev, rid, 0x89, p, 2); }
  597. } else if (arg < argc && strcmp(argv[arg], "seq") == 0) {
  598. int rc = run_seq(dev, argc, argv, arg + 1, 1);
  599. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  600. CFRelease(dev);
  601. return rc;
  602. } else if (arg < argc && strcmp(argv[arg], "seq-noget") == 0) {
  603. int rc = run_seq(dev, argc, argv, arg + 1, 0);
  604. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  605. CFRelease(dev);
  606. return rc;
  607. } else if (arg + 1 < argc && strncmp(argv[arg], "rid", 3) == 0) { // custom command on one report id
  608. int rid = atoi(argv[arg] + 3);
  609. if (rid < 1 || rid > 255) { fprintf(stderr, "bad report id: %s\n", argv[arg]); return 2; }
  610. uint8_t cmd = (uint8_t)strtol(argv[arg + 1], NULL, 16);
  611. uint8_t pl[62]; int pln = 0;
  612. for (int i = arg + 2; i < argc && pln < 62; i++) pl[pln++] = (uint8_t)strtol(argv[i], NULL, 16);
  613. sc_cmd(dev, rid, cmd, pl, pln);
  614. } else if (arg < argc) { // custom single command on both command report ids
  615. uint8_t cmd = (uint8_t)strtol(argv[arg], NULL, 16);
  616. uint8_t pl[62]; int pln = 0;
  617. for (int i = arg + 1; i < argc && pln < 62; i++) pl[pln++] = (uint8_t)strtol(argv[i], NULL, 16);
  618. for (int rid = 1; rid <= 2; rid++) sc_cmd(dev, rid, cmd, pl, pln);
  619. } else { // safe read battery on the working channel (report id 2)
  620. int rid = 2;
  621. printf("\n########## string attributes 0xAE (idx 0..15) ##########\n");
  622. for (int a = 0; a < 16; a++) { uint8_t aa = (uint8_t)a; sc_cmd(dev, rid, 0xAE, &aa, 1); }
  623. printf("\n########## 0x83 GET_ATTRIBUTES_VALUES ##########\n");
  624. sc_cmd(dev, rid, 0x83, NULL, 0);
  625. printf("\n########## 0xB4 DONGLE_GET_WIRELESS_STATE (no idx, then idx 0..3) ##########\n");
  626. sc_cmd(dev, rid, 0xB4, NULL, 0);
  627. for (int i = 0; i < 4; i++) { uint8_t ii = (uint8_t)i; sc_cmd(dev, rid, 0xB4, &ii, 1); }
  628. printf("\n########## 0x82 GET_DIGITAL_MAPPINGS (read) ##########\n");
  629. sc_cmd(dev, rid, 0x82, NULL, 0);
  630. }
  631. IOHIDDeviceClose(dev, kIOHIDOptionsTypeNone);
  632. CFRelease(dev);
  633. return 0;
  634. }