/*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2026 Abdelkader Boudih */ /* * 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 #include "opt_hid.h" #include #include #include #include #include #include #include #include #include #include #include #define HID_DEBUG_VAR appleir_debug #include #include #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);