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
This commit is contained in:
Abdelkader Boudih
2026-04-13 19:29:43 -07:00
committed by Enji Cooper
parent 2d13620c6e
commit a85c4ab626
6 changed files with 544 additions and 0 deletions
+1
View File
@@ -44,6 +44,7 @@ MAN= aac.4 \
alc.4 \
ale.4 \
alpm.4 \
appleir.4 \
altq.4 \
amdpm.4 \
${_amdsbwd.4} \
+93
View File
@@ -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.
+1
View File
@@ -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
+440
View File
@@ -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);
+1
View File
@@ -6,6 +6,7 @@ SUBDIR = \
hidraw
SUBDIR += \
appleir \
bcm5974 \
hconf \
hcons \
+8
View File
@@ -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>