Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 159 additions & 5 deletions main.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,31 @@
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <poll.h>
#include <sys/wait.h>
#include <time.h>

#ifdef __APPLE__
#include <IOKit/hid/IOHIDManager.h>
#include <CoreFoundation/CoreFoundation.h>
#else
#include <poll.h>
#include <threads.h>
#include <libudev.h>
#endif

#ifdef __APPLE__
#include <hidapi.h>
#else
#include <hidapi/hidapi.h>
#include <libudev.h>
#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
Expand Down Expand Up @@ -401,8 +416,16 @@ static bool dualsense_init(struct dualsense *ds, const char *serial)

wchar_t *serial_number = dev->serial_number;

if (wcslen(serial_number) != 17) {
fprintf(stderr, "Invalid device serial number: %ls\n", serial_number);
if (!serial_number || wcslen(serial_number) != 17) {
#ifdef __APPLE__
/* On macOS USB, serial number is often not exposed via IOKit — this is normal */
#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";
}
Expand Down Expand Up @@ -475,12 +498,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];
Comment thread
alfaoz marked this conversation as resolved.
Outdated
#endif
Comment thread
alfaoz marked this conversation as resolved.
} 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);
Comment thread
alfaoz marked this conversation as resolved.
Outdated
return 3;
}

Expand Down Expand Up @@ -1066,6 +1106,114 @@ 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);
Comment thread
alfaoz marked this conversation as resolved.
Outdated
/* 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++) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trap for future refactoring!

Suggested change
for (int i = 0; i < 17; i++) {
for (int i = 0; i < len; i++) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair! will change.

if (buf[i] == '-') buf[i] = ':';
serial_number[i] = toupper(buf[i]);
Comment thread
alfaoz marked this conversation as resolved.
Outdated
}
serial_number[17] = '\0';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
serial_number[17] = '\0';
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)context; (void)result; (void)sender;
char serial_number[18] = "00:00:00:00:00:00";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
char serial_number[18] = "00:00:00:00:00:00";
char serial_number[] = "00:00:00:00:00:00";

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here. will change.

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";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);
}
}

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);
Comment thread
alfaoz marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fprintf(stderr, "Failed to open IOHIDManager: 0x%x\n", ret);
fprintf(stderr, "Failed to open IOHIDManager: %#04x\n", ret);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will change!

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;
}

#else
/* Linux: udev-based device monitoring */

static uint32_t read_file_hex(const char *path)
{
uint32_t out = 0;
Expand Down Expand Up @@ -1201,6 +1349,7 @@ static int command_monitor(void)

return 0;
}
#endif /* __APPLE__ */

static int dualsense_send_fw_feature(struct dualsense *ds, uint8_t *buf)
{
Expand Down Expand Up @@ -1456,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 */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe replace the entire branch on Darwin instead of falling back?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in my testing macos always stripped the report id, but i kept the generic branch so a build where hidapi doesn't strip still works. i'd rather not hard-assume iokit behavior here lol

ds_report = (struct dualsense_input_report *)&data[0];
#endif
} else {
fprintf(stderr, "Unhandled report ID %d\n", (int)data[0]);
return 3;
Expand Down
31 changes: 21 additions & 10 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Comment thread
alfaoz marked this conversation as resolved.
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,
)
Loading