Last active
July 11, 2026 18:53
-
-
Save binarycraft007/8000c26ba90b7e7ccfe35a85444812fd to your computer and use it in GitHub Desktop.
HID driver for ASUS Vivobook S15 S5507 Keyboard
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * HID driver for ASUS Vivobook S15 S5507 Keyboard | |
| * | |
| * Copyright (c) 2024 Elliot Huang (keep.it.sns@gmail.com) | |
| * | |
| * This driver handles special function keys for keyboard backlight control, | |
| * Fn-Lock, and re-maps certain vendor-specific keys to standard consumer | |
| * control usages (e.g., display brightness). It also handles suspend and | |
| * resume by turning off the backlight on suspend and restoring the full | |
| * device state on resume. | |
| */ | |
| #include <linux/hid.h> | |
| #include <linux/module.h> | |
| #include <linux/workqueue.h> | |
| #include <linux/atomic.h> | |
| #include <linux/leds.h> | |
| #define VID_ASUS 0x0B05 | |
| #define PID_VIVOBOOK_S15_S5507_KEYBOARD 0x4543 | |
| /* | |
| * The device sends vendor-specific reports with ID 0x5A for special keys. | |
| * We intercept these and either schedule work or remap them to standard | |
| * reports. | |
| */ | |
| #define ASUS_VENDOR_REPORT_ID 0x5A | |
| /* Key codes from vendor report */ | |
| #define VENDOR_KEY_FNLOCK 0x4E | |
| #define VENDOR_KEY_BACKLIGHT 0xC7 | |
| #define VENDOR_KEY_BRIGHTNESS_DOWN 0x10 | |
| #define VENDOR_KEY_BRIGHTNESS_UP 0x20 | |
| #define VENDOR_KEY_RELEASE 0x00 | |
| /* | |
| * We remap brightness keys to a custom report with ID 0x37, which the | |
| * device's report descriptor seems to handle as consumer control. | |
| */ | |
| #define CONSUMER_REPORT_ID 0x37 | |
| #define CONSUMER_USAGE_BRIGHTNESS_DOWN 0x70 | |
| #define CONSUMER_USAGE_BRIGHTNESS_UP 0x6F | |
| #define CONSUMER_USAGE_RELEASE 0x00 | |
| enum work_type { | |
| WORK_TYPE_INIT, | |
| WORK_TYPE_BACKLIGHT, | |
| WORK_TYPE_FNLOCK, | |
| }; | |
| struct vivobook_s15_kbd { | |
| struct hid_device *hdev; | |
| struct work_struct work; | |
| /* Bitmask of pending work items */ | |
| atomic_t pending_work; | |
| /* Device state */ | |
| u8 backlight_brightness; | |
| bool fn_lock_state; | |
| /* State for remapping key-up events */ | |
| bool remap_next_keyup; | |
| /* LED Class device for keyboard backlight */ | |
| struct led_classdev kbd_backlight; | |
| }; | |
| /* | |
| * The following functions send HID_FEATURE_REPORT commands to the device. | |
| * They return 0 on success, or a negative error code on failure. | |
| */ | |
| static int set_init_unk_1(struct hid_device *hdev) | |
| { | |
| u8 *buf; | |
| int ret; | |
| buf = kmalloc(64, GFP_KERNEL); | |
| if (!buf) | |
| return -ENOMEM; | |
| buf[0] = 0x5A; | |
| buf[1] = 0x05; | |
| buf[2] = 0x20; | |
| buf[3] = 0x31; | |
| buf[4] = 0x00; | |
| buf[5] = 0x08; | |
| ret = hid_hw_raw_request(hdev, 0x5A, buf, 64, HID_FEATURE_REPORT, HID_REQ_SET_REPORT); | |
| kfree(buf); | |
| return ret < 0 ? ret : 0; | |
| } | |
| static int set_init_unk_2(struct hid_device *hdev) | |
| { | |
| u8 *buf; | |
| int ret; | |
| buf = kmalloc(64, GFP_KERNEL); | |
| if (!buf) | |
| return -ENOMEM; | |
| buf[0] = 0x5A; | |
| buf[1] = 0xD0; | |
| buf[2] = 0x8F; | |
| buf[3] = 0x01; | |
| ret = hid_hw_raw_request(hdev, 0x5A, buf, 64, HID_FEATURE_REPORT, HID_REQ_SET_REPORT); | |
| kfree(buf); | |
| return ret < 0 ? ret : 0; | |
| } | |
| static int set_brightness(struct hid_device *hdev, u8 brightness) | |
| { | |
| u8 *buf; | |
| int ret; | |
| buf = kmalloc(64, GFP_KERNEL); | |
| if (!buf) | |
| return -ENOMEM; | |
| buf[0] = 0x5A; | |
| buf[1] = 0xBA; | |
| buf[2] = 0xC5; | |
| buf[3] = 0xC4; | |
| buf[4] = brightness; | |
| ret = hid_hw_raw_request(hdev, 0x5A, buf, 64, HID_FEATURE_REPORT, HID_REQ_SET_REPORT); | |
| kfree(buf); | |
| return ret < 0 ? ret : 0; | |
| } | |
| static int set_fn_lock(struct hid_device *hdev, bool fn_lock) | |
| { | |
| u8 *buf; | |
| int ret; | |
| buf = kmalloc(64, GFP_KERNEL); | |
| if (!buf) | |
| return -ENOMEM; | |
| buf[0] = 0x5A; | |
| buf[1] = 0xD0; | |
| buf[2] = 0x4E; | |
| buf[3] = fn_lock; | |
| ret = hid_hw_raw_request(hdev, 0x5A, buf, 64, HID_FEATURE_REPORT, HID_REQ_SET_REPORT); | |
| kfree(buf); | |
| return ret < 0 ? ret : 0; | |
| } | |
| static int vivobook_s15_kbd_bl_set(struct led_classdev *led_cdev, | |
| enum led_brightness brightness) | |
| { | |
| struct vivobook_s15_kbd *priv = container_of(led_cdev, | |
| struct vivobook_s15_kbd, kbd_backlight); | |
| int ret; | |
| ret = set_brightness(priv->hdev, brightness); | |
| if (ret == 0) | |
| priv->backlight_brightness = brightness; | |
| return ret; | |
| } | |
| static enum led_brightness vivobook_s15_kbd_bl_get(struct led_classdev *led_cdev) | |
| { | |
| struct vivobook_s15_kbd *priv = container_of(led_cdev, | |
| struct vivobook_s15_kbd, kbd_backlight); | |
| return priv->backlight_brightness; | |
| } | |
| static void vivobook_s15_work_handler(struct work_struct *work) | |
| { | |
| struct vivobook_s15_kbd *priv = container_of(work, struct vivobook_s15_kbd, work); | |
| struct hid_device *hdev = priv->hdev; | |
| int ret; | |
| if (test_and_clear_bit(WORK_TYPE_BACKLIGHT, (void *)&priv->pending_work)) { | |
| u8 new_brightness = (priv->backlight_brightness + 1); | |
| if (new_brightness > 3) | |
| new_brightness = 0; | |
| led_set_brightness_sync(&priv->kbd_backlight, new_brightness); | |
| } | |
| if (test_and_clear_bit(WORK_TYPE_FNLOCK, (void *)&priv->pending_work)) { | |
| bool new_fn_lock = !priv->fn_lock_state; | |
| ret = set_fn_lock(hdev, new_fn_lock); | |
| if (ret == 0) | |
| priv->fn_lock_state = new_fn_lock; | |
| else | |
| dev_err(&hdev->dev, "Failed to set Fn-Lock: %d\n", ret); | |
| } | |
| if (test_and_clear_bit(WORK_TYPE_INIT, (void *)&priv->pending_work)) { | |
| dev_info(&hdev->dev, "Initializing keyboard\n"); | |
| set_init_unk_1(hdev); | |
| set_init_unk_2(hdev); | |
| led_set_brightness_sync(&priv->kbd_backlight, priv->backlight_brightness); | |
| set_fn_lock(hdev, priv->fn_lock_state); | |
| } | |
| } | |
| static void schedule_vivobook_work(struct vivobook_s15_kbd *priv, enum work_type type) | |
| { | |
| set_bit(type, (void *)&priv->pending_work); | |
| schedule_work(&priv->work); | |
| } | |
| static int remap_to_consumer_control(struct vivobook_s15_kbd *priv, u8 *data, u8 code) | |
| { | |
| data[0] = CONSUMER_REPORT_ID; | |
| data[1] = code; | |
| data[2] = 0x00; | |
| priv->remap_next_keyup = (code != CONSUMER_USAGE_RELEASE); | |
| return 3; /* New report size is 3 bytes */ | |
| } | |
| static int vivobook_s15_raw_event(struct hid_device *hdev, | |
| struct hid_report *report, u8 *data, int size) | |
| { | |
| struct vivobook_s15_kbd *priv = hid_get_drvdata(hdev); | |
| /* We are only interested in vendor-defined input reports */ | |
| if (report->type != HID_INPUT_REPORT || data[0] != ASUS_VENDOR_REPORT_ID) | |
| return 0; | |
| /* The vendor report for special keys is 6 bytes long */ | |
| if (size != 6) | |
| return 0; | |
| switch (data[1]) { | |
| case VENDOR_KEY_FNLOCK: | |
| schedule_vivobook_work(priv, WORK_TYPE_FNLOCK); | |
| break; | |
| case VENDOR_KEY_BACKLIGHT: | |
| schedule_vivobook_work(priv, WORK_TYPE_BACKLIGHT); | |
| break; | |
| case VENDOR_KEY_BRIGHTNESS_DOWN: | |
| return remap_to_consumer_control(priv, data, CONSUMER_USAGE_BRIGHTNESS_DOWN); | |
| case VENDOR_KEY_BRIGHTNESS_UP: | |
| return remap_to_consumer_control(priv, data, CONSUMER_USAGE_BRIGHTNESS_UP); | |
| case VENDOR_KEY_RELEASE: | |
| if (priv->remap_next_keyup) | |
| return remap_to_consumer_control(priv, data, CONSUMER_USAGE_RELEASE); | |
| break; | |
| default: | |
| /* Let unhandled vendor keys pass through if needed, or drop them */ | |
| break; | |
| } | |
| /* Drop the original vendor report, as we have handled it. */ | |
| return 0; | |
| } | |
| static int vivobook_s15_probe(struct hid_device *hdev, const struct hid_device_id *id) | |
| { | |
| struct vivobook_s15_kbd *priv; | |
| int ret; | |
| priv = devm_kzalloc(&hdev->dev, sizeof(*priv), GFP_KERNEL); | |
| if (!priv) | |
| return -ENOMEM; | |
| priv->hdev = hdev; | |
| hid_set_drvdata(hdev, priv); | |
| /* Set initial default state */ | |
| priv->backlight_brightness = 1; | |
| priv->fn_lock_state = false; | |
| priv->remap_next_keyup = false; | |
| atomic_set(&priv->pending_work, 0); | |
| INIT_WORK(&priv->work, vivobook_s15_work_handler); | |
| ret = hid_parse(hdev); | |
| if (ret) { | |
| dev_err(&hdev->dev, "hid_parse failed: %d\n", ret); | |
| return ret; | |
| } | |
| ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); | |
| if (ret) { | |
| dev_err(&hdev->dev, "hid_hw_start failed: %d\n", ret); | |
| return ret; | |
| } | |
| priv->kbd_backlight.name = "asus::kbd_backlight"; | |
| priv->kbd_backlight.max_brightness = 3; | |
| priv->kbd_backlight.brightness_set_blocking = vivobook_s15_kbd_bl_set; | |
| priv->kbd_backlight.brightness_get = vivobook_s15_kbd_bl_get; | |
| priv->kbd_backlight.flags = LED_BRIGHT_HW_CHANGED; | |
| ret = devm_led_classdev_register(&hdev->dev, &priv->kbd_backlight); | |
| if (ret) { | |
| dev_err(&hdev->dev, "Failed to register kbd_backlight LED: %d\n", ret); | |
| return ret; | |
| } | |
| /* Schedule work to perform initial setup */ | |
| schedule_vivobook_work(priv, WORK_TYPE_INIT); | |
| return 0; | |
| } | |
| static void vivobook_s15_remove(struct hid_device *hdev) | |
| { | |
| struct vivobook_s15_kbd *priv = hid_get_drvdata(hdev); | |
| cancel_work_sync(&priv->work); | |
| hid_hw_stop(hdev); | |
| } | |
| static int vivobook_s15_suspend(struct hid_device *hdev, pm_message_t message) | |
| { | |
| struct vivobook_s15_kbd *priv = hid_get_drvdata(hdev); | |
| if (priv->backlight_brightness > 0) | |
| set_brightness(hdev, LED_OFF); | |
| return 0; | |
| } | |
| static int __maybe_unused vivobook_s15_reset_resume(struct hid_device *hdev) | |
| { | |
| struct vivobook_s15_kbd *priv = hid_get_drvdata(hdev); | |
| dev_info(&hdev->dev, "Resuming, restoring keyboard state\n"); | |
| /* | |
| * On resume, run the full initialization sequence and restore the | |
| * backlight and Fn-Lock states. | |
| */ | |
| set_init_unk_1(hdev); | |
| set_init_unk_2(hdev); | |
| led_set_brightness_sync(&priv->kbd_backlight, priv->backlight_brightness); | |
| set_fn_lock(hdev, priv->fn_lock_state); | |
| return 0; | |
| } | |
| static const struct hid_device_id vivobook_s15_kbd_id_table[] = { | |
| { HID_I2C_DEVICE(VID_ASUS, PID_VIVOBOOK_S15_S5507_KEYBOARD) }, | |
| { } /* Terminating entry */ | |
| }; | |
| MODULE_DEVICE_TABLE(hid, vivobook_s15_kbd_id_table); | |
| static struct hid_driver vivobook_s15_kbd_driver = { | |
| .name = "hid-asus-vivobook-s15", | |
| .id_table = vivobook_s15_kbd_id_table, | |
| .probe = vivobook_s15_probe, | |
| .remove = vivobook_s15_remove, | |
| .raw_event = vivobook_s15_raw_event, | |
| #ifdef CONFIG_PM | |
| .suspend = vivobook_s15_suspend, | |
| .reset_resume = vivobook_s15_reset_resume, | |
| #endif | |
| }; | |
| module_hid_driver(vivobook_s15_kbd_driver); | |
| MODULE_LICENSE("GPL"); | |
| MODULE_AUTHOR("Elliot Huang <keep.it.sns@gmail.com>"); | |
| MODULE_DESCRIPTION("HID driver for ASUS Vivobook S15 S5507 Keyboard"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment