Date: Wed, 15 Apr 2026 22:36:47 +0000 From: Enji Cooper <ngie@FreeBSD.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org Cc: Abdelkader Boudih <chaos@seuros.com> Subject: git: a85c4ab626ef - main - appleir: Add Apple IR receiver driver Message-ID: <69e012ff.3346d.bf55c2@gitrepo.freebsd.org>
index | next in thread | raw e-mail
The branch main has been updated by ngie: URL: https://cgit.FreeBSD.org/src/commit/?id=a85c4ab626ef52530400ace1957daaa35dd07534 commit a85c4ab626ef52530400ace1957daaa35dd07534 Author: Abdelkader Boudih <chaos@seuros.com> AuthorDate: 2026-04-14 02:29:43 +0000 Commit: Enji Cooper <ngie@FreeBSD.org> CommitDate: 2026-04-15 22:36:17 +0000 appleir: Add Apple IR receiver driver HID driver for Apple IR receivers (USB HID, vendor 0x05ac). Supports Apple Remote and generic IR remotes using NEC protocol. Supported hardware: - Apple IR Receiver (0x8240, 0x8241, 0x8242, 0x8243, 0x1440) Apple Remote protocol (proprietary 5-byte HID reports): - Key down/repeat/battery-low detection - 17-key mapping with two-packet command support - Synthesized key-up via 125ms callout timer Generic IR remotes (NEC protocol): - Format: [0x26][0x7f][0x80][code][~code] - Checksum: code + ~code = 0xFF - Default keymap with 8 common codes - See: https://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol Output via evdev with standard KEY_* codes. Raw HID access available at /dev/hidraw0 for custom remapping. Based on protocol reverse-engineering by James McKenzie et al. Reference: drivers/hid/hid-appleir.c (Linux) Tested on Mac Mini 2011 (0x05ac:0x8242). Differential Revision: https://reviews.freebsd.org/D55472 --- share/man/man4/Makefile | 1 + share/man/man4/appleir.4 | 93 +++++++++ sys/conf/files | 1 + sys/dev/hid/appleir.c | 440 +++++++++++++++++++++++++++++++++++++++ sys/modules/hid/Makefile | 1 + sys/modules/hid/appleir/Makefile | 8 + 6 files changed, 544 insertions(+) diff --git a/share/man/man4/Makefile b/share/man/man4/Makefile index 7920b2006822..60ab6e8bdab6 100644 --- a/share/man/man4/Makefile +++ b/share/man/man4/Makefile @@ -44,6 +44,7 @@ MAN= aac.4 \ alc.4 \ ale.4 \ alpm.4 \ + appleir.4 \ altq.4 \ amdpm.4 \ ${_amdsbwd.4} \ diff --git a/share/man/man4/appleir.4 b/share/man/man4/appleir.4 new file mode 100644 index 000000000000..8717bf1b2e83 --- /dev/null +++ b/share/man/man4/appleir.4 @@ -0,0 +1,93 @@ +.\" Copyright (c) 2026 Abdelkader Boudih <freebsd@seuros.com> +.\" +.\" SPDX-License-Identifier: BSD-2-Clause +.\" +.Dd February 13, 2026 +.Dt APPLEIR 4 +.Os +.Sh NAME +.Nm appleir +.Nd Apple IR receiver driver +.Sh SYNOPSIS +To compile this driver into the kernel, +place the following lines in your +kernel configuration file: +.Bd -ragged -offset indent +.Cd "device appleir" +.Cd "device hidbus" +.Cd "device hid" +.Cd "device evdev" +.Ed +.Pp +Alternatively, to load the driver as a +module at boot time, place the following line in +.Xr loader.conf 5 Ns : +.Bd -literal -offset indent +appleir_load="YES" +.Ed +.Sh DESCRIPTION +The +.Nm +driver provides support for Apple IR receivers found in Mac computers +(2006-2011 era). +It supports both Apple Remote controls and generic IR remotes using the +NEC infrared protocol. +.Pp +Supported devices include: +.Bl -bullet -compact +.It +Apple IR Receiver (USB product IDs 0x8240, 0x8241, 0x8242, 0x8243, 0x1440) +.El +.Pp +The driver decodes proprietary Apple Remote button presses and provides +a default keymap for common NEC protocol codes used by generic IR remotes. +Unmapped button codes can be accessed via the raw HID device at +.Pa /dev/hidrawX +for custom userland remapping. +.Pp +The +.Pa /dev/input/eventX +device presents the remote control as an +evdev +input device with standard KEY_* codes suitable for media applications. +.Sh HARDWARE +The +.Nm +driver supports Apple IR receivers with USB vendor ID 0x05ac and the +following product IDs: +.Pp +.Bl -tag -width "0x8242" -compact +.It 0x8240 +Apple IR Receiver (first generation) +.It 0x8241 +Apple IR Receiver +.It 0x8242 +Apple IR Receiver (Mac Mini 2011, MacBook Pro 3,1) +.It 0x8243 +Apple IR Receiver +.It 0x1440 +Apple IR Receiver (slim) +.El +.Sh FILES +.Bl -tag -width ".Pa /dev/input/eventX" -compact +.It Pa /dev/input/eventX +evdev input device +.It Pa /dev/hidrawX +raw HID device for custom button mapping +.El +.Sh SEE ALSO +evdev , +.Xr hidbus 4 , +.Xr usbhid 4 +.Pp +NEC Infrared Transmission Protocol: +.Lk https://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol +.Sh HISTORY +The +.Nm +driver first appeared in +.Fx 16.0 . +.Sh AUTHORS +.An Abdelkader Boudih Aq Mt freebsd@seuros.com +.Pp +Based on protocol reverse-engineering by James McKenzie and others. diff --git a/sys/conf/files b/sys/conf/files index 99ba7cdaba33..da07e370a712 100644 --- a/sys/conf/files +++ b/sys/conf/files @@ -1775,6 +1775,7 @@ dev/gpio/gpio_if.m optional gpio dev/gpio/gpiobus_if.m optional gpio dev/gpio/gpiopps.c optional gpiopps fdt dev/gpio/ofw_gpiobus.c optional fdt gpio +dev/hid/appleir.c optional appleir dev/hid/bcm5974.c optional bcm5974 dev/hid/hconf.c optional hconf dev/hid/hcons.c optional hcons diff --git a/sys/dev/hid/appleir.c b/sys/dev/hid/appleir.c new file mode 100644 index 000000000000..956ad27f6d70 --- /dev/null +++ b/sys/dev/hid/appleir.c @@ -0,0 +1,440 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026 Abdelkader Boudih <freebsd@seuros.com> + */ + +/* + * Apple IR Remote Control Driver + * + * HID driver for Apple IR receivers (USB HID, vendor 0x05ac). + * Supports Apple Remote and generic IR remotes using NEC protocol. + * + * The Apple Remote protocol was reverse-engineered by James McKenzie and + * others; key codes and packet format constants are derived from that work + * and are factual descriptions of the hardware protocol, not copied code. + * Linux reference (GPL-2.0, no code copied): drivers/hid/hid-appleir.c + * + * Apple Remote Protocol (proprietary): + * Key down: [0x25][0x87][0xee][remote_id][key_code] + * Key repeat: [0x26][0x87][0xee][remote_id][key_code] + * Battery low: [0x25][0x87][0xe0][remote_id][0x00] + * Key decode: (byte4 >> 1) & 0x0F -> keymap[index] + * Two-packet: bit 6 of key_code (0x40) set -> store index, use on next keydown + * + * Generic IR Protocol (NEC-style): + * Format: [0x26][0x7f][0x80][code][~code] + * Checksum: code + ~code = 0xFF + * + * NO hardware key-up events -- synthesize via 125ms callout timer. + */ + +#include <sys/cdefs.h> + +#include "opt_hid.h" + +#include <sys/param.h> +#include <sys/bus.h> +#include <sys/callout.h> +#include <sys/kernel.h> +#include <sys/lock.h> +#include <sys/malloc.h> +#include <sys/module.h> +#include <sys/mutex.h> +#include <sys/sysctl.h> + +#include <dev/evdev/input.h> +#include <dev/evdev/evdev.h> + +#define HID_DEBUG_VAR appleir_debug +#include <dev/hid/hid.h> +#include <dev/hid/hidbus.h> +#include "usbdevs.h" + +#ifdef HID_DEBUG +static int appleir_debug = 0; + +static SYSCTL_NODE(_hw_hid, OID_AUTO, appleir, CTLFLAG_RW, 0, + "Apple IR Remote Control"); +SYSCTL_INT(_hw_hid_appleir, OID_AUTO, debug, CTLFLAG_RWTUN, + &appleir_debug, 0, "Debug level"); +#endif + +/* Protocol constants */ +#define APPLEIR_REPORT_LEN 5 +#define APPLEIR_KEY_MASK 0x0F +#define APPLEIR_TWO_PKT_FLAG 0x40 /* bit 6: two-packet command */ +#define APPLEIR_KEYUP_TICKS MAX(1, hz / 8) /* 125ms */ +#define APPLEIR_TWOPKT_TICKS MAX(1, hz / 4) /* 250ms */ + +/* Report type markers (byte 0) */ +#define APPLEIR_PKT_KEYDOWN 0x25 /* key down / battery low */ +#define APPLEIR_PKT_REPEAT 0x26 /* key repeat / NEC generic */ + +/* Apple Remote signature (bytes 1-2) */ +#define APPLEIR_SIG_HI 0x87 +#define APPLEIR_SIG_KEYLO 0xee /* normal key event */ +#define APPLEIR_SIG_BATTLO 0xe0 /* battery low event */ + +/* Generic IR NEC signature (bytes 1-2) */ +#define APPLEIR_NEC_HI 0x7f +#define APPLEIR_NEC_LO 0x80 +#define APPLEIR_NEC_CHECKSUM 0xFF /* code + ~code must equal this */ + +/* + * Apple IR keymap: 17 entries, index = (key_code >> 1) & 0x0F + * Based on Linux driver (hid-appleir.c) keymap. + */ +static const uint16_t appleir_keymap[] = { + KEY_RESERVED, /* 0x00 */ + KEY_MENU, /* 0x01 - menu */ + KEY_PLAYPAUSE, /* 0x02 - play/pause */ + KEY_FORWARD, /* 0x03 - >> */ + KEY_BACK, /* 0x04 - << */ + KEY_VOLUMEUP, /* 0x05 - + */ + KEY_VOLUMEDOWN, /* 0x06 - - */ + KEY_RESERVED, /* 0x07 */ + KEY_RESERVED, /* 0x08 */ + KEY_RESERVED, /* 0x09 */ + KEY_RESERVED, /* 0x0A */ + KEY_RESERVED, /* 0x0B */ + KEY_RESERVED, /* 0x0C */ + KEY_RESERVED, /* 0x0D */ + KEY_ENTER, /* 0x0E - middle button (two-packet) */ + KEY_PLAYPAUSE, /* 0x0F - play/pause (two-packet) */ + KEY_RESERVED, /* 0x10 - out of range guard */ +}; +#define APPLEIR_NKEYS (nitems(appleir_keymap)) + +/* + * Generic IR keymap (NEC protocol codes). + * Maps raw NEC codes to evdev KEY_* codes. + */ +struct generic_ir_map { + uint8_t code; /* NEC IR code */ + uint16_t key; /* evdev KEY_* */ +}; + +static const struct generic_ir_map generic_keymap[] = { + { 0xe1, KEY_VOLUMEUP }, + { 0xe9, KEY_VOLUMEDOWN }, + { 0xed, KEY_CHANNELUP }, + { 0xf3, KEY_CHANNELDOWN }, + { 0xf5, KEY_PLAYPAUSE }, + { 0xf9, KEY_POWER }, + { 0xfb, KEY_MUTE }, + { 0xfe, KEY_OK }, +}; +#define GENERIC_NKEYS (nitems(generic_keymap)) + +static uint16_t +generic_ir_lookup(uint8_t code) +{ + int i; + + for (i = 0; i < GENERIC_NKEYS; i++) { + if (generic_keymap[i].code == code) + return (generic_keymap[i].key); + } + return (KEY_RESERVED); +} + +struct appleir_softc { + device_t sc_dev; + struct mtx sc_mtx; /* protects below + callout */ + struct evdev_dev *sc_evdev; + struct callout sc_co; /* key-up timer */ + struct callout sc_twoco; /* two-packet timeout */ + uint16_t sc_current_key; /* evdev keycode (0=none) */ + int sc_prev_key_idx;/* two-packet state (0=none) */ + bool sc_batt_warned; +}; + + +/* + * Callout: synthesize key-up event (no hardware key-up from remote). + * Runs with sc_mtx held (callout_init_mtx). + */ +static void +appleir_keyup(void *arg) +{ + struct appleir_softc *sc = arg; + + mtx_assert(&sc->sc_mtx, MA_OWNED); + + if (sc->sc_current_key != 0) { + evdev_push_key(sc->sc_evdev, sc->sc_current_key, 0); + evdev_sync(sc->sc_evdev); + sc->sc_current_key = 0; + sc->sc_prev_key_idx = 0; + } +} + +static void +appleir_twopacket_timeout(void *arg) +{ + struct appleir_softc *sc = arg; + + mtx_assert(&sc->sc_mtx, MA_OWNED); + sc->sc_prev_key_idx = 0; +} + +/* + * Process 5-byte HID interrupt report. + * Called from hidbus interrupt context. + */ +static void +appleir_intr(void *context, void *data, hid_size_t len) +{ + struct appleir_softc *sc = context; + uint8_t *buf = data; + uint8_t report[APPLEIR_REPORT_LEN]; + int index; + uint16_t new_key; + + if (len != APPLEIR_REPORT_LEN) { + DPRINTFN(1, "bad report len: %zu\n", (size_t)len); + return; + } + + memcpy(report, buf, APPLEIR_REPORT_LEN); + + mtx_lock(&sc->sc_mtx); + + /* Battery low: [KEYDOWN][SIG_HI][SIG_BATTLO] -- log and ignore */ + if (report[0] == APPLEIR_PKT_KEYDOWN && + report[1] == APPLEIR_SIG_HI && report[2] == APPLEIR_SIG_BATTLO) { + if (!sc->sc_batt_warned) { + device_printf(sc->sc_dev, + "remote battery may be low\n"); + sc->sc_batt_warned = true; + } + goto done; + } + + /* Key down: [KEYDOWN][SIG_HI][SIG_KEYLO][remote_id][key_code] */ + if (report[0] == APPLEIR_PKT_KEYDOWN && + report[1] == APPLEIR_SIG_HI && report[2] == APPLEIR_SIG_KEYLO) { + /* Release previous key if held */ + if (sc->sc_current_key != 0) { + evdev_push_key(sc->sc_evdev, sc->sc_current_key, 0); + evdev_sync(sc->sc_evdev); + sc->sc_current_key = 0; + } + + if (sc->sc_prev_key_idx > 0) { + /* Second packet of a two-packet command */ + index = sc->sc_prev_key_idx; + sc->sc_prev_key_idx = 0; + callout_stop(&sc->sc_twoco); + } else if (report[4] & APPLEIR_TWO_PKT_FLAG) { + /* First packet of a two-packet command -- wait for next */ + sc->sc_prev_key_idx = (report[4] >> 1) & APPLEIR_KEY_MASK; + callout_reset(&sc->sc_twoco, APPLEIR_TWOPKT_TICKS, + appleir_twopacket_timeout, sc); + goto done; + } else { + index = (report[4] >> 1) & APPLEIR_KEY_MASK; + } + + new_key = (index < APPLEIR_NKEYS) ? + appleir_keymap[index] : KEY_RESERVED; + if (new_key != KEY_RESERVED) { + sc->sc_current_key = new_key; + evdev_push_key(sc->sc_evdev, new_key, 1); + evdev_sync(sc->sc_evdev); + callout_reset(&sc->sc_co, APPLEIR_KEYUP_TICKS, + appleir_keyup, sc); + } + goto done; + } + + /* Key repeat: [REPEAT][SIG_HI][SIG_KEYLO][remote_id][key_code] */ + if (report[0] == APPLEIR_PKT_REPEAT && + report[1] == APPLEIR_SIG_HI && report[2] == APPLEIR_SIG_KEYLO) { + uint16_t repeat_key; + int repeat_idx; + + if (sc->sc_prev_key_idx > 0) + goto done; + if (report[4] & APPLEIR_TWO_PKT_FLAG) + goto done; + + repeat_idx = (report[4] >> 1) & APPLEIR_KEY_MASK; + repeat_key = (repeat_idx < APPLEIR_NKEYS) ? + appleir_keymap[repeat_idx] : KEY_RESERVED; + if (repeat_key == KEY_RESERVED || + repeat_key != sc->sc_current_key) + goto done; + + evdev_push_key(sc->sc_evdev, repeat_key, 1); + evdev_sync(sc->sc_evdev); + callout_reset(&sc->sc_co, APPLEIR_KEYUP_TICKS, + appleir_keyup, sc); + goto done; + } + + /* Generic IR (NEC protocol): [REPEAT][NEC_HI][NEC_LO][code][~code] */ + if (report[0] == APPLEIR_PKT_REPEAT && + report[1] == APPLEIR_NEC_HI && report[2] == APPLEIR_NEC_LO) { + uint8_t code = report[3]; + uint8_t checksum = report[4]; + + sc->sc_prev_key_idx = 0; + callout_stop(&sc->sc_twoco); + + if ((uint8_t)(code + checksum) != APPLEIR_NEC_CHECKSUM) { + DPRINTFN(1, "generic IR: bad checksum %02x+%02x\n", + code, checksum); + goto done; + } + + new_key = generic_ir_lookup(code); + if (new_key == KEY_RESERVED) + goto done; + + if (sc->sc_current_key != new_key) { + if (sc->sc_current_key != 0) + evdev_push_key(sc->sc_evdev, + sc->sc_current_key, 0); + sc->sc_current_key = new_key; + evdev_push_key(sc->sc_evdev, new_key, 1); + evdev_sync(sc->sc_evdev); + } else { + evdev_push_key(sc->sc_evdev, new_key, 1); + evdev_sync(sc->sc_evdev); + } + callout_reset(&sc->sc_co, APPLEIR_KEYUP_TICKS, + appleir_keyup, sc); + goto done; + } + + DPRINTFN(1, "unknown report: %02x %02x %02x\n", + report[0], report[1], report[2]); + +done: + mtx_unlock(&sc->sc_mtx); +} + +/* Apple IR receiver device IDs */ +static const struct hid_device_id appleir_devs[] = { + { HID_BVP(BUS_USB, USB_VENDOR_APPLE, 0x8240) }, + { HID_BVP(BUS_USB, USB_VENDOR_APPLE, 0x8241) }, + { HID_BVP(BUS_USB, USB_VENDOR_APPLE, 0x8242) }, + { HID_BVP(BUS_USB, USB_VENDOR_APPLE, 0x8243) }, + { HID_BVP(BUS_USB, USB_VENDOR_APPLE, 0x1440) }, +}; + +static int +appleir_probe(device_t dev) +{ + int error; + + error = HIDBUS_LOOKUP_DRIVER_INFO(dev, appleir_devs); + if (error != 0) + return (error); + + /* Only attach to first top-level collection (TLC index 0) */ + if (hidbus_get_index(dev) != 0) + return (ENXIO); + + hidbus_set_desc(dev, "Apple IR Receiver"); + return (BUS_PROBE_DEFAULT); +} + +static int +appleir_attach(device_t dev) +{ + struct appleir_softc *sc = device_get_softc(dev); + const struct hid_device_info *hw; + int i, error; + + sc->sc_dev = dev; + hw = hid_get_device_info(dev); + sc->sc_current_key = 0; + sc->sc_prev_key_idx = 0; + sc->sc_batt_warned = false; + mtx_init(&sc->sc_mtx, "appleir", NULL, MTX_DEF); + callout_init_mtx(&sc->sc_co, &sc->sc_mtx, 0); + callout_init_mtx(&sc->sc_twoco, &sc->sc_mtx, 0); + + sc->sc_evdev = evdev_alloc(); + evdev_set_name(sc->sc_evdev, device_get_desc(dev)); + evdev_set_phys(sc->sc_evdev, device_get_nameunit(dev)); + evdev_set_id(sc->sc_evdev, hw->idBus, hw->idVendor, hw->idProduct, + hw->idVersion); + evdev_set_serial(sc->sc_evdev, hw->serial); + evdev_support_event(sc->sc_evdev, EV_SYN); + evdev_support_event(sc->sc_evdev, EV_KEY); + evdev_support_event(sc->sc_evdev, EV_REP); + + for (i = 0; i < APPLEIR_NKEYS; i++) { + if (appleir_keymap[i] != KEY_RESERVED) + evdev_support_key(sc->sc_evdev, appleir_keymap[i]); + } + for (i = 0; i < GENERIC_NKEYS; i++) + evdev_support_key(sc->sc_evdev, generic_keymap[i].key); + + error = evdev_register_mtx(sc->sc_evdev, &sc->sc_mtx); + if (error != 0) { + device_printf(dev, "evdev_register_mtx failed: %d\n", error); + goto fail; + } + + hidbus_set_intr(dev, appleir_intr, sc); + + error = hid_intr_start(dev); + if (error != 0) { + device_printf(dev, "hid_intr_start failed: %d\n", error); + goto fail; + } + + return (0); + +fail: + if (sc->sc_evdev != NULL) + evdev_free(sc->sc_evdev); + callout_drain(&sc->sc_co); + callout_drain(&sc->sc_twoco); + mtx_destroy(&sc->sc_mtx); + return (error); +} + +static int +appleir_detach(device_t dev) +{ + struct appleir_softc *sc = device_get_softc(dev); + int error; + + error = hid_intr_stop(dev); + if (error != 0) { + device_printf(dev, "hid_intr_stop failed: %d\n", error); + return (error); + } + callout_drain(&sc->sc_co); + callout_drain(&sc->sc_twoco); + evdev_free(sc->sc_evdev); + mtx_destroy(&sc->sc_mtx); + + return (0); +} + +static device_method_t appleir_methods[] = { + DEVMETHOD(device_probe, appleir_probe), + DEVMETHOD(device_attach, appleir_attach), + DEVMETHOD(device_detach, appleir_detach), + DEVMETHOD_END +}; + +static driver_t appleir_driver = { + "appleir", + appleir_methods, + sizeof(struct appleir_softc) +}; + +DRIVER_MODULE(appleir, hidbus, appleir_driver, NULL, NULL); +MODULE_DEPEND(appleir, hid, 1, 1, 1); +MODULE_DEPEND(appleir, hidbus, 1, 1, 1); +MODULE_DEPEND(appleir, evdev, 1, 1, 1); +MODULE_VERSION(appleir, 1); +HID_PNP_INFO(appleir_devs); diff --git a/sys/modules/hid/Makefile b/sys/modules/hid/Makefile index 10720570deb7..a32b9326647e 100644 --- a/sys/modules/hid/Makefile +++ b/sys/modules/hid/Makefile @@ -6,6 +6,7 @@ SUBDIR = \ hidraw SUBDIR += \ + appleir \ bcm5974 \ hconf \ hcons \ diff --git a/sys/modules/hid/appleir/Makefile b/sys/modules/hid/appleir/Makefile new file mode 100644 index 000000000000..5b56b7ea3048 --- /dev/null +++ b/sys/modules/hid/appleir/Makefile @@ -0,0 +1,8 @@ +.PATH: ${SRCTOP}/sys/dev/hid + +KMOD= appleir +SRCS= appleir.c +SRCS+= opt_hid.h +SRCS+= bus_if.h device_if.h usbdevs.h + +.include <bsd.kmod.mk>home | help
Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?69e012ff.3346d.bf55c2>
