| /* |
| * HID driver for Google Fiber TV remote controls |
| * |
| * Copyright (c) 2014 Google Inc. |
| * |
| * This program is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License as published by the Free |
| * Software Foundation; either version 2 of the License, or (at your option) |
| * any later version. |
| */ |
| #include <linux/device.h> |
| #include <linux/hid.h> |
| #include <linux/module.h> |
| #include <linux/slab.h> |
| #include <linux/usb.h> |
| #include "hid-ids.h" |
| |
| #define GFRM100 1 /* Google Fiber GFRM100 (Bluetooth classic) */ |
| #define GFRM200 2 /* Google Fiber GFRM200 (Bluetooth LE) */ |
| #define TIARC 3 /* Texas Instruments CC2541ARC (Bluetooth LE) */ |
| |
| #define GFRM100_SEARCH_KEY_REPORT_ID 0xF7 |
| #define GFRM100_SEARCH_KEY_DOWN 0x0 |
| #define GFRM100_SEARCH_KEY_AUDIO_DATA 0x1 |
| #define GFRM100_SEARCH_KEY_UP 0x2 |
| |
| static u8 search_key_dn[3] = {0x40, 0x21, 0x02}; |
| static u8 search_key_up[3] = {0x40, 0x00, 0x00}; |
| |
| |
| static const char* rawEventToButton(u8* data, int size) |
| { |
| int code = 0xffff, key = 0xffff; // aka unset |
| static char msg[128]; |
| |
| if (size < 1) goto unknown; |
| |
| /* GFRM200: see "SRS_RC1534059_V0 11.pdf" */ |
| /* strings match vendor/broadcom/drivers/bt/3rdparty/embedded/brcm/linux/bthid/bthid.c */ |
| |
| code = data[0]; |
| switch (code) { |
| case 0x02: // GFRM200 usage page 0x01, 0x06 |
| if (size != 9) goto unknown; |
| key = data[3]; |
| switch (key) { |
| case 0x00: return "RELEASE2"; |
| case 0x1E: return "DIGIT_1"; |
| case 0x1F: return "DIGIT_2"; |
| case 0x20: return "DIGIT_3"; |
| case 0x21: return "DIGIT_4"; |
| case 0x22: return "DIGIT_5"; |
| case 0x23: return "DIGIT_6"; |
| case 0x24: return "DIGIT_7"; |
| case 0x25: return "DIGIT_8"; |
| case 0x26: return "DIGIT_9"; |
| case 0x27: return "DIGIT_0"; |
| case 0x2A: return "DIGIT_DEL"; |
| case 0x4F: return "RIGHT"; |
| case 0x50: return "LEFT"; |
| case 0x51: return "DOWN"; |
| case 0x52: return "UP"; |
| } |
| break; |
| |
| case 0x03: // GFRM200 usage page 0x01, 0x0c |
| if (size != 3) goto unknown; |
| key = data[1] | (data[2] << 8); |
| switch (key) { |
| case 0x00: return "RELEASE3"; |
| case 0x04: return "INFO"; |
| case 0x30: return "TV_BOX_POWER"; |
| case 0x40: return "MENU"; |
| case 0x41: return "OK"; |
| case 0x7F: return "TV_POWER"; |
| case 0x83: return "PREV"; |
| case 0x8D: return "GUIDE"; |
| case 0x8E: return "LIVE"; |
| case 0x94: return "EXIT"; |
| case 0x97: return "INPUT"; |
| case 0x9C: return "CH_UP"; |
| case 0x9D: return "CH_DOWN"; |
| case 0xB0: return "PLAY"; |
| case 0xB1: return "PAUSE"; |
| case 0xB2: return "RECORD"; |
| case 0xB3: return "FAST_FORWARD"; |
| case 0xB4: return "REWIND"; |
| case 0xB5: return "SKIP_FORWARD"; |
| case 0xB6: return "SKIP_BACKWARD"; |
| case 0xB7: return "STOP"; |
| case 0xE2: return "MUTE"; |
| case 0xE9: return "VOL_UP"; |
| case 0xEA: return "VOL_DOWN"; |
| case 0x221: return "SEARCH"; |
| case 0x224: return "BACK"; |
| } |
| break; |
| |
| case 0x81: // GFRM200 usage page 0xff, 0x00 |
| if (size != 2) goto unknown; |
| key = data[1]; |
| switch (key) { |
| case 0x00: return "RELEASE81"; |
| } |
| break; |
| |
| case 0x12: // GFRM100 type 0x12 |
| if (size != 3) goto unknown; |
| key = data[1]; |
| switch (key) { |
| case 0x00: return "RELEASE12"; |
| case 0x82: return "TV_POWER"; |
| } |
| break; |
| |
| case 0x13: // GFRM100 type 0x13 |
| if (size != 2) goto unknown; |
| key = data[1]; |
| sprintf(msg, "BATTERY_LEVEL=%d", key); // percent 0-100? |
| return msg; |
| break; |
| |
| case 0x40: // GFRM100 type 0x40 |
| if (size != 3) goto unknown; |
| key = data[1] | (data[2] << 8); |
| switch (key) { |
| case 0x00: return "RELEASE40"; |
| case 0x04: return "INFO"; |
| case 0x41: return "OK"; |
| case 0x42: return "UP"; |
| case 0x43: return "DOWN"; |
| case 0x44: return "LEFT"; |
| case 0x45: return "RIGHT"; |
| case 0x30: return "TV_BOX_POWER"; |
| case 0x40: return "MENU"; |
| case 0x82: return "INPUT"; |
| case 0x83: return "PREV"; |
| case 0x8D: return "GUIDE"; |
| case 0x9C: return "CH_UP"; |
| case 0x9D: return "CH_DOWN"; |
| case 0xB0: return "PLAY"; |
| case 0xB1: return "PAUSE"; |
| case 0xB2: return "RECORD"; |
| case 0xB3: return "FAST_FORWARD"; |
| case 0xB4: return "REWIND"; |
| case 0xB5: return "SKIP_FORWARD"; |
| case 0xB6: return "SKIP_BACKWARD"; |
| case 0xB7: return "STOP"; |
| case 0xE2: return "MUTE"; |
| case 0xE9: return "VOL_UP"; |
| case 0xEA: return "VOL_DOWN"; |
| case 0x204: return "EXIT"; |
| case 0x224: return "BACK"; |
| } |
| break; |
| |
| case 0x41: // GFRM100 type 0x41 |
| if (size != 2) goto unknown; |
| key = data[1]; |
| switch (key) { |
| case 0x00: return "RELEASE41"; |
| case 0x1E: return "DIGIT_1"; |
| case 0x1F: return "DIGIT_2"; |
| case 0x20: return "DIGIT_3"; |
| case 0x21: return "DIGIT_4"; |
| case 0x22: return "DIGIT_5"; |
| case 0x23: return "DIGIT_6"; |
| case 0x24: return "DIGIT_7"; |
| case 0x25: return "DIGIT_8"; |
| case 0x26: return "DIGIT_9"; |
| case 0x27: return "DIGIT_0"; |
| case 0x2a: return "DIGIT_DEL"; |
| } |
| break; |
| |
| case 0xf7: // GFRM100 type 0xf7 |
| if (size != 10) goto unknown; |
| key = data[1]; |
| switch (key) { |
| case 0x00: return "SEARCH"; |
| case 0x02: return "RELEASEF7"; |
| } |
| break; |
| } |
| unknown: |
| sprintf(msg, "UNKNOWN=%04x-%04x-%04x", code, key, size); |
| return msg; |
| } |
| |
| static void dataToHexString(char* str, int strLen, u8* data, int dataLen) |
| { |
| static char* hex = "0123456789abcdef"; |
| char* sp = str; |
| char* esp = str + strLen; |
| u8* dp = data; |
| u8* edp = data + dataLen; |
| |
| if (strLen < 1) return; |
| |
| while (sp + 3 < esp && dp < edp) { |
| *sp++ = hex[(*dp >> 4) & 0x0f]; |
| *sp++ = hex[(*dp >> 0) & 0x0f]; |
| dp++; |
| } |
| *sp = '\0'; |
| } |
| |
| static void logButton(struct hid_device *hdev, struct hid_report *report, |
| u8 *data, int size) |
| { |
| char dataStr[1024]; |
| const char* button = rawEventToButton(data, size); |
| |
| dataToHexString(dataStr, sizeof dataStr, data, size); |
| |
| // for backward compatibility with log parsers, use this format: |
| // BTHID bthid_write: [00241cb74c8f]Sending report to HID: {1f4102} -> KEY_DIGIT_2 |
| printk(KERN_INFO "BTHID bthid_write: Sending report to HID: {%s} -> KEY_%s\n", dataStr, button); |
| } |
| |
| static int gfrm_input_mapping(struct hid_device *hdev, struct hid_input *hi, |
| struct hid_field *field, struct hid_usage *usage, |
| unsigned long **bit, int *max) |
| { |
| int hdev_type = (int)hid_get_drvdata(hdev); |
| |
| if (hdev_type == GFRM100) { |
| if (usage->hid == (HID_UP_CONSUMER | 0x4)) { |
| /* Consumer.0004 -> KEY_INFO */ |
| hid_map_usage_clear(hi, usage, bit, max, EV_KEY, KEY_INFO); |
| return 1; |
| } |
| |
| if (usage->hid == (HID_UP_CONSUMER | 0x41)) { |
| /* Consumer.0041 -> KEY_OK */ |
| hid_map_usage_clear(hi, usage, bit, max, EV_KEY, KEY_OK); |
| return 1; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int gfrm_input_mapped(struct hid_device *hdev, struct hid_input *hi, |
| struct hid_field *field, struct hid_usage *usage, |
| unsigned long **bit, int *max) |
| { |
| int hdev_type = (int)hid_get_drvdata(hdev); |
| |
| if (hdev_type == TIARC) { |
| if (usage->code == KEY_LEFTMETA) { |
| hid_map_usage(hi, usage, bit, max, usage->type, KEY_MENU); |
| } else if (usage->code == KEY_BACKSPACE) { |
| hid_map_usage(hi, usage, bit, max, usage->type, KEY_BACK); |
| } else if (usage->code == KEY_MUTE) { |
| hid_map_usage(hi, usage, bit, max, usage->type, KEY_PROGRAM); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int gfrm_raw_event(struct hid_device *hdev, struct hid_report *report, |
| u8 *data, int size) |
| { |
| int hdev_type = (int)hid_get_drvdata(hdev); |
| int ret = 0; |
| |
| logButton(hdev, report, data, size); |
| |
| if (hdev_type != GFRM100) |
| return 0; |
| |
| if (size < 2 || data[0] != GFRM100_SEARCH_KEY_REPORT_ID) |
| return 0; |
| |
| /* |
| * Convert GFRM100 Search key reports into Consumer.0221 (Key.Search) |
| * reports. Ignore audio data. |
| */ |
| switch (data[1]) { |
| case GFRM100_SEARCH_KEY_DOWN: |
| ret = hid_report_raw_event(hdev, HID_INPUT_REPORT, search_key_dn, |
| sizeof(search_key_dn), 1); |
| break; |
| |
| case GFRM100_SEARCH_KEY_AUDIO_DATA: |
| break; |
| |
| case GFRM100_SEARCH_KEY_UP: |
| ret = hid_report_raw_event(hdev, HID_INPUT_REPORT, search_key_up, |
| sizeof(search_key_up), 1); |
| break; |
| |
| default: |
| break; |
| } |
| |
| return (ret < 0) ? ret : -1; |
| } |
| |
| static int gfrm_probe(struct hid_device *hdev, const struct hid_device_id *id) |
| { |
| int ret; |
| |
| hid_set_drvdata(hdev, (void *)id->driver_data); |
| |
| ret = hid_parse(hdev); |
| if (ret) |
| goto done; |
| |
| if (id->driver_data == GFRM100) { |
| /* |
| * GFRM100 HID Report Descriptor does not describe the Search |
| * key reports. Thus, we need to add it manually here, so that |
| * those reports reach gfrm_raw_event() from hid_input_report(). |
| */ |
| if (!hid_register_report(hdev, HID_INPUT_REPORT, |
| GFRM100_SEARCH_KEY_REPORT_ID)) { |
| ret = -ENOMEM; |
| goto done; |
| } |
| } |
| |
| ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); |
| done: |
| return ret; |
| } |
| |
| static void gfrm_remove(struct hid_device *hdev) |
| { |
| hid_hw_stop(hdev); |
| hid_set_drvdata(hdev, NULL); |
| } |
| |
| static const struct hid_device_id gfrm_devices[] = { |
| { HID_BLUETOOTH_DEVICE(0x58, 0x2000), |
| .driver_data = GFRM100 }, |
| { HID_BLUETOOTH_DEVICE(0x471, 0x2210), |
| .driver_data = GFRM200 }, |
| { HID_BLUETOOTH_DEVICE(0xD, 0x0), |
| .driver_data = TIARC }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(hid, gfrm_devices); |
| |
| static struct hid_driver gfrm_driver = { |
| .name = "gfrm", |
| .id_table = gfrm_devices, |
| .probe = gfrm_probe, |
| .remove = gfrm_remove, |
| .input_mapping = gfrm_input_mapping, |
| .input_mapped = gfrm_input_mapped, |
| .raw_event = gfrm_raw_event, |
| }; |
| |
| static int __init gfrm_init(void) |
| { |
| return hid_register_driver(&gfrm_driver); |
| } |
| |
| static void __exit gfrm_exit(void) |
| { |
| hid_unregister_driver(&gfrm_driver); |
| } |
| |
| module_init(gfrm_init); |
| module_exit(gfrm_exit); |
| MODULE_LICENSE("GPL"); |