Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
129 changes: 129 additions & 0 deletions macos.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: GPL-2.0-or-later

#include <ctype.h>
#include <stdio.h>
#include <string.h>

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/hid/IOHIDManager.h>

#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];
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;
}
}
}
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;
}
12 changes: 12 additions & 0 deletions macos.h
Original file line number Diff line number Diff line change
@@ -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
88 changes: 77 additions & 11 deletions main.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,32 @@
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <poll.h>
#include <sys/wait.h>
#include <time.h>

#ifdef __APPLE__
#include "macos.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"

#define YESNO(x) ((x) ? "Yes" : "No")

/* 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 +417,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 +499,27 @@ 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 ||
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
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=%s)\n", (int)data[0], res, YESNO(ds->bt));
return 3;
}

Expand Down Expand Up @@ -1066,6 +1105,31 @@ static void run_sh_command(const char *command, const char *serial_number)
}
}

static void run_add_command(const char *serial_number)
{
if (sh_command_add) {
run_sh_command(sh_command_add, serial_number);
}
}

static void run_remove_command(const char *serial_number)
{
if (sh_command_remove) {
run_sh_command(sh_command_remove, serial_number);
}
}

#ifdef __APPLE__

static int command_monitor(void)
{
return macos_monitor(DS_VENDOR_ID, DS_PRODUCT_ID, DS_EDGE_PRODUCT_ID,
run_add_command, run_remove_command);
}

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

static uint32_t read_file_hex(const char *path)
{
uint32_t out = 0;
Expand Down Expand Up @@ -1138,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)
Expand All @@ -1149,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)
Expand Down Expand Up @@ -1201,6 +1261,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 +1517,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
35 changes: 24 additions & 11 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,33 @@ 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 = []
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'])
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],
sources,
dependencies: deps,
install: true,
)