HP's Omen series comes with a great suite of features via the Omen Gaming Hub. These include a full-fledged lighting studio for the keyboard backlights (it even ships with a built-in audio visualiser and a bunch of themes), GPU performance modes, fan speed support and statistics. Unfortunately, this technology is only available on Windows through their proprietary app, with no plans from HP's end to expand support for Linux users as well.
+ +This makes our lives harder, as none of these features are available for Linux out-of-the-box. While there has been significant effort from the community to reverse-engineer parts of the stack and develop support for Linux, most features still remain unsupported. The Linux ecosystem still remains incomplete and undocumented. Through this blog series, I will be attempting to implement some of these features myself, something like asusctl, but for Omen laptops. My target is to create at least a complete utility and drivers to interface with software like OpenRGB.
Fortunately much of the groundwork has already been laid down by community members. Most significantly, pelrun has done a significant amount of reverse-engineering of the interfaces exposed by HP's firmware. Their work documents parts of the WMI/ACPI interface used by the Omen Gaming Hub on Windows and shows how they can be triggered outside of Windows as well. Their fork of the hp-wmi driver provided great insight into the command structure used by the firmware. However, the current solution is not a fully-integrated Linux solution yet. My goal with this project is to build on that and move towards something more cohesive.
+ +The Omen series exposes device controls through ACPI methods that are accessible through WMI. Unfortunately, HP does not make any documentation for these public, forcing us to poke around quite a bit in order to find relevant values. Like for example, the GUID for the WMI interface on my system was:
+ +5FB7F034-2C63-45E9-BE91-3D44E2C707E4
+
+ This will enable us to access the firmware like so:
+ +How Omen Features Work
+Windows
+(RGB • Fans • GPU modes)
Linux (Goal)
+(RGB • Fans • GPU modes)
To test this out, I wrote a minimal external kernel module, that registered itself with the WMI subsystem and attached itself to the firmware interface exposed by the system:
+ +#include <linux/module.h>
+#include <linux/wmi.h>
+
+#define HPWMI_BIOS_GUID "5FB7F034-2C63-45E9-BE91-3D44E2C70733"
+
+static int omen_wmi_probe(struct wmi_device *wdev, const void *context)
+{
+ pr_info("omen_wmi: device detected\n");
+ return 0;
+}
+
+static void omen_wmi_remove(struct wmi_device *wdev)
+{
+ pr_info("omen_wmi: device removed\n");
+}
+
+static const struct wmi_device_id omen_wmi_id_table[] = {
+ { HPWMI_BIOS_GUID, NULL },
+ { }
+};
+
+static struct wmi_driver omen_wmi_driver = {
+ .driver = {
+ .name = "omen_wmi",
+ },
+ .id_table = omen_wmi_id_table,
+ .probe = omen_wmi_probe,
+ .remove = omen_wmi_remove,
+};
+
+module_wmi_driver(omen_wmi_driver);
+
+MODULE_LICENSE("GPL");
+
+ The Makefile is a separate file in the same directory (save it as Makefile with no extension):
obj-m += omen_wmi_bridge.o
+
+all:
+ make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
+
+clean:
+ make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
+
+ In order to run this module:
+ +make
+sudo rmmod omen_wmi_bridge # No need to run this the first time
+sudo insmod omen_wmi_bridge.ko
+sudo dmesg -w
+
+ Et voilà, you should see the driver when you run lsmod | grep omen. However, this was a very minimal example to show the structure of the driver. However, we aren't exposing anything to the userspace yet. In order to control the backlight, we need to create interfaces that userspace programs will be able to interact with.
The ACPI interface used by Omen's firmware expects a 128-byte buffer as input. A significant chunk has already been reverse-engineered by pelrun and reveals some important details about the internals of the firmware and the structure of this buffer. The keyboard is divided into 4 different zones (Zone0 - Right, Zone1- Middle, Zone2 - Left, Zone3 - WASD) and the buffer looks like this:
+ +Firmware Command Buffer (128 bytes)
+ +PASS status
Unknown fields
Z1 R
Z1 G
Z1 B
Z2 R
Z2 G
Z2 B
Z3 R
Z3 G
Z3 B
Z4 R
Z4 G
Z4 B
Unknown / unused
If you want to play around with this yourself, here is the full kernel module:
+ +omen_wmi_bridge.c — full source
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/acpi.h>
+#include <linux/wmi.h>
+#include <linux/kobject.h>
+#include <linux/sysfs.h>
+
+#define HPWMI_BIOS_GUID "5FB7F034-2C63-45E9-BE91-3D44E2C707E4"
+
+#define HPWMI_FOURZONE 0x20009
+
+#define HPWMI_FOURZONE_COLOR_GET 2
+#define HPWMI_FOURZONE_COLOR_SET 3
+#define HPWMI_FOURZONE_BRIGHT_GET 4
+#define HPWMI_FOURZONE_BRIGHT_SET 5
+#define HPWMI_FOURZONE_ANIM_GET 6
+#define HPWMI_FOURZONE_ANIM_SET 7
+
+#define ZONE_BASE 25
+#define ZONE_COUNT 4
+
+struct bios_args {
+ u32 signature;
+ u32 command;
+ u32 commandtype;
+ u32 datasize;
+ u8 data[128];
+};
+
+static struct kobject *omen_kobj;
+
+/* ---------- WMI helper ---------- */
+
+static int omen_wmi_query(int cmdtype, u8 *buffer)
+{
+ struct bios_args args = {0};
+ struct acpi_buffer input;
+ struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
+ union acpi_object *obj;
+ int ret = -EINVAL;
+
+ args.signature = 0x55434553;
+ args.command = HPWMI_FOURZONE;
+ args.commandtype = cmdtype;
+ args.datasize = 128;
+
+ if (buffer && cmdtype == HPWMI_FOURZONE_COLOR_SET)
+ memcpy(args.data, buffer, 128);
+
+ input.length = sizeof(args);
+ input.pointer = &args;
+
+ wmi_evaluate_method(HPWMI_BIOS_GUID, 0, 3, &input, &output);
+
+ obj = output.pointer;
+
+ if (!obj || obj->type != ACPI_TYPE_BUFFER)
+ goto out;
+
+ if (buffer && cmdtype == HPWMI_FOURZONE_COLOR_GET)
+ memcpy(buffer, obj->buffer.pointer + 8, 128);
+
+ ret = 0;
+
+out:
+ kfree(obj);
+ return ret;
+}
+
+/* ---------- zone interface ---------- */
+
+static ssize_t zone_show(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ char *buf)
+{
+ u8 state[128];
+ int zone = attr->attr.name[4] - '0';
+ int off = ZONE_BASE + zone * 3;
+
+ if (omen_wmi_query(HPWMI_FOURZONE_COLOR_GET, state))
+ return -EIO;
+
+ return sprintf(buf, "%02x%02x%02x\n",
+ state[off], state[off+1], state[off+2]);
+}
+
+static ssize_t zone_store(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ const char *buf,
+ size_t count)
+{
+ u8 state[128];
+ unsigned int rgb;
+ int zone = attr->attr.name[4] - '0';
+ int off = ZONE_BASE + zone * 3;
+
+ if (kstrtouint(buf, 16, &rgb))
+ return -EINVAL;
+
+ if (omen_wmi_query(HPWMI_FOURZONE_COLOR_GET, state))
+ return -EIO;
+
+ state[off] = (rgb >> 16) & 0xff;
+ state[off+1] = (rgb >> 8) & 0xff;
+ state[off+2] = rgb & 0xff;
+
+ if (omen_wmi_query(HPWMI_FOURZONE_COLOR_SET, state))
+ return -EIO;
+
+ return count;
+}
+
+/* ---------- raw buffer ---------- */
+
+static ssize_t raw_show(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ char *buf)
+{
+ u8 state[128];
+
+ if (omen_wmi_query(HPWMI_FOURZONE_COLOR_GET, state))
+ return -EIO;
+
+ memcpy(buf, state, 128);
+ return 128;
+}
+
+static ssize_t raw_store(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ const char *buf,
+ size_t count)
+{
+ u8 state[128];
+
+ if (count != 128)
+ return -EINVAL;
+
+ memcpy(state, buf, 128);
+
+ if (omen_wmi_query(HPWMI_FOURZONE_COLOR_SET, state))
+ return -EIO;
+
+ return count;
+}
+
+/* ---------- command interface ---------- */
+
+static ssize_t cmd_store(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ const char *buf,
+ size_t count)
+{
+ struct bios_args args = {0};
+ struct acpi_buffer input;
+ struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
+ union acpi_object *obj;
+ unsigned int cmdtype;
+ unsigned int value = 0;
+
+ sscanf(buf, "%u %x", &cmdtype, &value);
+
+ args.signature = 0x55434553;
+ args.command = HPWMI_FOURZONE;
+ args.commandtype = cmdtype;
+ args.datasize = 128;
+
+ memset(args.data, 0, 128);
+ args.data[0] = value;
+
+ input.length = sizeof(args);
+ input.pointer = &args;
+
+ wmi_evaluate_method(HPWMI_BIOS_GUID, 0, 3, &input, &output);
+
+ obj = output.pointer;
+
+ if (obj && obj->type == ACPI_TYPE_BUFFER)
+ print_hex_dump(KERN_INFO,
+ "omen_cmd: ",
+ DUMP_PREFIX_OFFSET,
+ 16, 1,
+ obj->buffer.pointer,
+ obj->buffer.length,
+ false);
+
+ kfree(obj);
+ return count;
+}
+
+/* ---------- sysfs ---------- */
+
+#define ZONE_ATTR(n) \
+static struct kobj_attribute zone##n##_attr = \
+__ATTR(zone##n, 0664, zone_show, zone_store);
+
+ZONE_ATTR(0)
+ZONE_ATTR(1)
+ZONE_ATTR(2)
+ZONE_ATTR(3)
+
+static struct kobj_attribute raw_attr =
+ __ATTR(raw, 0664, raw_show, raw_store);
+
+static struct kobj_attribute cmd_attr =
+ __ATTR(cmd, 0220, NULL, cmd_store);
+
+static struct attribute *attrs[] = {
+ &zone0_attr.attr,
+ &zone1_attr.attr,
+ &zone2_attr.attr,
+ &zone3_attr.attr,
+ &raw_attr.attr,
+ &cmd_attr.attr,
+ NULL
+};
+
+static struct attribute_group attr_group = {
+ .attrs = attrs,
+};
+
+/* ---------- module ---------- */
+
+static int __init omen_init(void)
+{
+ int ret;
+
+ omen_kobj = kobject_create_and_add("omen_rgb", kernel_kobj);
+ if (!omen_kobj)
+ return -ENOMEM;
+
+ ret = sysfs_create_group(omen_kobj, &attr_group);
+
+ pr_info("omen rgb module loaded\n");
+ return ret;
+}
+
+static void __exit omen_exit(void)
+{
+ kobject_put(omen_kobj);
+ pr_info("omen rgb module unloaded\n");
+}
+
+module_init(omen_init);
+module_exit(omen_exit);
+
+MODULE_LICENSE("GPL");
+