From 08c26e2dc0cc77ab2b430c5657f7a8bd80af6ffc Mon Sep 17 00:00:00 2001 From: alfa <87366642+alfaoz@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:09:09 +0100 Subject: [PATCH 1/5] add macOS support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit replace libudev with IOKit/CoreFoundation for device monitoring, swap hidapi-hidraw for the generic hidapi (IOKit backend), handle macOS-specific HID report ID stripping, and add nanosleep compat for thrd_sleep. all changes behind #ifdef __APPLE__ — linux code is untouched. tested on macOS sequoia (arm64) over both USB and bluetooth. --- main.c | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++-- meson.build | 31 +++++++---- 2 files changed, 163 insertions(+), 14 deletions(-) diff --git a/main.c b/main.c index 9c92b4b..62617dc 100644 --- a/main.c +++ b/main.c @@ -14,16 +14,31 @@ #include #include #include -#include #include +#include +#ifdef __APPLE__ +#include +#include +#else +#include #include +#include +#endif +#ifdef __APPLE__ +#include +#else #include -#include +#endif #include "crc32.h" +/* Portable thrd_sleep replacement using nanosleep on macOS */ +#ifdef __APPLE__ +#define thrd_sleep(ts, rem) nanosleep((ts), (rem)) +#endif + #define DS_VENDOR_ID 0x054c #define DS_PRODUCT_ID 0x0ce6 #define DS_EDGE_PRODUCT_ID 0x0df2 @@ -401,8 +416,11 @@ static bool dualsense_init(struct dualsense *ds, const char *serial) wchar_t *serial_number = dev->serial_number; - if (wcslen(serial_number) != 17) { + if (!serial_number || wcslen(serial_number) != 17) { +#ifndef __APPLE__ + /* On macOS USB, serial number is often not exposed via IOKit — this is normal */ fprintf(stderr, "Invalid device serial number: %ls\n", serial_number); +#endif // Let's just fake serial number as everything will still work serial_number = L"00:00:00:00:00:00"; } @@ -475,12 +493,29 @@ static int command_battery(struct dualsense *ds) if (!ds->bt && data[0] == DS_INPUT_REPORT_USB && res == DS_INPUT_REPORT_USB_SIZE) { ds_report = (struct dualsense_input_report *)&data[1]; +#ifdef __APPLE__ + } else if (!ds->bt && res == DS_INPUT_REPORT_USB_SIZE - 1) { + /* macOS IOKit strips report ID on USB */ + ds_report = (struct dualsense_input_report *)&data[0]; + } else if (ds->bt && res == DS_INPUT_REPORT_BT_SIZE - 4 - 1) { + /* macOS IOKit strips report ID on BT; skip 1 byte (tag area) */ + ds_report = (struct dualsense_input_report *)&data[1]; + } else if (ds->bt && res == DS_INPUT_REPORT_BT_SIZE - 1) { + /* macOS IOKit strips report ID on BT, but keeps CRC */ + ds_report = (struct dualsense_input_report *)&data[1]; +#endif } else if (ds->bt && data[0] == DS_INPUT_REPORT_BT && res == DS_INPUT_REPORT_BT_SIZE) { /* Last 4 bytes of input report contain crc32 */ /* uint32_t report_crc = *(uint32_t*)&data[res - 4]; */ ds_report = (struct dualsense_input_report *)&data[2]; } else { - fprintf(stderr, "Unhandled report ID %d\n", (int)data[0]); +#ifdef __APPLE__ + if (ds->bt && res <= 10) { + fprintf(stderr, "Battery reading requires USB connection on macOS (Bluetooth only provides simple reports)\n"); + return 2; + } +#endif + fprintf(stderr, "Unhandled report ID %d (size %d, bt=%d)\n", (int)data[0], res, ds->bt); return 3; } @@ -1066,6 +1101,108 @@ static void run_sh_command(const char *command, const char *serial_number) } } +#ifdef __APPLE__ +/* macOS: IOKit-based device monitoring */ + +static void get_serial_from_hid_device(IOHIDDeviceRef device, char serial_number[18]) +{ + CFStringRef serial = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDSerialNumberKey)); + if (serial && CFGetTypeID(serial) == CFStringGetTypeID()) { + char buf[64] = {0}; + CFStringGetCString(serial, buf, sizeof(buf), kCFStringEncodingUTF8); + /* Serial may come as "aa-bb-cc-dd-ee-ff" or "aa:bb:cc:dd:ee:ff" */ + size_t len = strlen(buf); + if (len == 17) { + /* Replace dashes with colons if needed, uppercase */ + for (int i = 0; i < 17; i++) { + if (buf[i] == '-') buf[i] = ':'; + serial_number[i] = toupper(buf[i]); + } + serial_number[17] = '\0'; + return; + } + } + strncpy(serial_number, "00:00:00:00:00:00", 18); +} + +static void iokit_device_added(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) +{ + (void)context; (void)result; (void)sender; + char serial_number[18] = "00:00:00:00:00:00"; + get_serial_from_hid_device(device, serial_number); + if (sh_command_add) { + run_sh_command(sh_command_add, serial_number); + } +} + +static void iokit_device_removed(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) +{ + (void)context; (void)result; (void)sender; + char serial_number[18] = "00:00:00:00:00:00"; + get_serial_from_hid_device(device, serial_number); + if (sh_command_remove) { + run_sh_command(sh_command_remove, serial_number); + } +} + +static int command_monitor(void) +{ + IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + if (!manager) { + fprintf(stderr, "Failed to create IOHIDManager\n"); + return 1; + } + + /* Match DualSense and DualSense Edge by vendor/product ID */ + CFNumberRef vendor_id = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &(int){DS_VENDOR_ID}); + CFNumberRef product_id_ds = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &(int){DS_PRODUCT_ID}); + CFNumberRef product_id_edge = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &(int){DS_EDGE_PRODUCT_ID}); + + CFDictionaryRef match_ds = CFDictionaryCreate(kCFAllocatorDefault, + (const void *[]){ CFSTR(kIOHIDVendorIDKey), CFSTR(kIOHIDProductIDKey) }, + (const void *[]){ vendor_id, product_id_ds }, + 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + + CFDictionaryRef match_edge = CFDictionaryCreate(kCFAllocatorDefault, + (const void *[]){ CFSTR(kIOHIDVendorIDKey), CFSTR(kIOHIDProductIDKey) }, + (const void *[]){ vendor_id, product_id_edge }, + 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + + CFDictionaryRef matches[] = { match_ds, match_edge }; + CFArrayRef match_array = CFArrayCreate(kCFAllocatorDefault, (const void **)matches, 2, &kCFTypeArrayCallBacks); + + IOHIDManagerSetDeviceMatchingMultiple(manager, match_array); + + IOHIDManagerRegisterDeviceMatchingCallback(manager, iokit_device_added, NULL); + IOHIDManagerRegisterDeviceRemovalCallback(manager, iokit_device_removed, NULL); + + IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + + IOReturn ret = IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone); + if (ret != kIOReturnSuccess) { + fprintf(stderr, "Failed to open IOHIDManager: 0x%x\n", ret); + CFRelease(manager); + return 1; + } + + /* Run the event loop — blocks until interrupted */ + CFRunLoopRun(); + + IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); + CFRelease(match_array); + CFRelease(match_edge); + CFRelease(match_ds); + CFRelease(product_id_edge); + CFRelease(product_id_ds); + CFRelease(vendor_id); + CFRelease(manager); + + return 0; +} + +#else +/* Linux: udev-based device monitoring */ + static uint32_t read_file_hex(const char *path) { uint32_t out = 0; @@ -1201,6 +1338,7 @@ static int command_monitor(void) return 0; } +#endif /* __APPLE__ */ static int dualsense_send_fw_feature(struct dualsense *ds, uint8_t *buf) { diff --git a/meson.build b/meson.build index 4e95fd8..ef93e22 100644 --- a/meson.build +++ b/meson.build @@ -10,20 +10,31 @@ project( ], ) -add_project_arguments( - [ - '-Wl,--exclude-libs=ALL', - '-DDUALSENSECTL_VERSION="@0@"'.format(meson.project_version()), - ], - language: 'c', - ) +cc = meson.get_compiler('c') +extra_args = ['-DDUALSENSECTL_VERSION="@0@"'.format(meson.project_version())] +deps = [] + +if host_machine.system() == 'darwin' + # macOS: use generic hidapi (IOKit backend), no udev + hidapi = dependency('hidapi') + deps += hidapi + + # Link IOKit and CoreFoundation frameworks for the monitor command + iokit = dependency('appleframeworks', modules: ['IOKit', 'CoreFoundation']) + deps += iokit +else + # Linux: use hidapi-hidraw backend + libudev + extra_args += '-Wl,--exclude-libs=ALL' + hidapi_hidraw = dependency('hidapi-hidraw') + udev = dependency('libudev') + deps += [hidapi_hidraw, udev] +endif -udev = dependency('libudev') -hidapi_hidraw = dependency('hidapi-hidraw') +add_project_arguments(extra_args, language: 'c') executable( 'dualsensectl', ['main.c'], - dependencies: [udev, hidapi_hidraw], + dependencies: deps, install: true, ) From f907d2fda6e255112fe40a2ebb2e904009568fe6 Mon Sep 17 00:00:00 2001 From: alfa <87366642+alfaoz@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:10:50 +0100 Subject: [PATCH 2/5] address review feedback - fix potential NULL dereference in serial number fprintf - handle macOS report ID stripping in update command (not just battery) - fix CF object leak on IOHIDManagerOpen error path - use bracket syntax for meson array appends --- main.c | 20 ++++++++++++++++++-- meson.build | 6 +++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/main.c b/main.c index 62617dc..4894152 100644 --- a/main.c +++ b/main.c @@ -417,9 +417,14 @@ static bool dualsense_init(struct dualsense *ds, const char *serial) wchar_t *serial_number = dev->serial_number; if (!serial_number || wcslen(serial_number) != 17) { -#ifndef __APPLE__ +#ifdef __APPLE__ /* On macOS USB, serial number is often not exposed via IOKit — this is normal */ - fprintf(stderr, "Invalid device serial number: %ls\n", serial_number); +#else + if (serial_number) { + fprintf(stderr, "Invalid device serial number: %ls\n", serial_number); + } else { + fprintf(stderr, "Missing device serial number\n"); + } #endif // Let's just fake serial number as everything will still work serial_number = L"00:00:00:00:00:00"; @@ -1181,6 +1186,12 @@ static int command_monitor(void) IOReturn ret = IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone); if (ret != kIOReturnSuccess) { fprintf(stderr, "Failed to open IOHIDManager: 0x%x\n", ret); + CFRelease(match_array); + CFRelease(match_edge); + CFRelease(match_ds); + CFRelease(product_id_edge); + CFRelease(product_id_ds); + CFRelease(vendor_id); CFRelease(manager); return 1; } @@ -1594,6 +1605,11 @@ static int command_update(struct dualsense *ds, const char *path) if (data[0] == DS_INPUT_REPORT_USB && res == DS_INPUT_REPORT_USB_SIZE) { ds_report = (struct dualsense_input_report *)&data[1]; +#ifdef __APPLE__ + } else if (res == DS_INPUT_REPORT_USB_SIZE - 1) { + /* macOS IOKit strips report ID */ + ds_report = (struct dualsense_input_report *)&data[0]; +#endif } else { fprintf(stderr, "Unhandled report ID %d\n", (int)data[0]); return 3; diff --git a/meson.build b/meson.build index ef93e22..a0a1051 100644 --- a/meson.build +++ b/meson.build @@ -17,14 +17,14 @@ deps = [] if host_machine.system() == 'darwin' # macOS: use generic hidapi (IOKit backend), no udev hidapi = dependency('hidapi') - deps += hidapi + deps += [hidapi] # Link IOKit and CoreFoundation frameworks for the monitor command iokit = dependency('appleframeworks', modules: ['IOKit', 'CoreFoundation']) - deps += iokit + deps += [iokit] else # Linux: use hidapi-hidraw backend + libudev - extra_args += '-Wl,--exclude-libs=ALL' + extra_args += ['-Wl,--exclude-libs=ALL'] hidapi_hidraw = dependency('hidapi-hidraw') udev = dependency('libudev') deps += [hidapi_hidraw, udev] From 68e38814c3badbd8cac2f7cfd806997a950fcf98 Mon Sep 17 00:00:00 2001 From: alfa <87366642+alfaoz@users.noreply.github.com> Date: Sun, 9 Aug 2026 06:00:57 +0300 Subject: [PATCH 3/5] apply review suggestions --- main.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/main.c b/main.c index 4894152..791ce6a 100644 --- a/main.c +++ b/main.c @@ -1119,11 +1119,11 @@ static void get_serial_from_hid_device(IOHIDDeviceRef device, char serial_number size_t len = strlen(buf); if (len == 17) { /* Replace dashes with colons if needed, uppercase */ - for (int i = 0; i < 17; i++) { + for (size_t i = 0; i < len; i++) { if (buf[i] == '-') buf[i] = ':'; serial_number[i] = toupper(buf[i]); } - serial_number[17] = '\0'; + serial_number[len] = '\0'; return; } } @@ -1133,7 +1133,7 @@ static void get_serial_from_hid_device(IOHIDDeviceRef device, char serial_number static void iokit_device_added(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) { (void)context; (void)result; (void)sender; - char serial_number[18] = "00:00:00:00:00:00"; + char serial_number[] = "00:00:00:00:00:00"; get_serial_from_hid_device(device, serial_number); if (sh_command_add) { run_sh_command(sh_command_add, serial_number); @@ -1143,7 +1143,7 @@ static void iokit_device_added(void *context, IOReturn result, void *sender, IOH static void iokit_device_removed(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) { (void)context; (void)result; (void)sender; - char serial_number[18] = "00:00:00:00:00:00"; + char serial_number[] = "00:00:00:00:00:00"; get_serial_from_hid_device(device, serial_number); if (sh_command_remove) { run_sh_command(sh_command_remove, serial_number); @@ -1185,7 +1185,7 @@ static int command_monitor(void) IOReturn ret = IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone); if (ret != kIOReturnSuccess) { - fprintf(stderr, "Failed to open IOHIDManager: 0x%x\n", ret); + fprintf(stderr, "Failed to open IOHIDManager: %#04x\n", ret); CFRelease(match_array); CFRelease(match_edge); CFRelease(match_ds); From ff0939c47b7b81a662a9c56e2622fbf22430bc04 Mon Sep 17 00:00:00 2001 From: alfa <87366642+alfaoz@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:11:02 +0300 Subject: [PATCH 4/5] move macOS monitor to separate file --- macos.c | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++ macos.h | 12 +++++ main.c | 106 ++++--------------------------------------- meson.build | 4 +- 4 files changed, 152 insertions(+), 98 deletions(-) create mode 100644 macos.c create mode 100644 macos.h diff --git a/macos.c b/macos.c new file mode 100644 index 0000000..26a151a --- /dev/null +++ b/macos.c @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include +#include + +#include "macos.h" + +struct monitor_context { + macos_device_callback add_device; + macos_device_callback remove_device; +}; + +static void get_serial_from_hid_device(IOHIDDeviceRef device, char serial_number[18]) +{ + CFStringRef serial = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDSerialNumberKey)); + if (serial && CFGetTypeID(serial) == CFStringGetTypeID()) { + char buf[64] = {0}; + CFStringGetCString(serial, buf, sizeof(buf), kCFStringEncodingUTF8); + /* Serial may come as "aa-bb-cc-dd-ee-ff" or "aa:bb:cc:dd:ee:ff" */ + size_t len = strlen(buf); + if (len == 17) { + /* Replace dashes with colons if needed, uppercase */ + for (size_t i = 0; i < len; i++) { + if (buf[i] == '-') buf[i] = ':'; + serial_number[i] = toupper(buf[i]); + } + serial_number[len] = '\0'; + return; + } + } + strncpy(serial_number, "00:00:00:00:00:00", 18); +} + +static void iokit_device_added(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) +{ + (void)result; + (void)sender; + + struct monitor_context *monitor = context; + char serial_number[] = "00:00:00:00:00:00"; + get_serial_from_hid_device(device, serial_number); + if (monitor->add_device) { + monitor->add_device(serial_number); + } +} + +static void iokit_device_removed(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) +{ + (void)result; + (void)sender; + + struct monitor_context *monitor = context; + char serial_number[] = "00:00:00:00:00:00"; + get_serial_from_hid_device(device, serial_number); + if (monitor->remove_device) { + monitor->remove_device(serial_number); + } +} + +int macos_monitor(int vendor_id_value, int product_id_value, int edge_product_id_value, + macos_device_callback add_device, + macos_device_callback remove_device) +{ + IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + if (!manager) { + fprintf(stderr, "Failed to create IOHIDManager\n"); + return 1; + } + + CFNumberRef vendor_id = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &vendor_id_value); + CFNumberRef product_id = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &product_id_value); + CFNumberRef edge_product_id = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &edge_product_id_value); + + CFDictionaryRef match_ds = CFDictionaryCreate(kCFAllocatorDefault, + (const void *[]){ CFSTR(kIOHIDVendorIDKey), CFSTR(kIOHIDProductIDKey) }, + (const void *[]){ vendor_id, product_id }, + 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + + CFDictionaryRef match_edge = CFDictionaryCreate(kCFAllocatorDefault, + (const void *[]){ CFSTR(kIOHIDVendorIDKey), CFSTR(kIOHIDProductIDKey) }, + (const void *[]){ vendor_id, edge_product_id }, + 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + + CFDictionaryRef matches[] = { match_ds, match_edge }; + CFArrayRef match_array = CFArrayCreate(kCFAllocatorDefault, (const void **)matches, 2, &kCFTypeArrayCallBacks); + + IOHIDManagerSetDeviceMatchingMultiple(manager, match_array); + + struct monitor_context context = { + .add_device = add_device, + .remove_device = remove_device, + }; + IOHIDManagerRegisterDeviceMatchingCallback(manager, iokit_device_added, &context); + IOHIDManagerRegisterDeviceRemovalCallback(manager, iokit_device_removed, &context); + + IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + + IOReturn ret = IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone); + if (ret != kIOReturnSuccess) { + fprintf(stderr, "Failed to open IOHIDManager: %#04x\n", ret); + CFRelease(match_array); + CFRelease(match_edge); + CFRelease(match_ds); + CFRelease(edge_product_id); + CFRelease(product_id); + CFRelease(vendor_id); + CFRelease(manager); + return 1; + } + + /* Run the event loop — blocks until interrupted */ + CFRunLoopRun(); + + IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); + CFRelease(match_array); + CFRelease(match_edge); + CFRelease(match_ds); + CFRelease(edge_product_id); + CFRelease(product_id); + CFRelease(vendor_id); + CFRelease(manager); + + return 0; +} diff --git a/macos.h b/macos.h new file mode 100644 index 0000000..6086346 --- /dev/null +++ b/macos.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef DUALSENSECTL_MACOS_H +#define DUALSENSECTL_MACOS_H + +typedef void (*macos_device_callback)(const char *serial_number); + +int macos_monitor(int vendor_id, int product_id, int edge_product_id, + macos_device_callback add_device, + macos_device_callback remove_device); + +#endif diff --git a/main.c b/main.c index 791ce6a..c162339 100644 --- a/main.c +++ b/main.c @@ -18,8 +18,7 @@ #include #ifdef __APPLE__ -#include -#include +#include "macos.h" #else #include #include @@ -1106,109 +1105,26 @@ static void run_sh_command(const char *command, const char *serial_number) } } -#ifdef __APPLE__ -/* macOS: IOKit-based device monitoring */ - -static void get_serial_from_hid_device(IOHIDDeviceRef device, char serial_number[18]) -{ - CFStringRef serial = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDSerialNumberKey)); - if (serial && CFGetTypeID(serial) == CFStringGetTypeID()) { - char buf[64] = {0}; - CFStringGetCString(serial, buf, sizeof(buf), kCFStringEncodingUTF8); - /* Serial may come as "aa-bb-cc-dd-ee-ff" or "aa:bb:cc:dd:ee:ff" */ - size_t len = strlen(buf); - if (len == 17) { - /* Replace dashes with colons if needed, uppercase */ - for (size_t i = 0; i < len; i++) { - if (buf[i] == '-') buf[i] = ':'; - serial_number[i] = toupper(buf[i]); - } - serial_number[len] = '\0'; - return; - } - } - strncpy(serial_number, "00:00:00:00:00:00", 18); -} - -static void iokit_device_added(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) +static void run_add_command(const char *serial_number) { - (void)context; (void)result; (void)sender; - char serial_number[] = "00:00:00:00:00:00"; - get_serial_from_hid_device(device, serial_number); if (sh_command_add) { run_sh_command(sh_command_add, serial_number); } } -static void iokit_device_removed(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) +static void run_remove_command(const char *serial_number) { - (void)context; (void)result; (void)sender; - char serial_number[] = "00:00:00:00:00:00"; - get_serial_from_hid_device(device, serial_number); if (sh_command_remove) { run_sh_command(sh_command_remove, serial_number); } } +#ifdef __APPLE__ + static int command_monitor(void) { - IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); - if (!manager) { - fprintf(stderr, "Failed to create IOHIDManager\n"); - return 1; - } - - /* Match DualSense and DualSense Edge by vendor/product ID */ - CFNumberRef vendor_id = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &(int){DS_VENDOR_ID}); - CFNumberRef product_id_ds = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &(int){DS_PRODUCT_ID}); - CFNumberRef product_id_edge = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &(int){DS_EDGE_PRODUCT_ID}); - - CFDictionaryRef match_ds = CFDictionaryCreate(kCFAllocatorDefault, - (const void *[]){ CFSTR(kIOHIDVendorIDKey), CFSTR(kIOHIDProductIDKey) }, - (const void *[]){ vendor_id, product_id_ds }, - 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); - - CFDictionaryRef match_edge = CFDictionaryCreate(kCFAllocatorDefault, - (const void *[]){ CFSTR(kIOHIDVendorIDKey), CFSTR(kIOHIDProductIDKey) }, - (const void *[]){ vendor_id, product_id_edge }, - 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); - - CFDictionaryRef matches[] = { match_ds, match_edge }; - CFArrayRef match_array = CFArrayCreate(kCFAllocatorDefault, (const void **)matches, 2, &kCFTypeArrayCallBacks); - - IOHIDManagerSetDeviceMatchingMultiple(manager, match_array); - - IOHIDManagerRegisterDeviceMatchingCallback(manager, iokit_device_added, NULL); - IOHIDManagerRegisterDeviceRemovalCallback(manager, iokit_device_removed, NULL); - - IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); - - IOReturn ret = IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone); - if (ret != kIOReturnSuccess) { - fprintf(stderr, "Failed to open IOHIDManager: %#04x\n", ret); - CFRelease(match_array); - CFRelease(match_edge); - CFRelease(match_ds); - CFRelease(product_id_edge); - CFRelease(product_id_ds); - CFRelease(vendor_id); - CFRelease(manager); - return 1; - } - - /* Run the event loop — blocks until interrupted */ - CFRunLoopRun(); - - IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); - CFRelease(match_array); - CFRelease(match_edge); - CFRelease(match_ds); - CFRelease(product_id_edge); - CFRelease(product_id_ds); - CFRelease(vendor_id); - CFRelease(manager); - - return 0; + return macos_monitor(DS_VENDOR_ID, DS_PRODUCT_ID, DS_EDGE_PRODUCT_ID, + run_add_command, run_remove_command); } #else @@ -1286,9 +1202,7 @@ static void add_device(struct udev_device *dev) if (!check_dualsense_device(dev, serial_number)) { return; } - if (sh_command_add) { - run_sh_command(sh_command_add, serial_number); - } + run_add_command(serial_number); } static void remove_device(struct udev_device *dev) @@ -1297,9 +1211,7 @@ static void remove_device(struct udev_device *dev) if (!check_dualsense_device(dev, serial_number)) { return; } - if (sh_command_remove) { - run_sh_command(sh_command_remove, serial_number); - } + run_remove_command(serial_number); } static int command_monitor(void) diff --git a/meson.build b/meson.build index a0a1051..96cdd0e 100644 --- a/meson.build +++ b/meson.build @@ -13,11 +13,13 @@ project( cc = meson.get_compiler('c') extra_args = ['-DDUALSENSECTL_VERSION="@0@"'.format(meson.project_version())] deps = [] +sources = ['main.c'] if host_machine.system() == 'darwin' # macOS: use generic hidapi (IOKit backend), no udev hidapi = dependency('hidapi') deps += [hidapi] + sources += ['macos.c'] # Link IOKit and CoreFoundation frameworks for the monitor command iokit = dependency('appleframeworks', modules: ['IOKit', 'CoreFoundation']) @@ -34,7 +36,7 @@ add_project_arguments(extra_args, language: 'c') executable( 'dualsensectl', - ['main.c'], + sources, dependencies: deps, install: true, ) From b6123cc2a0301e4df8ebcae85444cf8bb05887ae Mon Sep 17 00:00:00 2001 From: alfa <87366642+alfaoz@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:30:00 +0300 Subject: [PATCH 5/5] address review follow-ups --- macos.c | 23 ++++++++++++----------- main.c | 12 ++++++------ 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/macos.c b/macos.c index 26a151a..e1cf81a 100644 --- a/macos.c +++ b/macos.c @@ -18,18 +18,19 @@ static void get_serial_from_hid_device(IOHIDDeviceRef device, char serial_number { CFStringRef serial = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDSerialNumberKey)); if (serial && CFGetTypeID(serial) == CFStringGetTypeID()) { - char buf[64] = {0}; - CFStringGetCString(serial, buf, sizeof(buf), kCFStringEncodingUTF8); - /* Serial may come as "aa-bb-cc-dd-ee-ff" or "aa:bb:cc:dd:ee:ff" */ - size_t len = strlen(buf); - if (len == 17) { - /* Replace dashes with colons if needed, uppercase */ - for (size_t i = 0; i < len; i++) { - if (buf[i] == '-') buf[i] = ':'; - serial_number[i] = toupper(buf[i]); + char buf[64]; + if (CFStringGetCString(serial, buf, sizeof(buf), kCFStringEncodingUTF8)) { + /* Serial may come as "aa-bb-cc-dd-ee-ff" or "aa:bb:cc:dd:ee:ff" */ + size_t len = strlen(buf); + if (len == 17) { + /* Replace dashes with colons if needed, uppercase */ + for (size_t i = 0; i < len; i++) { + if (buf[i] == '-') buf[i] = ':'; + serial_number[i] = toupper(buf[i]); + } + serial_number[len] = '\0'; + return; } - serial_number[len] = '\0'; - return; } } strncpy(serial_number, "00:00:00:00:00:00", 18); diff --git a/main.c b/main.c index c162339..52d3177 100644 --- a/main.c +++ b/main.c @@ -33,6 +33,8 @@ #include "crc32.h" +#define YESNO(x) ((x) ? "Yes" : "No") + /* Portable thrd_sleep replacement using nanosleep on macOS */ #ifdef __APPLE__ #define thrd_sleep(ts, rem) nanosleep((ts), (rem)) @@ -501,11 +503,9 @@ static int command_battery(struct dualsense *ds) } else if (!ds->bt && res == DS_INPUT_REPORT_USB_SIZE - 1) { /* macOS IOKit strips report ID on USB */ ds_report = (struct dualsense_input_report *)&data[0]; - } else if (ds->bt && res == DS_INPUT_REPORT_BT_SIZE - 4 - 1) { - /* macOS IOKit strips report ID on BT; skip 1 byte (tag area) */ - ds_report = (struct dualsense_input_report *)&data[1]; - } else if (ds->bt && res == DS_INPUT_REPORT_BT_SIZE - 1) { - /* macOS IOKit strips report ID on BT, but keeps CRC */ + } else if (ds->bt && (res == DS_INPUT_REPORT_BT_SIZE - 4 - 1 || + res == DS_INPUT_REPORT_BT_SIZE - 1)) { + /* macOS IOKit strips the BT report ID, with or without the CRC */ ds_report = (struct dualsense_input_report *)&data[1]; #endif } else if (ds->bt && data[0] == DS_INPUT_REPORT_BT && res == DS_INPUT_REPORT_BT_SIZE) { @@ -519,7 +519,7 @@ static int command_battery(struct dualsense *ds) return 2; } #endif - fprintf(stderr, "Unhandled report ID %d (size %d, bt=%d)\n", (int)data[0], res, ds->bt); + fprintf(stderr, "Unhandled report ID %d (size %d, bt=%s)\n", (int)data[0], res, YESNO(ds->bt)); return 3; }