From b386fd350b90a4c73abaf961470b22dc43c8753d Mon Sep 17 00:00:00 2001 From: Joseph East Date: Sun, 30 Mar 2025 22:41:56 +1030 Subject: [PATCH 1/9] Include driver for Razer Hanbo AIO --- Documentation/hwmon/razer_hanbo.rst | 163 +++++ drivers/hwmon/Makefile | 2 +- drivers/hwmon/dkms.conf.in | 3 + drivers/hwmon/razer_hanbo.c | 893 ++++++++++++++++++++++++++++ 4 files changed, 1060 insertions(+), 1 deletion(-) create mode 100644 Documentation/hwmon/razer_hanbo.rst create mode 100644 drivers/hwmon/razer_hanbo.c diff --git a/Documentation/hwmon/razer_hanbo.rst b/Documentation/hwmon/razer_hanbo.rst new file mode 100644 index 0000000..ee96b7f --- /dev/null +++ b/Documentation/hwmon/razer_hanbo.rst @@ -0,0 +1,163 @@ +.. SPDX-License-Identifier: GPL-2.0-or-later + +Kernel driver razer_hanbo +================================= + +Supported devices: + +* Razer Hanbo 360mm + +Author: Joseph East + +Description +----------- + +This driver enables hardware monitoring support for the Razer Hanbo all-in-one +CPU liquid coolers. Available sensors are pump and fan speeds in RPM, their +PWM duty cycles as percentages, coolant temperature and other state trackers. +Also available through debugfs is the firmware version. This has been +validated against OEM firmware 1.2.0, it is unknown whether this driver is +compatible with other versions. + +Like the OEM software the pump and fans are unable to be directly controlled. +Instead there are four profile modes which are selectable via sysfs to change +device behaviour explained later. The pump and fans can run on different +profiles. It is not possible to control individual fans in terms of thermals, +they are treated as the one entity. + +Attaching fans is optional and allows them to be controlled from the device, +freeing motherboard resources. If they are not connected, the fan-related +sensors will report zeroes, this driver will not report an error. + +The addressable RGB LEDs are not supported in this driver and should be +controlled through userspace tools instead. + +Usage notes +----------- + +As these are USB HIDs, the driver can be loaded automatically by the kernel +and supports hot swapping. + +The Razer Hanbo has the following behaviours during startup: +* Device goes to 100% if the USB interface fails i.e. not connected. + This is the power-on and fault state. +* The previous active profile including curves is restored from hardware + when the USB interface is enumerated, driver present or not. This is the + running state. +* Lighting is a free-running ARGB spectrum cycling sequence regardless. + There are no other internal effect modes. + +Performance Profiles +The fan and pump can run independent performance profiles which are equivalent +to the OEM software. Referring to the sysfs entries table below: +1 = Quiet, 20% duty +2 = Normal, 50% duty +3 = Performance, 80% duty +4 = Curve mode + +Be aware that all the fan profiles rely on an external CPU temperature to +function. See curve mode notes below. + +The profiles can be changed by providing the profile number to the pwmX_enable +node. e.g. echo 3 > /sys/class/<...>/hwmonZ/pwm1_enable sets the pump to +performance mode. + +Profile 4 Curve Mode: +9 curve points correspond to +20 degrees C through +100 degrees C in 10 degree +steps where 'point 1' represents 20 degrees C. Each point is associated with a +1-byte PWM duty cycle from 20-100% (x14-x64) to drive the cooler whilst within +that temperature range. The AIO interpolates between points automatically. +Each point is written to individually using the tempX_auto_pointY_pwm nodes in +sysfs. When writing to these nodes, the driver will accept values between +20-100 inclusive (x14-x64) and clamp invalid values to the relevant extreme. + +e.g. echo 30 > /sys/class/<...>/hwmonZ/temp2_auto_point2_pwm to set fan curve +point 2 (30 degrees) to 30% PWM. + +Progressive fan curve PWM values must be equal to or higher than the previous +point throughout the curve. This is the responsibility of the user. +An invalid curve is reported upon attempting to switch to profile 4 via a +write error: Invalid argument error. Upon switching to profile 4 for either +the fan or pump, the respective curve is sanity checked and uploaded to the +AIO. If changes are made to the curve via sysfs post-switch you will need to +enable profile 4 again to upload the new curve. + +How do profiles know what the temperature is? + +For the pump, operation is autonomous as the reference temperature is +the internal liquid temperature in the AIO. This matches the value of the +temperature at temp1_input in sysfs, no hand-holding needed. + +For the fans, curves will be traversed based on CPU temperature feedback which +is provided via the temp2_input sysfs node. Temperature updates can occur at +any time. It can take between 3-10 seconds for a CPU temperature update +to be reflected in curve behaviour. As there are no timeouts, CPU temperature +updates do not go stale. The last written value will continue to be used as +the reference until it changes. This includes changing profiles. It is +unknown if this survives power cycles as the driver overrides the value +every time it is loaded. Like other sysfs nodes in this driver, the +temp2_input node has valid vaules between 0-100 with values outside this +range internally clamped. Negative numbers are treated as 0 for this node. + +The hwmon interface dictates that temperatures are to be transacted in +in millidegrees C. The Razer Hanbo resolves CPU temperatures in 1 degree +steps. The driver will accept a millidegree input and round as appropriate +before sending to the AIO. Liquid temperature is natively reported as +decidegrees from the AIO. + +As part of driver initialisation, a one-shot CPU temperature of 30 degrees C +is written along with a basic fan and pump curves. This is to prevent +activation of profile 4 with unknown curve parameters. It is assumed +that userspace tools will be used to manage fan operation. +It is not possible to change the temperature values of the curves, only the +duty cycles associated with them. + +Driver default curves- + +Temp: 20C 30C 40C 50C 60C 70C 80C 90C 100C +Fan: { 0x18, 0x1e, 0x28, 0x30, 0x3c, 0x51, 0x64, 0x64, 0x64 }; +Pump: { 0x14, 0x28, 0x3c, 0x50, 0x64, 0x64, 0x64, 0x64, 0x64 }; + +A feature of profile 4 is that it cannot be queried nor is it broadcast. +If the driver initiated curve mode, it will make profile 4 'sticky' when +reading pwmX_enable until the driver is commanded to change to another +profile. In the event that the driver is reloaded, knowledge of curve mode is +lost and sysfs will reflect the HID status reports which only show profiles +1-3. You cannot rely on the AIO to reliably give you its complete state. This +is only achieved if a profile change occurred during the connection lifecycle +as the driver would be aware of what it told the AIO. The driver state is +retained during sleep and resume but will be lost on shutdown. If one intends +to use profile 4 as their default it should be manually reloaded every time +the driver is started for accurate state tracking. It is not possible to +download curves from the AIO. + +Similarly, the CPU reference temperature at temp2_input when read only +reflects what the driver previously sent to the AIO in this session, not what +is actually in firmware. + +Sysfs entries +------------- + +============= ============================================= +fan1_input R: Pump speed (in rpm) +fan2_input R: Fan speed (in rpm) +temp1_input R: Coolant temperature (in millidegrees Celsius) +temp2_input RW: CPU feedback temperature (in millidegrees Celsius) +pwm1 R: Pump achieved PWM rate as a percentage +pwm2 R: Fan achieved PWM rate as a percentage +pwm1_enable R: Get pump active profile + W: Set profile from 1-4 +pwm2_enable R: Get fan active profile + W: Set profile from 1-4 +pwm1_setpoint R: Commanded RPM for the pump +pwm2_setpoint R: Commanded RPM for the fan +temp1_auto... W: Pump curve data points, PWM rate as a percentage. +temp2_auto... W: Fan curve data points, PWM rate as a percentage. +============= ============================================= + +Debugfs entries +--------------- + +================ ======================= +firmware_version Device firmware version +================ ======================= diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index 64207a8..d4a732b 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -1 +1 @@ -obj-m := nzxt-kraken2.o nzxt-grid3.o nzxt-kraken3.o nzxt-smart2.o +obj-m := nzxt-kraken2.o nzxt-grid3.o nzxt-kraken3.o nzxt-smart2.o razer_hanbo.o diff --git a/drivers/hwmon/dkms.conf.in b/drivers/hwmon/dkms.conf.in index a448580..ea48c30 100644 --- a/drivers/hwmon/dkms.conf.in +++ b/drivers/hwmon/dkms.conf.in @@ -13,4 +13,7 @@ DEST_MODULE_LOCATION[2]="/kernel/drivers/hwmon" BUILT_MODULE_NAME[3]="nzxt-smart2" DEST_MODULE_LOCATION[3]="/kernel/drivers/hwmon" +BUILT_MODULE_NAME[4]="razer_hanbo" +DEST_MODULE_LOCATION[4]="/kernel/drivers/hwmon" + AUTOINSTALL="yes" diff --git a/drivers/hwmon/razer_hanbo.c b/drivers/hwmon/razer_hanbo.c new file mode 100644 index 0000000..d3ced6d --- /dev/null +++ b/drivers/hwmon/razer_hanbo.c @@ -0,0 +1,893 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * hwmon driver for Razer Hanbo AIO CPU coolers. + * + * Copyright 2025 Joseph East + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRIVER_NAME "razer_hanbo" + +/* Device constraints from firmware */ +#define USB_VENDOR_ID_RAZER 0x1532 +#define USB_PRODUCT_ID_HANBO 0x0f35 + +#define STATUS_VALIDITY_MS (2 * 1000) +#define MAX_REPORT_LENGTH 64 +#define DUTY_CYCLE_MIN 20 +#define DUTY_CYCLE_MAX 100 +#define TEMPERATURE_MAX 100 +#define CUSTOM_CURVE_POINTS 9 + +/* Firmware command response signatures */ +#define FIRMWARE_STATUS_REPORT_ID 0x02 +#define PUMP_STATUS_REPORT_ID 0x13 +#define PUMP_ACK_REPORT_ID 0x15 +#define PUMP_CURVE_ACK_REPORT_ID 0x19 +#define FAN_STATUS_REPORT_ID 0x21 +#define FAN_PROFILE_ACK_REPORT_ID 0x23 +#define BRIGHTNESS_ACK_REPORT_ID 0x71 +#define BRIGHTNESS_STATUS_REPORT_ID 0x73 +#define RGB_MODE_SET_ACK_REPORT_ID 0x81 +#define RGB_MODE_STATUS_REPORT_ID 0x83 +#define CPU_TEMP_ACK_REPORT_ID 0xC1 +#define FAN_CURVE_ACK_REPORT_ID 0xC9 + +/* Firmware commands and templates */ +static const u8 get_firmware_ver_cmd[] = { 0x01, 0x01 }; +static const u8 get_pump_status_cmd[] = { 0x12, 0x01 }; +static const u8 set_pump_fan_cmd_template[] = { 0x14, 0x01, 0x00, 0x00 }; +static const u8 get_fan_status_cmd[] = { 0x20, 0x01 }; +static const u8 set_vcpu_temp_cmd_template[] = { 0xc0, 0x01, 0x00, 0x00, 0x1e, 0x00 }; +static const u8 set_pump_fan_curve_cmd_template[] = { + 0x18, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; +static const u8 default_fan_curve[] = { 0x18, 0x1e, 0x28, 0x30, 0x3c, 0x51, 0x64, 0x64, 0x64 }; +static const u8 default_pump_curve[] = { 0x14, 0x28, 0x3c, 0x50, 0x64, 0x64, 0x64, 0x64, 0x64 }; + +/* Firmware command lengths and offsets */ +#define GET_STATUS_CMD_LENGTH 2 +#define GET_FIRMWARE_VER_CMD_LENGTH 2 +#define SET_PROFILE_CMD_LENGTH 4 +#define SET_CURVE_CMD_LENGTH 13 +#define SET_CURVE_CMD_HEADER 4 +#define SET_CPU_TEMP_CMD_LENGTH 6 +#define SET_PROFILE_ID_OFFSET 2 +#define SET_PROFILE_PWM_OFFSET 3 +#define SET_CPU_TEMP_CMD_OFFSET 2 +#define FIRMWARE_VERSION_OFFSET 26 +#define SHORT_ACK 2 +#define REG_ACK 3 +#define LONG_ACK 4 + +/* Convenience labels specific to this driver */ +#define PUMP_CHANNEL 0 +#define FAN_CHANNEL 1 +#define CURVE_PROFILE_ID 4 + +static const char *const hanbo_temp_label[] = { + "Coolant temp", + "Fan curve CPU temp" +}; + +static const char *const hanbo_speed_label[] = { + "Pump speed", + "Fan speed" +}; + +/* Convenience structure for storing PWM info */ +struct hanbo_pwm_channel { + u16 tacho; + u8 commanded_pwm; + u8 attained_pwm; + u8 active_profile; + u8 pwm_points[CUSTOM_CURVE_POINTS]; +}; + +/* Global data structure for HID and hwmon functions */ +struct hanbo_data { + struct hid_device *hdev; + struct device *hwmon_dev; + struct dentry *debugfs; + /* For locking access to buffer */ + struct mutex buffer_lock; + /* For queueing multiple readers */ + struct mutex status_report_request_mutex; + /* For reinitializing the completion below */ + spinlock_t status_report_request_lock; + struct completion status_report_received; + struct completion fw_version_processed; + /* Sensor data */ + u32 temp_input[2]; + struct hanbo_pwm_channel channel_info[2]; + /* Staging buffer for sending HID packets */ + u8 *buffer; + u8 firmware_version[8]; + unsigned long updated; /* jiffies */ +}; + +/* Validates the internal layout of a report, not the contents */ +static int hanbo_hid_validate_header(int header_size, const u8 *header, + const u8 *data, int eop_offset) +{ + int i; + + for (i = 0; i < header_size; i++) { + if (header[i] != data[i]) + return 0; + } + for (i = eop_offset; i < MAX_REPORT_LENGTH; i++) { + if (data[i] != 0) + return 0; + } + return 1; +} + +/* Write a command to the device with zero padding the report size */ +static int hanbo_hid_write_expanded(struct hanbo_data *priv, const u8 *cmd, + int cmd_length) +{ + int ret; + + mutex_lock(&priv->buffer_lock); + memcpy_and_pad(priv->buffer, MAX_REPORT_LENGTH, cmd, cmd_length, 0x00); + ret = hid_hw_output_report(priv->hdev, priv->buffer, MAX_REPORT_LENGTH); + mutex_unlock(&priv->buffer_lock); + return ret; +} + +/* Convenience function to declutter hanbo_hwmon_read() */ +static int hanbo_hid_get_status(struct hanbo_data *priv) +{ + int ret = mutex_lock_interruptible(&priv->status_report_request_mutex); + + if (ret < 0) + return ret; + + /* Data is up to date */ + if (!time_after(jiffies, priv->updated + msecs_to_jiffies(STATUS_VALIDITY_MS))) + goto unlock_and_return; + + /* + * Disable raw event parsing for a moment to safely reinitialize the + * completion. Reinit is done because hidraw could have triggered + * the raw event parsing and marked the priv->status_report_received + * completion as done. This is done per transaction. + */ + spin_lock_bh(&priv->status_report_request_lock); + reinit_completion(&priv->status_report_received); + spin_unlock_bh(&priv->status_report_request_lock); + + /* Send status requests - Fans */ + ret = hanbo_hid_write_expanded(priv, get_fan_status_cmd, GET_STATUS_CMD_LENGTH); + if (ret < 0) + goto unlock_and_return; + + ret = wait_for_completion_interruptible_timeout(&priv->status_report_received, + msecs_to_jiffies(STATUS_VALIDITY_MS)); + if (ret == 0) + ret = -ETIMEDOUT; + + /* Then pump */ + spin_lock_bh(&priv->status_report_request_lock); + reinit_completion(&priv->status_report_received); + spin_unlock_bh(&priv->status_report_request_lock); + + ret = hanbo_hid_write_expanded(priv, get_pump_status_cmd, GET_STATUS_CMD_LENGTH); + if (ret < 0) + goto unlock_and_return; + + ret = wait_for_completion_interruptible_timeout(&priv->status_report_received, + msecs_to_jiffies(STATUS_VALIDITY_MS)); + if (ret == 0) + ret = -ETIMEDOUT; + +unlock_and_return: + mutex_unlock(&priv->status_report_request_mutex); + if (ret < 0) { + /* If we've failed to send for whatever reason, cancel the completion */ + spin_lock(&priv->status_report_request_lock); + if (!completion_done(&priv->status_report_received)) + complete_all(&priv->status_report_received); + + spin_unlock(&priv->status_report_request_lock); + return ret; + } + return 0; +} + +/* Convenience function to declutter hanbo_hwmon_write() */ +static int hanbo_hid_profile_send(struct hanbo_data *priv, int channel, + u8 profile, u8 duty) +{ + int ret = 0; + + u8 set_profile_cmd[SET_CURVE_CMD_LENGTH]; + + if (profile == CURVE_PROFILE_ID) { + memcpy(set_profile_cmd, set_pump_fan_curve_cmd_template, SET_CURVE_CMD_LENGTH); + /* Templates come with pump commands, replace with fan commands */ + if (channel == FAN_CHANNEL) { + set_profile_cmd[0] = 0xc8; + set_profile_cmd[2] = 0x00; + } + int i; + /* + * Sanity check curve profile, PWM duty cycles cannot decrease + * the higher up the curve they are. + */ + for (i = SET_CURVE_CMD_LENGTH - 1; i > SET_CURVE_CMD_HEADER - 1; i--) { + set_profile_cmd[i] = priv->channel_info[channel].pwm_points[i - SET_CURVE_CMD_HEADER]; + if (i != SET_CURVE_CMD_LENGTH - 1 && set_profile_cmd[i + 1] < set_profile_cmd[i]) + ret = -EINVAL; + } + if (ret < 0) + return ret; + ret = hanbo_hid_write_expanded(priv, set_profile_cmd, SET_CURVE_CMD_LENGTH); + + /* If not sending a curve, we're setting a fixed profile */ + } else { + memcpy(set_profile_cmd, set_pump_fan_cmd_template, SET_PROFILE_CMD_LENGTH); + if (channel == FAN_CHANNEL) + set_profile_cmd[0] = 0x22; + + set_profile_cmd[SET_PROFILE_ID_OFFSET] = profile; + /* Technically this value does nothing, kept as OEM software sends it */ + set_profile_cmd[SET_PROFILE_PWM_OFFSET] = duty; + ret = hanbo_hid_write_expanded(priv, set_profile_cmd, SET_PROFILE_CMD_LENGTH); + } + if (ret >= 0) + priv->channel_info[channel].active_profile = profile; + + return ret; +} + +/* Set hwmon sysfs nodes, see documentation for rationale */ +static umode_t hanbo_hwmon_is_visible(const void *data, + enum hwmon_sensor_types type, + u32 attr, int channel) +{ + switch (type) { + case hwmon_temp: + switch (attr) { + case hwmon_temp_label: + return 0444; + case hwmon_temp_input: + if (channel == FAN_CHANNEL) + return 0644; + return 0444; + default: + break; + } + break; + case hwmon_fan: + switch (attr) { + case hwmon_fan_label: + case hwmon_fan_input: + return 0444; + default: + break; + } + break; + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_input: + return 0444; + case hwmon_pwm_enable: + return 0644; + default: + break; + } + break; + default: + break; + } + return 0; +} + +static int hanbo_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct hanbo_data *priv = dev_get_drvdata(dev); + int ret = hanbo_hid_get_status(priv); + + if (ret < 0) + return ret; + + switch (type) { + case hwmon_temp: + *val = priv->temp_input[channel]; + break; + case hwmon_fan: + *val = priv->channel_info[channel].tacho; + break; + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_input: + /* + * Scaling hack for lm-sensors to show real-er PWM readings. + * The Hanbo generally reports base 10 values-as-hex, it does + * not utilise the complete 8-bit number space. + */ + *val = ((int)(priv->channel_info[channel].attained_pwm * 2) & 0xFF); + break; + case hwmon_pwm_enable: + *val = priv->channel_info[channel].active_profile; + break; + default: + return -EOPNOTSUPP; + } + break; + default: + return -EOPNOTSUPP; /* unreachable */ + } + return 0; +} + +static int hanbo_hwmon_read_string(struct device *dev, + enum hwmon_sensor_types type, u32 attr, + int channel, const char **str) +{ + switch (type) { + case hwmon_temp: + *str = hanbo_temp_label[channel]; + break; + case hwmon_fan: + *str = hanbo_speed_label[channel]; + break; + default: + return -EOPNOTSUPP; /* unreachable */ + } + return 0; +} + +static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + struct hanbo_data *priv = dev_get_drvdata(dev); + int ret = mutex_lock_interruptible(&priv->status_report_request_mutex); + + if (ret < 0) + goto unlock_and_return; + + long sane_val; + + /* + * As writes generate acknowledgement reports the spinlock pattern + * is used here to satisfy that we see them through. + */ + spin_lock_bh(&priv->status_report_request_lock); + reinit_completion(&priv->status_report_received); + spin_unlock_bh(&priv->status_report_request_lock); + switch (type) { + /* Set CPU reference temperature */ + case hwmon_temp: + switch (attr) { + case hwmon_temp_input: + /* Clamp out of range CPU temperatures */ + if (val < 0) { + sane_val = 0; + } else { + sane_val = DIV_ROUND_CLOSEST(val, 1000); + if (sane_val > TEMPERATURE_MAX) + sane_val = TEMPERATURE_MAX; + } + u8 set_cpu_temp_cmd[SET_CPU_TEMP_CMD_LENGTH]; + + memcpy(set_cpu_temp_cmd, set_vcpu_temp_cmd_template, SET_CPU_TEMP_CMD_LENGTH); + set_cpu_temp_cmd[SET_CPU_TEMP_CMD_OFFSET] = sane_val & 0xFF; + ret = hanbo_hid_write_expanded(priv, set_cpu_temp_cmd, SET_CPU_TEMP_CMD_LENGTH); + priv->temp_input[1] = sane_val * 1000; + if (ret < 0) + goto unlock_and_return; + break; + + default: /* unreachable */ + ret = -EOPNOTSUPP; + goto unlock_and_return; + } + break; + /* + * Set a profile + * The PWM duty as the last argument in hanbo_hid_profile_send appears + * to do nothing, but it is included to match the behaviour of the OEM + * software. + */ + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_enable: + switch (channel) { + case FAN_CHANNEL: + switch (val) { + case 1: + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x14); + if (ret < 0) + goto unlock_and_return; + break; + case 2: + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x32); + if (ret < 0) + goto unlock_and_return; + break; + case 3: + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x50); + if (ret < 0) + goto unlock_and_return; + break; + case 4: + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x00); + if (ret < 0) + goto unlock_and_return; + break; + default: + ret = -EINVAL; + goto unlock_and_return; + } + break; + case PUMP_CHANNEL: + switch (val) { + case 1: + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x14); + if (ret < 0) + goto unlock_and_return; + break; + case 2: + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x32); + if (ret < 0) + goto unlock_and_return; + break; + case 3: + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x50); + if (ret < 0) + goto unlock_and_return; + break; + case 4: + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x00); + if (ret < 0) + goto unlock_and_return; + break; + default: + ret = -EINVAL; + goto unlock_and_return; + } + break; + default: /* unreachable */ + ret = -EINVAL; + goto unlock_and_return; + } + break; + default: /* unreachable */ + ret = -EOPNOTSUPP; + goto unlock_and_return; + } + break; + default: /* unreachable */ + ret = -EOPNOTSUPP; + goto unlock_and_return; + } + +unlock_and_return: + if (ret < 0) { + /* If we've failed to send for whatever reason, cancel the completion */ + spin_lock(&priv->status_report_request_lock); + if (!completion_done(&priv->status_report_received)) + complete_all(&priv->status_report_received); + + spin_unlock(&priv->status_report_request_lock); + } else { + ret = 0; + } + + mutex_unlock(&priv->status_report_request_mutex); + return ret; +} + +/* + * Consumes curve points from sysfs and stores in global struct. + * Custom attribute + */ +static ssize_t hanbo_fan_curve_pwm_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sensor_device_attribute_2 *dev_attr = to_sensor_dev_attr_2(attr); + struct hanbo_data *priv = dev_get_drvdata(dev); + long val; + + if (kstrtol(buf, 10, &val) < 0) + return -EINVAL; + + if (val < DUTY_CYCLE_MIN) + val = DUTY_CYCLE_MIN; + + if (val > DUTY_CYCLE_MAX) + val = DUTY_CYCLE_MAX; + + priv->channel_info[dev_attr->nr].pwm_points[dev_attr->index] = val & 0xFF; + return count; +} + +/* + * Presents internal PWM set points from firmware to sysfs, for interest. + * Custom attribute + */ +static ssize_t hanbo_pwm_setpoint_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sensor_device_attribute *dev_attr = to_sensor_dev_attr(attr); + struct hanbo_data *priv = dev_get_drvdata(dev); + u8 value = priv->channel_info[dev_attr->index].commanded_pwm; + + return sysfs_emit(buf, "%d\n", value); +} + +/* + * Define custom attributes for pump and fan curves. Describes 9 points, + * (10 degrees apart defined in hardware) representing 20C to 100C. + */ +static SENSOR_DEVICE_ATTR_2_WO(temp1_auto_point1_pwm, hanbo_fan_curve_pwm, 0, 0); +static SENSOR_DEVICE_ATTR_2_WO(temp1_auto_point2_pwm, hanbo_fan_curve_pwm, 0, 1); +static SENSOR_DEVICE_ATTR_2_WO(temp1_auto_point3_pwm, hanbo_fan_curve_pwm, 0, 2); +static SENSOR_DEVICE_ATTR_2_WO(temp1_auto_point4_pwm, hanbo_fan_curve_pwm, 0, 3); +static SENSOR_DEVICE_ATTR_2_WO(temp1_auto_point5_pwm, hanbo_fan_curve_pwm, 0, 4); +static SENSOR_DEVICE_ATTR_2_WO(temp1_auto_point6_pwm, hanbo_fan_curve_pwm, 0, 5); +static SENSOR_DEVICE_ATTR_2_WO(temp1_auto_point7_pwm, hanbo_fan_curve_pwm, 0, 6); +static SENSOR_DEVICE_ATTR_2_WO(temp1_auto_point8_pwm, hanbo_fan_curve_pwm, 0, 7); +static SENSOR_DEVICE_ATTR_2_WO(temp1_auto_point9_pwm, hanbo_fan_curve_pwm, 0, 8); + +static SENSOR_DEVICE_ATTR_2_WO(temp2_auto_point1_pwm, hanbo_fan_curve_pwm, 1, 0); +static SENSOR_DEVICE_ATTR_2_WO(temp2_auto_point2_pwm, hanbo_fan_curve_pwm, 1, 1); +static SENSOR_DEVICE_ATTR_2_WO(temp2_auto_point3_pwm, hanbo_fan_curve_pwm, 1, 2); +static SENSOR_DEVICE_ATTR_2_WO(temp2_auto_point4_pwm, hanbo_fan_curve_pwm, 1, 3); +static SENSOR_DEVICE_ATTR_2_WO(temp2_auto_point5_pwm, hanbo_fan_curve_pwm, 1, 4); +static SENSOR_DEVICE_ATTR_2_WO(temp2_auto_point6_pwm, hanbo_fan_curve_pwm, 1, 5); +static SENSOR_DEVICE_ATTR_2_WO(temp2_auto_point7_pwm, hanbo_fan_curve_pwm, 1, 6); +static SENSOR_DEVICE_ATTR_2_WO(temp2_auto_point8_pwm, hanbo_fan_curve_pwm, 1, 7); +static SENSOR_DEVICE_ATTR_2_WO(temp2_auto_point9_pwm, hanbo_fan_curve_pwm, 1, 8); + +/* Define custom attributes to reveal internal PWM set points */ +static SENSOR_DEVICE_ATTR_RO(pwm1_setpoint, hanbo_pwm_setpoint, 0); +static SENSOR_DEVICE_ATTR_RO(pwm2_setpoint, hanbo_pwm_setpoint, 1); + +static struct attribute *hanbo_curve_attrs[] = { + /* Pump control curve */ + &sensor_dev_attr_temp1_auto_point1_pwm.dev_attr.attr, + &sensor_dev_attr_temp1_auto_point2_pwm.dev_attr.attr, + &sensor_dev_attr_temp1_auto_point3_pwm.dev_attr.attr, + &sensor_dev_attr_temp1_auto_point4_pwm.dev_attr.attr, + &sensor_dev_attr_temp1_auto_point5_pwm.dev_attr.attr, + &sensor_dev_attr_temp1_auto_point6_pwm.dev_attr.attr, + &sensor_dev_attr_temp1_auto_point7_pwm.dev_attr.attr, + &sensor_dev_attr_temp1_auto_point8_pwm.dev_attr.attr, + &sensor_dev_attr_temp1_auto_point9_pwm.dev_attr.attr, + /* Fan control curve */ + &sensor_dev_attr_temp2_auto_point1_pwm.dev_attr.attr, + &sensor_dev_attr_temp2_auto_point2_pwm.dev_attr.attr, + &sensor_dev_attr_temp2_auto_point3_pwm.dev_attr.attr, + &sensor_dev_attr_temp2_auto_point4_pwm.dev_attr.attr, + &sensor_dev_attr_temp2_auto_point5_pwm.dev_attr.attr, + &sensor_dev_attr_temp2_auto_point6_pwm.dev_attr.attr, + &sensor_dev_attr_temp2_auto_point7_pwm.dev_attr.attr, + &sensor_dev_attr_temp2_auto_point8_pwm.dev_attr.attr, + &sensor_dev_attr_temp2_auto_point9_pwm.dev_attr.attr, + /* Remaining information */ + &sensor_dev_attr_pwm1_setpoint.dev_attr.attr, + &sensor_dev_attr_pwm2_setpoint.dev_attr.attr, + NULL +}; + +static umode_t hanbo_curve_props_are_visible(struct kobject *kobj, + struct attribute *attr, + int index) +{ + return attr->mode; +} + +static const struct attribute_group hanbo_curves_group = { + .attrs = hanbo_curve_attrs, + .is_visible = hanbo_curve_props_are_visible +}; + +static const struct attribute_group *hanbo_groups[] = { + &hanbo_curves_group, + NULL +}; + +static const struct hwmon_ops hanbo_hwmon_ops = { + .is_visible = hanbo_hwmon_is_visible, + .read = hanbo_hwmon_read, + .read_string = hanbo_hwmon_read_string, + .write = hanbo_hwmon_write +}; + +static const struct hwmon_channel_info *hanbo_info[] = { + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL), + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL), + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_INPUT | HWMON_PWM_ENABLE, + HWMON_PWM_INPUT | HWMON_PWM_ENABLE), + NULL +}; + +static const struct hwmon_chip_info hanbo_chip_info = { + .ops = &hanbo_hwmon_ops, + .info = hanbo_info, +}; + +static int firmware_version_show(struct seq_file *seqf, void *unused) +{ + struct hanbo_data *priv = seqf->private; + int i; + + for (i = 0; i < 8; i++) + seq_printf(seqf, "%02X", priv->firmware_version[i]); + + return 0; +} + +DEFINE_SHOW_ATTRIBUTE(firmware_version); + +static void hanbo_debugfs_init(struct hanbo_data *priv) +{ + char name[64]; + + if (priv->firmware_version[0] != 0x80) + return; /* When here, nothing to show in debugfs */ + + scnprintf(name, sizeof(name), "%s-%s", DRIVER_NAME, + dev_name(&priv->hdev->dev)); + + priv->debugfs = debugfs_create_dir(name, NULL); + debugfs_create_file("firmware_version", 0444, priv->debugfs, priv, + &firmware_version_fops); +} + +/* + * Parses USB reports and splits the payload into the relevant data structures + * at the global level for fetching. + */ +static int hanbo_raw_event(struct hid_device *hdev, struct hid_report *report, + u8 *data, int size) +{ + struct hanbo_data *priv = hid_get_drvdata(hdev); + unsigned char code; + + if (size != MAX_REPORT_LENGTH) + return -1; + + code = data[0]; + + switch (code) { + /* Status reports with payload */ + case FIRMWARE_STATUS_REPORT_ID: + if (hanbo_hid_validate_header(SHORT_ACK, (u8 []){code, 0x02}, data, 34)) { + int i; + + for (i = 0; i < 8; i++) + priv->firmware_version[i] = data[FIRMWARE_VERSION_OFFSET + i]; + + if (!completion_done(&priv->fw_version_processed)) + complete_all(&priv->fw_version_processed); + } + break; + case PUMP_STATUS_REPORT_ID: + if (hanbo_hid_validate_header(REG_ACK, (u8 []){code, 0x02, 0x01}, data, 11)) { + priv->temp_input[0] = (data[5] * 1000) + (data[6] * 100); + priv->channel_info[PUMP_CHANNEL].tacho = get_unaligned_be16(data + 7); + priv->channel_info[PUMP_CHANNEL].attained_pwm = data[10]; + priv->channel_info[PUMP_CHANNEL].commanded_pwm = data[9]; + if (priv->channel_info[PUMP_CHANNEL].active_profile != CURVE_PROFILE_ID) + priv->channel_info[PUMP_CHANNEL].active_profile = data[3]; + } + break; + case FAN_STATUS_REPORT_ID: + if (hanbo_hid_validate_header(LONG_ACK, (u8 []){code, 0x02, 0x02, 0x01}, data, 10)) { + priv->channel_info[FAN_CHANNEL].tacho = get_unaligned_be16(data + 6); + priv->channel_info[FAN_CHANNEL].attained_pwm = data[9]; + priv->channel_info[FAN_CHANNEL].commanded_pwm = data[8]; + if (priv->channel_info[FAN_CHANNEL].active_profile != CURVE_PROFILE_ID) + priv->channel_info[FAN_CHANNEL].active_profile = data[4]; + } + break; + /* Acknowledgement reports for commands */ + case PUMP_ACK_REPORT_ID: + case PUMP_CURVE_ACK_REPORT_ID: + case FAN_PROFILE_ACK_REPORT_ID: + case CPU_TEMP_ACK_REPORT_ID: + case FAN_CURVE_ACK_REPORT_ID: + case RGB_MODE_SET_ACK_REPORT_ID: + if (!hanbo_hid_validate_header(REG_ACK, (u8 []){code, 0x02, 0x01}, data, 3)) + hid_warn(hdev, "Received corrupted thermal ACK report"); + break; + /* Here for completeness, unlikely these are triggered from driver */ + case BRIGHTNESS_ACK_REPORT_ID: + case BRIGHTNESS_STATUS_REPORT_ID: + case RGB_MODE_STATUS_REPORT_ID: + if (!hanbo_hid_validate_header(SHORT_ACK, (u8 []){code, 0x02}, data, 4)) + hid_warn(hdev, "Received corrupted RGB ACK report"); + break; + default: + return -1; + } + spin_lock(&priv->status_report_request_lock); + if (!completion_done(&priv->status_report_received)) + complete_all(&priv->status_report_received); + + spin_unlock(&priv->status_report_request_lock); + priv->updated = jiffies; + return 0; +} + +/* This likely serves no purpose other than possibly online reset */ +static int __maybe_unused hanbo_reset_resume(struct hid_device *hdev) +{ + return 0; +} + +static const struct hid_device_id hanbo_table[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_RAZER, USB_PRODUCT_ID_HANBO) }, + { } +}; + +MODULE_DEVICE_TABLE(hid, hanbo_table); + +/* One-shot functions to perform during driver startup */ +static int hanbo_drv_init(struct hid_device *hdev) +{ + struct hanbo_data *priv = hid_get_drvdata(hdev); + int ret; + + ret = hanbo_hid_write_expanded(priv, get_firmware_ver_cmd, + GET_FIRMWARE_VER_CMD_LENGTH); + if (ret < 0) + return ret; + + ret = wait_for_completion_interruptible_timeout(&priv->fw_version_processed, + msecs_to_jiffies(STATUS_VALIDITY_MS)); + if (ret == 0) + return -ETIMEDOUT; + else if (ret < 0) + return ret; + /* + * Set CPU reference to 30 degrees C and pre-load default curves. + * Curves are not sent to the AIO yet as doing so changes the profile. + * This allows activating profile 4 without setting each sysfs pwm node. + */ + enum hwmon_sensor_types mytype = hwmon_temp; + enum hwmon_temp_attributes myattr = hwmon_temp_input; + + ret = hanbo_hwmon_write(&hdev->dev, mytype, myattr, FAN_CHANNEL, 30000); + memcpy(priv->channel_info[FAN_CHANNEL].pwm_points, default_fan_curve, CUSTOM_CURVE_POINTS); + memcpy(priv->channel_info[PUMP_CHANNEL].pwm_points, default_pump_curve, CUSTOM_CURVE_POINTS); + return ret; +} + +static int hanbo_probe(struct hid_device *hdev, const struct hid_device_id *id) +{ + struct hanbo_data *priv; + int ret; + + priv = devm_kzalloc(&hdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->hdev = hdev; + hid_set_drvdata(hdev, priv); + + /* + * Initialize priv->updated to STATUS_VALIDITY_MS in the past, making + * the initial empty data invalid for hanbo_hwmon_read() without the + * need for a special case there. + */ + priv->updated = jiffies - msecs_to_jiffies(STATUS_VALIDITY_MS); + + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, "hid parse failed with %d\n", ret); + return ret; + } + + /* + * Enable hidraw so existing user-space tools can continue to work. + */ + ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); + if (ret) { + hid_err(hdev, "hid hw start failed with %d\n", ret); + return ret; + } + + ret = hid_hw_open(hdev); + if (ret) { + hid_err(hdev, "hid hw open failed with %d\n", ret); + goto fail_and_stop; + } + + priv->buffer = devm_kzalloc(&hdev->dev, MAX_REPORT_LENGTH, GFP_KERNEL); + if (!priv->buffer) { + ret = -ENOMEM; + goto fail_and_close; + } + + mutex_init(&priv->status_report_request_mutex); + mutex_init(&priv->buffer_lock); + spin_lock_init(&priv->status_report_request_lock); + init_completion(&priv->status_report_received); + init_completion(&priv->fw_version_processed); + hid_device_io_start(hdev); + /* + * The Razer Hanbo does not have a mandatory startup sequence. However + * there are things that can be done during bring up to make state + * tracking easier throughout the driver lifecycle. + * These are done in this init function. + */ + ret = hanbo_drv_init(hdev); + if (ret < 0) + hid_warn(hdev, "Driver init failed with %d\n", ret); + + priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, DRIVER_NAME, + priv, &hanbo_chip_info, hanbo_groups); + if (IS_ERR(priv->hwmon_dev)) { + ret = PTR_ERR(priv->hwmon_dev); + hid_err(hdev, "hwmon registration failed with %d\n", ret); + goto fail_and_close; + } + hanbo_debugfs_init(priv); + return 0; + +fail_and_close: + hid_hw_close(hdev); +fail_and_stop: + hid_hw_stop(hdev); + return ret; +} + +static void hanbo_remove(struct hid_device *hdev) +{ + struct hanbo_data *priv = hid_get_drvdata(hdev); + + debugfs_remove_recursive(priv->debugfs); + hwmon_device_unregister(priv->hwmon_dev); + hid_hw_close(hdev); + hid_hw_stop(hdev); +} + +static struct hid_driver hanbo_driver = { + .name = DRIVER_NAME, + .id_table = hanbo_table, + .probe = hanbo_probe, + .remove = hanbo_remove, + .raw_event = hanbo_raw_event, +#ifdef CONFIG_PM + .reset_resume = hanbo_reset_resume, +#endif +}; + +static int __init hanbo_init(void) +{ + return hid_register_driver(&hanbo_driver); +} + +static void __exit hanbo_exit(void) +{ + hid_unregister_driver(&hanbo_driver); +} + +/* When compiled into the kernel, initialize after the HID bus */ +late_initcall(hanbo_init); +module_exit(hanbo_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Joseph East "); +MODULE_DESCRIPTION("Hwmon driver for the Razer Hanbo cooler"); From 2ffd8f86a9b43345ebc1a2b2f5392891aae1a1cd Mon Sep 17 00:00:00 2001 From: Joseph East Date: Mon, 31 Mar 2025 20:54:48 +1030 Subject: [PATCH 2/9] Update documentation, apply formatting fixes from checkpatch.pl --- Documentation/hwmon/razer_hanbo.rst | 218 +++++++++++++++------------- drivers/hwmon/razer_hanbo.c | 68 +++++---- 2 files changed, 161 insertions(+), 125 deletions(-) diff --git a/Documentation/hwmon/razer_hanbo.rst b/Documentation/hwmon/razer_hanbo.rst index ee96b7f..8147b3b 100644 --- a/Documentation/hwmon/razer_hanbo.rst +++ b/Documentation/hwmon/razer_hanbo.rst @@ -5,135 +5,157 @@ Kernel driver razer_hanbo Supported devices: -* Razer Hanbo 360mm +* Razer Hanbo Chroma 360mm Author: Joseph East Description ----------- -This driver enables hardware monitoring support for the Razer Hanbo all-in-one -CPU liquid coolers. Available sensors are pump and fan speeds in RPM, their -PWM duty cycles as percentages, coolant temperature and other state trackers. -Also available through debugfs is the firmware version. This has been -validated against OEM firmware 1.2.0, it is unknown whether this driver is -compatible with other versions. +This driver enables hardware monitoring support for the Razer Hanbo Chroma +all-in-one CPU liquid coolers. Available sensors are pump and fan speeds in RPM, +their PWM duty cycles as percentages, coolant temperature and other state +trackers. Also available through debugfs is the firmware version. This driver +has been developed against OEM firmware 1.2.0. Like the OEM software the pump and fans are unable to be directly controlled. Instead there are four profile modes which are selectable via sysfs to change -device behaviour explained later. The pump and fans can run on different +device behaviour explained further on. The pump and fans can run on different profiles. It is not possible to control individual fans in terms of thermals, they are treated as the one entity. Attaching fans is optional and allows them to be controlled from the device, -freeing motherboard resources. If they are not connected, the fan-related -sensors will report zeroes, this driver will not report an error. +freeing motherboard resources. If they are not connected the fan-related +sensors will report zeroes, this driver though will not report an error. The addressable RGB LEDs are not supported in this driver and should be controlled through userspace tools instead. -Usage notes +Usage Notes ----------- +The driver exposes two hwmon channels. Channel 1 refers to pump functions +with Channel 2 referring to the fan. + As these are USB HIDs, the driver can be loaded automatically by the kernel and supports hot swapping. -The Razer Hanbo has the following behaviours during startup: +The Razer Hanbo Chroma has the following startup behaviours: + * Device goes to 100% if the USB interface fails i.e. not connected. This is the power-on and fault state. * The previous active profile including curves is restored from hardware when the USB interface is enumerated, driver present or not. This is the - running state. + running state and it cannot be fully queried. * Lighting is a free-running ARGB spectrum cycling sequence regardless. There are no other internal effect modes. Performance Profiles +^^^^^^^^^^^^^^^^^^^^ + The fan and pump can run independent performance profiles which are equivalent -to the OEM software. Referring to the sysfs entries table below: -1 = Quiet, 20% duty -2 = Normal, 50% duty -3 = Performance, 80% duty -4 = Curve mode - -Be aware that all the fan profiles rely on an external CPU temperature to -function. See curve mode notes below. - -The profiles can be changed by providing the profile number to the pwmX_enable -node. e.g. echo 3 > /sys/class/<...>/hwmonZ/pwm1_enable sets the pump to -performance mode. - -Profile 4 Curve Mode: -9 curve points correspond to +20 degrees C through +100 degrees C in 10 degree -steps where 'point 1' represents 20 degrees C. Each point is associated with a -1-byte PWM duty cycle from 20-100% (x14-x64) to drive the cooler whilst within -that temperature range. The AIO interpolates between points automatically. -Each point is written to individually using the tempX_auto_pointY_pwm nodes in -sysfs. When writing to these nodes, the driver will accept values between -20-100 inclusive (x14-x64) and clamp invalid values to the relevant extreme. - -e.g. echo 30 > /sys/class/<...>/hwmonZ/temp2_auto_point2_pwm to set fan curve -point 2 (30 degrees) to 30% PWM. - -Progressive fan curve PWM values must be equal to or higher than the previous -point throughout the curve. This is the responsibility of the user. -An invalid curve is reported upon attempting to switch to profile 4 via a -write error: Invalid argument error. Upon switching to profile 4 for either -the fan or pump, the respective curve is sanity checked and uploaded to the -AIO. If changes are made to the curve via sysfs post-switch you will need to -enable profile 4 again to upload the new curve. - -How do profiles know what the temperature is? - -For the pump, operation is autonomous as the reference temperature is -the internal liquid temperature in the AIO. This matches the value of the -temperature at temp1_input in sysfs, no hand-holding needed. - -For the fans, curves will be traversed based on CPU temperature feedback which -is provided via the temp2_input sysfs node. Temperature updates can occur at -any time. It can take between 3-10 seconds for a CPU temperature update -to be reflected in curve behaviour. As there are no timeouts, CPU temperature -updates do not go stale. The last written value will continue to be used as -the reference until it changes. This includes changing profiles. It is -unknown if this survives power cycles as the driver overrides the value -every time it is loaded. Like other sysfs nodes in this driver, the -temp2_input node has valid vaules between 0-100 with values outside this -range internally clamped. Negative numbers are treated as 0 for this node. - -The hwmon interface dictates that temperatures are to be transacted in -in millidegrees C. The Razer Hanbo resolves CPU temperatures in 1 degree -steps. The driver will accept a millidegree input and round as appropriate -before sending to the AIO. Liquid temperature is natively reported as -decidegrees from the AIO. - -As part of driver initialisation, a one-shot CPU temperature of 30 degrees C -is written along with a basic fan and pump curves. This is to prevent -activation of profile 4 with unknown curve parameters. It is assumed -that userspace tools will be used to manage fan operation. -It is not possible to change the temperature values of the curves, only the -duty cycles associated with them. - -Driver default curves- - -Temp: 20C 30C 40C 50C 60C 70C 80C 90C 100C -Fan: { 0x18, 0x1e, 0x28, 0x30, 0x3c, 0x51, 0x64, 0x64, 0x64 }; -Pump: { 0x14, 0x28, 0x3c, 0x50, 0x64, 0x64, 0x64, 0x64, 0x64 }; - -A feature of profile 4 is that it cannot be queried nor is it broadcast. -If the driver initiated curve mode, it will make profile 4 'sticky' when -reading pwmX_enable until the driver is commanded to change to another -profile. In the event that the driver is reloaded, knowledge of curve mode is -lost and sysfs will reflect the HID status reports which only show profiles -1-3. You cannot rely on the AIO to reliably give you its complete state. This -is only achieved if a profile change occurred during the connection lifecycle -as the driver would be aware of what it told the AIO. The driver state is -retained during sleep and resume but will be lost on shutdown. If one intends -to use profile 4 as their default it should be manually reloaded every time -the driver is started for accurate state tracking. It is not possible to -download curves from the AIO. - -Similarly, the CPU reference temperature at temp2_input when read only -reflects what the driver previously sent to the AIO in this session, not what -is actually in firmware. +to the OEM software. + +===== ===================== +ID Profile +===== ===================== +1 Quiet (20% duty cycle) +2 Normal (50% duty cycle) +3 Performance (80% duty cycle) +4 Custom Curve Mode +===== ===================== + +Switching a profile is achieved by writing an ID to a ``pwmX_enable`` sysfs +node. e.g. to enable performance mode on the pump issue: + +``echo 3 > /sys/class/<...>/hwmonZ/pwm1_enable`` + +Be aware that *all* *fan* profiles rely on external reference temperature to +function. See AIO Reference Temperature below. + +Custom Curve Mode (Profile 4) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Each channel has nine curve points which correspond to +20 degrees C through ++100 degrees C in 10 degree steps. Sysfs node ``tempX_auto_point1_pwm`` +represents 20 degrees C. It is not possible to change the temperature value of +the points, only the duty cycles associated with them. To that end, each point +is associated with a 1-byte PWM duty cycle ranging from 20-100% (x14-x64) which +the AIO will select as the reference temperature traverses the curve. The AIO +interpolates between points automatically. Each point is written to individually +using the ``tempX_auto_pointY_pwm`` nodes in sysfs. e.g. to set fan curve point +2 (30 degrees) to 40% PWM: + +``echo 40 > /sys/class/<...>/hwmonZ/temp2_auto_point2_pwm`` + +When writing to these nodes, the driver will accept values between 20-100 +inclusive (x14-x64) and clamp invalid values to the relevant extreme. Curve PWM +values must be equal to or greater than the previous point as the curve +progresses. Switching to profile 4, fan or pump, sanity checks the associated +curve before uploading to the AIO. An invalid curve is reported upon attempting +to switch to profile 4 via ``write error: Invalid argument``, in which case no +changes are made to the AIO. Should profile 4 be active and a curve point is +altered via sysfs you will need to set profile 4 again on that channel to upload +the new curve. + +AIO Reference Temperature +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The fan curve is traversed using a CPU reference temperature which is provided +at the ``temp2_input`` sysfs node. Temperature updates can be issued from there +at any time. It can take between 3-10 seconds for a CPU temperature update to be +reflected in hardware behaviour but protocol wise this is non-blocking. As there +are no timeouts, CPU temperature updates do not go stale. The last written value +will continue to be used as the reference until it changes. This survives +profile changes. The hwmon interface dictates that temperatures are to be +formatted in millidegrees C. The Razer Hanbo Chroma resolves CPU reference +temperature in 1 degree steps. The driver will accept a millidegree input, then +round or clamp as appropriate before sending to the AIO. The Razer Hanbo Chroma +has a valid temperature range of 0-100 degrees C, any negative numbers are +treated as 0. + +For the pump, curve traversal is autonomous as the reference temperature is +the internal coolant temperature in the AIO. This matches the value of the +temperature at ``temp1_input`` in sysfs. The coolant temperature is natively +reported as decidegrees from the AIO and converted to millidegrees when reading. + +Driver Lifecycle +^^^^^^^^^^^^^^^^ + +The Razer Hanbo Chroma does not provide sufficient reporting to reconstruct its +complete internal state should the driver or other user of it happen to reset. +One side effect of this is that it is impossible to determine if profile 4 +specifically is actually running on the AIO; the driver had to have been active +at the time when the command was sent and be the origin of that command. This is +not the case for other profiles. If the driver initiated curve mode, it will +make profile 4 'sticky' when reading ``pwmX_enable`` until the driver is +commanded to change profile. + +Similarly, the CPU reference temperature at ``temp2_input`` only reflects what +the driver previously sent to the AIO in this session when read, not what the +AIO is actually acting on in hardware. + +It is for this reason that as part of driver initialization, CPU reference +temperature is set to 30 degrees C and the internal data structures are +initialized with basic fan and pump curves. This is to prevent activation of +profile 4 with unknown curve parameters. The driver does not set any profile +upon being loaded. + +The driver state is retained during sleep and resume but will be lost on +shutdown. If one intends to use profile 4 as their default it should be +manually reloaded every time the driver is started for accurate state tracking. +It is assumed that userspace tools will be used for this purpose. It is +not possible to download curves from the AIO. + +**Driver default curves** + ++------+------+------+------+------+------+------+------+------+------+ +| Temp | 20C | 30C | 40C | 50C | 60C | 70C | 80C | 90C | 100C | ++======+======+======+======+======+======+======+======+======+======+ +| Fan | 0x18 | 0x1e | 0x28 | 0x30 | 0x3c | 0x51 | 0x64 | 0x64 | 0x64 | ++------+------+------+------+------+------+------+------+------+------+ +| Pump | 0x14 | 0x28 | 0x3c | 0x50 | 0x64 | 0x64 | 0x64 | 0x64 | 0x64 | ++------+------+------+------+------+------+------+------+------+------+ Sysfs entries ------------- diff --git a/drivers/hwmon/razer_hanbo.c b/drivers/hwmon/razer_hanbo.c index d3ced6d..55784a2 100644 --- a/drivers/hwmon/razer_hanbo.c +++ b/drivers/hwmon/razer_hanbo.c @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0+ /* - * hwmon driver for Razer Hanbo AIO CPU coolers. + * hwmon driver for Razer Hanbo Chroma AIO CPU coolers. * * Copyright 2025 Joseph East */ @@ -16,7 +16,7 @@ #define DRIVER_NAME "razer_hanbo" -/* Device constraints from firmware */ +/* Device parameters */ #define USB_VENDOR_ID_RAZER 0x1532 #define USB_PRODUCT_ID_HANBO 0x0f35 @@ -224,8 +224,10 @@ static int hanbo_hid_profile_send(struct hanbo_data *priv, int channel, * the higher up the curve they are. */ for (i = SET_CURVE_CMD_LENGTH - 1; i > SET_CURVE_CMD_HEADER - 1; i--) { - set_profile_cmd[i] = priv->channel_info[channel].pwm_points[i - SET_CURVE_CMD_HEADER]; - if (i != SET_CURVE_CMD_LENGTH - 1 && set_profile_cmd[i + 1] < set_profile_cmd[i]) + set_profile_cmd[i] = + priv->channel_info[channel].pwm_points[i - SET_CURVE_CMD_HEADER]; + if (i != SET_CURVE_CMD_LENGTH - 1 && + set_profile_cmd[i + 1] < set_profile_cmd[i]) ret = -EINVAL; } if (ret < 0) @@ -322,11 +324,11 @@ static int hanbo_hwmon_read(struct device *dev, enum hwmon_sensor_types type, *val = priv->channel_info[channel].active_profile; break; default: - return -EOPNOTSUPP; + return -EOPNOTSUPP; /* sysfs unreachable */ } break; default: - return -EOPNOTSUPP; /* unreachable */ + return -EOPNOTSUPP; /* sysfs unreachable */ } return 0; } @@ -343,7 +345,7 @@ static int hanbo_hwmon_read_string(struct device *dev, *str = hanbo_speed_label[channel]; break; default: - return -EOPNOTSUPP; /* unreachable */ + return -EOPNOTSUPP; /* sysfs unreachable */ } return 0; } @@ -381,15 +383,17 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, } u8 set_cpu_temp_cmd[SET_CPU_TEMP_CMD_LENGTH]; - memcpy(set_cpu_temp_cmd, set_vcpu_temp_cmd_template, SET_CPU_TEMP_CMD_LENGTH); + memcpy(set_cpu_temp_cmd, set_vcpu_temp_cmd_template, + SET_CPU_TEMP_CMD_LENGTH); set_cpu_temp_cmd[SET_CPU_TEMP_CMD_OFFSET] = sane_val & 0xFF; - ret = hanbo_hid_write_expanded(priv, set_cpu_temp_cmd, SET_CPU_TEMP_CMD_LENGTH); + ret = hanbo_hid_write_expanded(priv, set_cpu_temp_cmd, + SET_CPU_TEMP_CMD_LENGTH); priv->temp_input[1] = sane_val * 1000; if (ret < 0) goto unlock_and_return; break; - default: /* unreachable */ + default: /* sysfs unreachable */ ret = -EOPNOTSUPP; goto unlock_and_return; } @@ -407,22 +411,26 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, case FAN_CHANNEL: switch (val) { case 1: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x14); + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, + 0x14); if (ret < 0) goto unlock_and_return; break; case 2: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x32); + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, + 0x32); if (ret < 0) goto unlock_and_return; break; case 3: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x50); + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, + 0x50); if (ret < 0) goto unlock_and_return; break; case 4: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x00); + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, + 0x00); if (ret < 0) goto unlock_and_return; break; @@ -434,22 +442,26 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, case PUMP_CHANNEL: switch (val) { case 1: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x14); + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, + 0x14); if (ret < 0) goto unlock_and_return; break; case 2: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x32); + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, + 0x32); if (ret < 0) goto unlock_and_return; break; case 3: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x50); + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, + 0x50); if (ret < 0) goto unlock_and_return; break; case 4: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, 0x00); + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, + 0x00); if (ret < 0) goto unlock_and_return; break; @@ -458,17 +470,17 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, goto unlock_and_return; } break; - default: /* unreachable */ + default: /* sysfs unreachable */ ret = -EINVAL; goto unlock_and_return; } break; - default: /* unreachable */ + default: /* sysfs unreachable */ ret = -EOPNOTSUPP; goto unlock_and_return; } break; - default: /* unreachable */ + default: /* sysfs unreachable */ ret = -EOPNOTSUPP; goto unlock_and_return; } @@ -693,7 +705,8 @@ static int hanbo_raw_event(struct hid_device *hdev, struct hid_report *report, } break; case FAN_STATUS_REPORT_ID: - if (hanbo_hid_validate_header(LONG_ACK, (u8 []){code, 0x02, 0x02, 0x01}, data, 10)) { + if (hanbo_hid_validate_header(LONG_ACK, + (u8 []){code, 0x02, 0x02, 0x01}, data, 10)) { priv->channel_info[FAN_CHANNEL].tacho = get_unaligned_be16(data + 6); priv->channel_info[FAN_CHANNEL].attained_pwm = data[9]; priv->channel_info[FAN_CHANNEL].commanded_pwm = data[8]; @@ -770,7 +783,8 @@ static int hanbo_drv_init(struct hid_device *hdev) ret = hanbo_hwmon_write(&hdev->dev, mytype, myattr, FAN_CHANNEL, 30000); memcpy(priv->channel_info[FAN_CHANNEL].pwm_points, default_fan_curve, CUSTOM_CURVE_POINTS); - memcpy(priv->channel_info[PUMP_CHANNEL].pwm_points, default_pump_curve, CUSTOM_CURVE_POINTS); + memcpy(priv->channel_info[PUMP_CHANNEL].pwm_points, default_pump_curve, + CUSTOM_CURVE_POINTS); return ret; } @@ -827,9 +841,9 @@ static int hanbo_probe(struct hid_device *hdev, const struct hid_device_id *id) init_completion(&priv->fw_version_processed); hid_device_io_start(hdev); /* - * The Razer Hanbo does not have a mandatory startup sequence. However - * there are things that can be done during bring up to make state - * tracking easier throughout the driver lifecycle. + * The Razer Hanbo Chroma does not have a mandatory startup sequence. + * However there are things that can be done during bring up to make + * state tracking easier throughout the driver lifecycle. * These are done in this init function. */ ret = hanbo_drv_init(hdev); @@ -890,4 +904,4 @@ module_exit(hanbo_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Joseph East "); -MODULE_DESCRIPTION("Hwmon driver for the Razer Hanbo cooler"); +MODULE_DESCRIPTION("Hwmon driver for the Razer Hanbo Chroma cooler"); From 1159c87a12c14834d56bf2d8ae9eeae505bb53a0 Mon Sep 17 00:00:00 2001 From: Joseph East Date: Tue, 1 Apr 2025 23:11:02 +1030 Subject: [PATCH 3/9] Address the first round of comments: * Refactor hanbo_hid_validate_header to use less arguments. * Refactor hanbo_hid_profile_send to use less arguments. * Include device serial number in DebugFS. * Removed power management placeholder code. * 'ret' used as return from more functions. * Replaced a number of values with appropriate error codes or constants. * mutex_unlocks occur after spin_unlocks. * Removed redundant goto statements in hanbo_hwmon_write. * Minor formatting updates and changes to constants for clarity. Signed-off-by: Joseph East --- Documentation/hwmon/razer_hanbo.rst | 2 +- drivers/hwmon/razer_hanbo.c | 247 +++++++++++++--------------- 2 files changed, 119 insertions(+), 130 deletions(-) diff --git a/Documentation/hwmon/razer_hanbo.rst b/Documentation/hwmon/razer_hanbo.rst index 8147b3b..8c039c1 100644 --- a/Documentation/hwmon/razer_hanbo.rst +++ b/Documentation/hwmon/razer_hanbo.rst @@ -7,7 +7,7 @@ Supported devices: * Razer Hanbo Chroma 360mm -Author: Joseph East +Author: Joseph East Description ----------- diff --git a/drivers/hwmon/razer_hanbo.c b/drivers/hwmon/razer_hanbo.c index 55784a2..a7452f2 100644 --- a/drivers/hwmon/razer_hanbo.c +++ b/drivers/hwmon/razer_hanbo.c @@ -51,21 +51,27 @@ static const u8 set_pump_fan_curve_cmd_template[] = { 0x18, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const u8 default_fan_curve[] = { 0x18, 0x1e, 0x28, 0x30, 0x3c, 0x51, 0x64, 0x64, 0x64 }; static const u8 default_pump_curve[] = { 0x14, 0x28, 0x3c, 0x50, 0x64, 0x64, 0x64, 0x64, 0x64 }; +static const u8 profile_base_duties[] = { 0x00, 0x14, 0x32, 0x50 }; +static const u8 ack_header_type_a[] = { 0x00, 0x02, 0x01, 0x00 }; +static const u8 ack_header_type_b[] = { 0x00, 0x02, 0x02, 0x01 }; /* Firmware command lengths and offsets */ #define GET_STATUS_CMD_LENGTH 2 #define GET_FIRMWARE_VER_CMD_LENGTH 2 #define SET_PROFILE_CMD_LENGTH 4 #define SET_CURVE_CMD_LENGTH 13 -#define SET_CURVE_CMD_HEADER 4 #define SET_CPU_TEMP_CMD_LENGTH 6 +#define FIRMWARE_VERSION_LENGTH 8 +#define SERIAL_NUMBER_LENGTH 15 #define SET_PROFILE_ID_OFFSET 2 #define SET_PROFILE_PWM_OFFSET 3 -#define SET_CPU_TEMP_CMD_OFFSET 2 +#define SET_CPU_TEMP_PAYLOAD_OFFSET 2 #define FIRMWARE_VERSION_OFFSET 26 -#define SHORT_ACK 2 -#define REG_ACK 3 -#define LONG_ACK 4 +#define SERIAL_NUMBER_OFFSET 3 +#define CURVE_PAYLOAD_OFFSET 4 +#define SHORT_ACK_LENGTH 2 +#define REG_ACK_LENGTH 3 +#define LONG_ACK_LENGTH 4 /* Convenience labels specific to this driver */ #define PUMP_CHANNEL 0 @@ -74,7 +80,7 @@ static const u8 default_pump_curve[] = { 0x14, 0x28, 0x3c, 0x50, 0x64, 0x64, 0x6 static const char *const hanbo_temp_label[] = { "Coolant temp", - "Fan curve CPU temp" + "Reference temp" }; static const char *const hanbo_speed_label[] = { @@ -110,24 +116,37 @@ struct hanbo_data { /* Staging buffer for sending HID packets */ u8 *buffer; u8 firmware_version[8]; + char serial_number[15]; unsigned long updated; /* jiffies */ }; /* Validates the internal layout of a report, not the contents */ -static int hanbo_hid_validate_header(int header_size, const u8 *header, - const u8 *data, int eop_offset) +static int hanbo_hid_validate_header(int header_size, const u8 *data, + int eop_offset) { int i; + u8 header[LONG_ACK_LENGTH]; - for (i = 0; i < header_size; i++) { + switch (header_size) { + case SHORT_ACK_LENGTH: + case REG_ACK_LENGTH: + memcpy(header, ack_header_type_a, REG_ACK_LENGTH); + break; + case LONG_ACK_LENGTH: + memcpy(header, ack_header_type_b, LONG_ACK_LENGTH); + break; + default: + return -EPROTO; + } + for (i = 1; i < header_size; i++) { if (header[i] != data[i]) - return 0; + return -EPROTO; } for (i = eop_offset; i < MAX_REPORT_LENGTH; i++) { if (data[i] != 0) - return 0; + return -EPROTO; } - return 1; + return 0; } /* Write a command to the device with zero padding the report size */ @@ -150,11 +169,9 @@ static int hanbo_hid_get_status(struct hanbo_data *priv) if (ret < 0) return ret; - /* Data is up to date */ if (!time_after(jiffies, priv->updated + msecs_to_jiffies(STATUS_VALIDITY_MS))) goto unlock_and_return; - /* * Disable raw event parsing for a moment to safely reinitialize the * completion. Reinit is done because hidraw could have triggered @@ -169,12 +186,10 @@ static int hanbo_hid_get_status(struct hanbo_data *priv) ret = hanbo_hid_write_expanded(priv, get_fan_status_cmd, GET_STATUS_CMD_LENGTH); if (ret < 0) goto unlock_and_return; - ret = wait_for_completion_interruptible_timeout(&priv->status_report_received, msecs_to_jiffies(STATUS_VALIDITY_MS)); if (ret == 0) ret = -ETIMEDOUT; - /* Then pump */ spin_lock_bh(&priv->status_report_request_lock); reinit_completion(&priv->status_report_received); @@ -183,32 +198,27 @@ static int hanbo_hid_get_status(struct hanbo_data *priv) ret = hanbo_hid_write_expanded(priv, get_pump_status_cmd, GET_STATUS_CMD_LENGTH); if (ret < 0) goto unlock_and_return; - ret = wait_for_completion_interruptible_timeout(&priv->status_report_received, msecs_to_jiffies(STATUS_VALIDITY_MS)); if (ret == 0) ret = -ETIMEDOUT; - unlock_and_return: - mutex_unlock(&priv->status_report_request_mutex); + /* If we've failed to send for whatever reason, cancel the completion */ if (ret < 0) { - /* If we've failed to send for whatever reason, cancel the completion */ spin_lock(&priv->status_report_request_lock); if (!completion_done(&priv->status_report_received)) complete_all(&priv->status_report_received); - spin_unlock(&priv->status_report_request_lock); - return ret; } - return 0; + mutex_unlock(&priv->status_report_request_mutex); + return ret; } /* Convenience function to declutter hanbo_hwmon_write() */ static int hanbo_hid_profile_send(struct hanbo_data *priv, int channel, - u8 profile, u8 duty) + u8 profile) { int ret = 0; - u8 set_profile_cmd[SET_CURVE_CMD_LENGTH]; if (profile == CURVE_PROFILE_ID) { @@ -223,9 +233,9 @@ static int hanbo_hid_profile_send(struct hanbo_data *priv, int channel, * Sanity check curve profile, PWM duty cycles cannot decrease * the higher up the curve they are. */ - for (i = SET_CURVE_CMD_LENGTH - 1; i > SET_CURVE_CMD_HEADER - 1; i--) { + for (i = SET_CURVE_CMD_LENGTH - 1; i > CURVE_PAYLOAD_OFFSET - 1; i--) { set_profile_cmd[i] = - priv->channel_info[channel].pwm_points[i - SET_CURVE_CMD_HEADER]; + priv->channel_info[channel].pwm_points[i - CURVE_PAYLOAD_OFFSET]; if (i != SET_CURVE_CMD_LENGTH - 1 && set_profile_cmd[i + 1] < set_profile_cmd[i]) ret = -EINVAL; @@ -233,21 +243,19 @@ static int hanbo_hid_profile_send(struct hanbo_data *priv, int channel, if (ret < 0) return ret; ret = hanbo_hid_write_expanded(priv, set_profile_cmd, SET_CURVE_CMD_LENGTH); - /* If not sending a curve, we're setting a fixed profile */ - } else { + } else if (profile > 0 && profile < CURVE_PROFILE_ID) { memcpy(set_profile_cmd, set_pump_fan_cmd_template, SET_PROFILE_CMD_LENGTH); + /* Templates come with pump commands, replace with fan commands */ if (channel == FAN_CHANNEL) set_profile_cmd[0] = 0x22; - set_profile_cmd[SET_PROFILE_ID_OFFSET] = profile; /* Technically this value does nothing, kept as OEM software sends it */ - set_profile_cmd[SET_PROFILE_PWM_OFFSET] = duty; + set_profile_cmd[SET_PROFILE_PWM_OFFSET] = profile_base_duties[profile]; ret = hanbo_hid_write_expanded(priv, set_profile_cmd, SET_PROFILE_CMD_LENGTH); } if (ret >= 0) priv->channel_info[channel].active_profile = profile; - return ret; } @@ -302,7 +310,6 @@ static int hanbo_hwmon_read(struct device *dev, enum hwmon_sensor_types type, if (ret < 0) return ret; - switch (type) { case hwmon_temp: *val = priv->temp_input[channel]; @@ -330,7 +337,7 @@ static int hanbo_hwmon_read(struct device *dev, enum hwmon_sensor_types type, default: return -EOPNOTSUPP; /* sysfs unreachable */ } - return 0; + return ret; } static int hanbo_hwmon_read_string(struct device *dev, @@ -354,13 +361,11 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long val) { struct hanbo_data *priv = dev_get_drvdata(dev); + long degrees_c; int ret = mutex_lock_interruptible(&priv->status_report_request_mutex); if (ret < 0) - goto unlock_and_return; - - long sane_val; - + return ret; /* * As writes generate acknowledgement reports the spinlock pattern * is used here to satisfy that we see them through. @@ -368,6 +373,7 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, spin_lock_bh(&priv->status_report_request_lock); reinit_completion(&priv->status_report_received); spin_unlock_bh(&priv->status_report_request_lock); + switch (type) { /* Set CPU reference temperature */ case hwmon_temp: @@ -375,20 +381,21 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, case hwmon_temp_input: /* Clamp out of range CPU temperatures */ if (val < 0) { - sane_val = 0; + degrees_c = 0; } else { - sane_val = DIV_ROUND_CLOSEST(val, 1000); - if (sane_val > TEMPERATURE_MAX) - sane_val = TEMPERATURE_MAX; + degrees_c = DIV_ROUND_CLOSEST(val, 1000); + if (degrees_c > TEMPERATURE_MAX) + degrees_c = TEMPERATURE_MAX; } u8 set_cpu_temp_cmd[SET_CPU_TEMP_CMD_LENGTH]; memcpy(set_cpu_temp_cmd, set_vcpu_temp_cmd_template, SET_CPU_TEMP_CMD_LENGTH); - set_cpu_temp_cmd[SET_CPU_TEMP_CMD_OFFSET] = sane_val & 0xFF; + set_cpu_temp_cmd[SET_CPU_TEMP_PAYLOAD_OFFSET] = degrees_c & 0xFF; ret = hanbo_hid_write_expanded(priv, set_cpu_temp_cmd, SET_CPU_TEMP_CMD_LENGTH); - priv->temp_input[1] = sane_val * 1000; + /* Store the final value for reading via sysfs */ + priv->temp_input[1] = degrees_c * 1000; if (ret < 0) goto unlock_and_return; break; @@ -411,92 +418,59 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, case FAN_CHANNEL: switch (val) { case 1: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, - 0x14); - if (ret < 0) - goto unlock_and_return; + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); break; case 2: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, - 0x32); - if (ret < 0) - goto unlock_and_return; + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); break; case 3: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, - 0x50); - if (ret < 0) - goto unlock_and_return; + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); break; case 4: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, - 0x00); - if (ret < 0) - goto unlock_and_return; + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); break; default: ret = -EINVAL; - goto unlock_and_return; } break; case PUMP_CHANNEL: switch (val) { case 1: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, - 0x14); - if (ret < 0) - goto unlock_and_return; + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); break; case 2: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, - 0x32); - if (ret < 0) - goto unlock_and_return; + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); break; case 3: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, - 0x50); - if (ret < 0) - goto unlock_and_return; + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); break; case 4: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF, - 0x00); - if (ret < 0) - goto unlock_and_return; + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); break; default: ret = -EINVAL; - goto unlock_and_return; } break; default: /* sysfs unreachable */ ret = -EINVAL; - goto unlock_and_return; } break; default: /* sysfs unreachable */ ret = -EOPNOTSUPP; - goto unlock_and_return; } break; default: /* sysfs unreachable */ ret = -EOPNOTSUPP; - goto unlock_and_return; } unlock_and_return: + /* If we've failed to send for whatever reason, cancel the completion */ if (ret < 0) { - /* If we've failed to send for whatever reason, cancel the completion */ spin_lock(&priv->status_report_request_lock); if (!completion_done(&priv->status_report_received)) complete_all(&priv->status_report_received); - spin_unlock(&priv->status_report_request_lock); - } else { - ret = 0; } - mutex_unlock(&priv->status_report_request_mutex); return ret; } @@ -643,13 +617,25 @@ static int firmware_version_show(struct seq_file *seqf, void *unused) struct hanbo_data *priv = seqf->private; int i; - for (i = 0; i < 8; i++) + for (i = 0; i < FIRMWARE_VERSION_LENGTH; i++) seq_printf(seqf, "%02X", priv->firmware_version[i]); + seq_puts(seqf, "\n"); + return 0; +} +static int serial_number_show(struct seq_file *seqf, void *unused) +{ + struct hanbo_data *priv = seqf->private; + int i; + + for (i = 0; i < SERIAL_NUMBER_LENGTH; i++) + seq_printf(seqf, "%c", priv->serial_number[i]); + seq_puts(seqf, "\n"); return 0; } DEFINE_SHOW_ATTRIBUTE(firmware_version); +DEFINE_SHOW_ATTRIBUTE(serial_number); static void hanbo_debugfs_init(struct hanbo_data *priv) { @@ -664,6 +650,8 @@ static void hanbo_debugfs_init(struct hanbo_data *priv) priv->debugfs = debugfs_create_dir(name, NULL); debugfs_create_file("firmware_version", 0444, priv->debugfs, priv, &firmware_version_fops); + debugfs_create_file("serial_number", 0444, priv->debugfs, priv, + &serial_number_fops); } /* @@ -674,45 +662,49 @@ static int hanbo_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) { struct hanbo_data *priv = hid_get_drvdata(hdev); - unsigned char code; + unsigned char rid; + int ret; if (size != MAX_REPORT_LENGTH) - return -1; + return -EPROTO; - code = data[0]; + rid = data[0]; - switch (code) { + switch (rid) { /* Status reports with payload */ case FIRMWARE_STATUS_REPORT_ID: - if (hanbo_hid_validate_header(SHORT_ACK, (u8 []){code, 0x02}, data, 34)) { - int i; - - for (i = 0; i < 8; i++) - priv->firmware_version[i] = data[FIRMWARE_VERSION_OFFSET + i]; + ret = hanbo_hid_validate_header(SHORT_ACK_LENGTH, data, 34); + if (ret < 0) + goto fail_and_return; + int i; - if (!completion_done(&priv->fw_version_processed)) - complete_all(&priv->fw_version_processed); - } + for (i = 0; i < FIRMWARE_VERSION_LENGTH; i++) + priv->firmware_version[i] = data[FIRMWARE_VERSION_OFFSET + i]; + for (i = 0; i < SERIAL_NUMBER_LENGTH; i++) + priv->serial_number[i] = data[SERIAL_NUMBER_OFFSET + i]; + if (!completion_done(&priv->fw_version_processed)) + complete_all(&priv->fw_version_processed); break; case PUMP_STATUS_REPORT_ID: - if (hanbo_hid_validate_header(REG_ACK, (u8 []){code, 0x02, 0x01}, data, 11)) { - priv->temp_input[0] = (data[5] * 1000) + (data[6] * 100); - priv->channel_info[PUMP_CHANNEL].tacho = get_unaligned_be16(data + 7); - priv->channel_info[PUMP_CHANNEL].attained_pwm = data[10]; - priv->channel_info[PUMP_CHANNEL].commanded_pwm = data[9]; - if (priv->channel_info[PUMP_CHANNEL].active_profile != CURVE_PROFILE_ID) - priv->channel_info[PUMP_CHANNEL].active_profile = data[3]; - } + ret = hanbo_hid_validate_header(REG_ACK_LENGTH, data, 11); + if (ret < 0) + goto fail_and_return; + priv->temp_input[0] = (data[5] * 1000) + (data[6] * 100); + priv->channel_info[PUMP_CHANNEL].tacho = get_unaligned_be16(data + 7); + priv->channel_info[PUMP_CHANNEL].attained_pwm = data[10]; + priv->channel_info[PUMP_CHANNEL].commanded_pwm = data[9]; + if (priv->channel_info[PUMP_CHANNEL].active_profile != CURVE_PROFILE_ID) + priv->channel_info[PUMP_CHANNEL].active_profile = data[3]; break; case FAN_STATUS_REPORT_ID: - if (hanbo_hid_validate_header(LONG_ACK, - (u8 []){code, 0x02, 0x02, 0x01}, data, 10)) { - priv->channel_info[FAN_CHANNEL].tacho = get_unaligned_be16(data + 6); - priv->channel_info[FAN_CHANNEL].attained_pwm = data[9]; - priv->channel_info[FAN_CHANNEL].commanded_pwm = data[8]; - if (priv->channel_info[FAN_CHANNEL].active_profile != CURVE_PROFILE_ID) - priv->channel_info[FAN_CHANNEL].active_profile = data[4]; - } + ret = hanbo_hid_validate_header(LONG_ACK_LENGTH, data, 10); + if (ret < 0) + goto fail_and_return; + priv->channel_info[FAN_CHANNEL].tacho = get_unaligned_be16(data + 6); + priv->channel_info[FAN_CHANNEL].attained_pwm = data[9]; + priv->channel_info[FAN_CHANNEL].commanded_pwm = data[8]; + if (priv->channel_info[FAN_CHANNEL].active_profile != CURVE_PROFILE_ID) + priv->channel_info[FAN_CHANNEL].active_profile = data[4]; break; /* Acknowledgement reports for commands */ case PUMP_ACK_REPORT_ID: @@ -721,32 +713,32 @@ static int hanbo_raw_event(struct hid_device *hdev, struct hid_report *report, case CPU_TEMP_ACK_REPORT_ID: case FAN_CURVE_ACK_REPORT_ID: case RGB_MODE_SET_ACK_REPORT_ID: - if (!hanbo_hid_validate_header(REG_ACK, (u8 []){code, 0x02, 0x01}, data, 3)) + ret = hanbo_hid_validate_header(REG_ACK_LENGTH, data, 3); + if (ret < 0) { hid_warn(hdev, "Received corrupted thermal ACK report"); + goto fail_and_return; + } break; /* Here for completeness, unlikely these are triggered from driver */ case BRIGHTNESS_ACK_REPORT_ID: case BRIGHTNESS_STATUS_REPORT_ID: case RGB_MODE_STATUS_REPORT_ID: - if (!hanbo_hid_validate_header(SHORT_ACK, (u8 []){code, 0x02}, data, 4)) + ret = hanbo_hid_validate_header(SHORT_ACK_LENGTH, data, 4); + if (ret < 0) { hid_warn(hdev, "Received corrupted RGB ACK report"); + goto fail_and_return; + } break; default: - return -1; + return -EPROTO; } spin_lock(&priv->status_report_request_lock); if (!completion_done(&priv->status_report_received)) complete_all(&priv->status_report_received); - spin_unlock(&priv->status_report_request_lock); priv->updated = jiffies; - return 0; -} - -/* This likely serves no purpose other than possibly online reset */ -static int __maybe_unused hanbo_reset_resume(struct hid_device *hdev) -{ - return 0; +fail_and_return: + return ret; } static const struct hid_device_id hanbo_table[] = { @@ -883,9 +875,6 @@ static struct hid_driver hanbo_driver = { .probe = hanbo_probe, .remove = hanbo_remove, .raw_event = hanbo_raw_event, -#ifdef CONFIG_PM - .reset_resume = hanbo_reset_resume, -#endif }; static int __init hanbo_init(void) From 8a212011481c353355349f1261185e5ede1b133d Mon Sep 17 00:00:00 2001 From: Joseph East Date: Wed, 2 Apr 2025 20:09:06 +1030 Subject: [PATCH 4/9] Address the second round of comments and other minor changes: * Documentation updated (serial number & sysfs table) * hanbo_hwmon_write simplification by arg checking in hanbo_profile_send * All hwmon_ops return 0 on success as per hwmon.h definition * CPU reference temperature only updated if HID transfer succeeds * Driver will emit hid_fail and bail out if init sequence fails. Signed-off-by: Joseph East --- Documentation/hwmon/razer_hanbo.rst | 27 +++++----- drivers/hwmon/razer_hanbo.c | 83 ++++++++--------------------- 2 files changed, 38 insertions(+), 72 deletions(-) diff --git a/Documentation/hwmon/razer_hanbo.rst b/Documentation/hwmon/razer_hanbo.rst index 8c039c1..2bf945a 100644 --- a/Documentation/hwmon/razer_hanbo.rst +++ b/Documentation/hwmon/razer_hanbo.rst @@ -15,8 +15,8 @@ Description This driver enables hardware monitoring support for the Razer Hanbo Chroma all-in-one CPU liquid coolers. Available sensors are pump and fan speeds in RPM, their PWM duty cycles as percentages, coolant temperature and other state -trackers. Also available through debugfs is the firmware version. This driver -has been developed against OEM firmware 1.2.0. +trackers. Also available through debugfs is the firmware version and serial +number. This driver has been developed against OEM firmware 1.2.0. Like the OEM software the pump and fans are unable to be directly controlled. Instead there are four profile modes which are selectable via sysfs to change @@ -161,20 +161,20 @@ Sysfs entries ------------- ============= ============================================= -fan1_input R: Pump speed (in rpm) -fan2_input R: Fan speed (in rpm) -temp1_input R: Coolant temperature (in millidegrees Celsius) -temp2_input RW: CPU feedback temperature (in millidegrees Celsius) -pwm1 R: Pump achieved PWM rate as a percentage -pwm2 R: Fan achieved PWM rate as a percentage +fan1_input R: Pump speed (rpm) +fan2_input R: Fan speed (rpm) +temp1_input R: Coolant temperature (millidegrees Celsius) +temp2_input RW: CPU feedback temperature (millidegrees Celsius) +pwm1 R: Pump achieved PWM duty cycle (%) +pwm2 R: Fan achieved PWM duty cycle (%) pwm1_enable R: Get pump active profile W: Set profile from 1-4 pwm2_enable R: Get fan active profile W: Set profile from 1-4 -pwm1_setpoint R: Commanded RPM for the pump -pwm2_setpoint R: Commanded RPM for the fan -temp1_auto... W: Pump curve data points, PWM rate as a percentage. -temp2_auto... W: Fan curve data points, PWM rate as a percentage. +pwm1_setpoint R: Pump commanded PWM duty cycle (%) +pwm2_setpoint R: Fan commanded PWM duty cycle (%) +temp1_auto... W: Pump curve data points, PWM duty cycle (%) +temp2_auto... W: Fan curve data points, PWM duty cycle (%) ============= ============================================= Debugfs entries @@ -183,3 +183,6 @@ Debugfs entries ================ ======================= firmware_version Device firmware version ================ ======================= +============= ==================== +serial_number Device serial number +============= ==================== diff --git a/drivers/hwmon/razer_hanbo.c b/drivers/hwmon/razer_hanbo.c index a7452f2..b2daebc 100644 --- a/drivers/hwmon/razer_hanbo.c +++ b/drivers/hwmon/razer_hanbo.c @@ -76,6 +76,7 @@ static const u8 ack_header_type_b[] = { 0x00, 0x02, 0x02, 0x01 }; /* Convenience labels specific to this driver */ #define PUMP_CHANNEL 0 #define FAN_CHANNEL 1 +#define QUIET_PROFILE_ID 1 #define CURVE_PROFILE_ID 4 static const char *const hanbo_temp_label[] = { @@ -221,6 +222,10 @@ static int hanbo_hid_profile_send(struct hanbo_data *priv, int channel, int ret = 0; u8 set_profile_cmd[SET_CURVE_CMD_LENGTH]; + if (channel < PUMP_CHANNEL || channel > FAN_CHANNEL) + return -EINVAL; /* sysfs unreachable */ + if (profile < QUIET_PROFILE_ID || profile > CURVE_PROFILE_ID) + return -EINVAL; if (profile == CURVE_PROFILE_ID) { memcpy(set_profile_cmd, set_pump_fan_curve_cmd_template, SET_CURVE_CMD_LENGTH); /* Templates come with pump commands, replace with fan commands */ @@ -243,8 +248,7 @@ static int hanbo_hid_profile_send(struct hanbo_data *priv, int channel, if (ret < 0) return ret; ret = hanbo_hid_write_expanded(priv, set_profile_cmd, SET_CURVE_CMD_LENGTH); - /* If not sending a curve, we're setting a fixed profile */ - } else if (profile > 0 && profile < CURVE_PROFILE_ID) { + } else { /* sending a profile */ memcpy(set_profile_cmd, set_pump_fan_cmd_template, SET_PROFILE_CMD_LENGTH); /* Templates come with pump commands, replace with fan commands */ if (channel == FAN_CHANNEL) @@ -337,6 +341,8 @@ static int hanbo_hwmon_read(struct device *dev, enum hwmon_sensor_types type, default: return -EOPNOTSUPP; /* sysfs unreachable */ } + if (ret > 0) + return 0; return ret; } @@ -375,10 +381,11 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, spin_unlock_bh(&priv->status_report_request_lock); switch (type) { - /* Set CPU reference temperature */ - case hwmon_temp: + case hwmon_temp: /* Set CPU reference temperature */ switch (attr) { case hwmon_temp_input: + u8 set_cpu_temp_cmd[SET_CPU_TEMP_CMD_LENGTH]; + /* Clamp out of range CPU temperatures */ if (val < 0) { degrees_c = 0; @@ -387,17 +394,15 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, if (degrees_c > TEMPERATURE_MAX) degrees_c = TEMPERATURE_MAX; } - u8 set_cpu_temp_cmd[SET_CPU_TEMP_CMD_LENGTH]; - memcpy(set_cpu_temp_cmd, set_vcpu_temp_cmd_template, SET_CPU_TEMP_CMD_LENGTH); set_cpu_temp_cmd[SET_CPU_TEMP_PAYLOAD_OFFSET] = degrees_c & 0xFF; ret = hanbo_hid_write_expanded(priv, set_cpu_temp_cmd, SET_CPU_TEMP_CMD_LENGTH); - /* Store the final value for reading via sysfs */ - priv->temp_input[1] = degrees_c * 1000; if (ret < 0) goto unlock_and_return; + /* Store the final value for reading via sysfs */ + priv->temp_input[1] = degrees_c * 1000; break; default: /* sysfs unreachable */ @@ -405,55 +410,10 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, goto unlock_and_return; } break; - /* - * Set a profile - * The PWM duty as the last argument in hanbo_hid_profile_send appears - * to do nothing, but it is included to match the behaviour of the OEM - * software. - */ - case hwmon_pwm: + case hwmon_pwm: /* Set a profile */ switch (attr) { case hwmon_pwm_enable: - switch (channel) { - case FAN_CHANNEL: - switch (val) { - case 1: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); - break; - case 2: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); - break; - case 3: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); - break; - case 4: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); - break; - default: - ret = -EINVAL; - } - break; - case PUMP_CHANNEL: - switch (val) { - case 1: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); - break; - case 2: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); - break; - case 3: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); - break; - case 4: - ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); - break; - default: - ret = -EINVAL; - } - break; - default: /* sysfs unreachable */ - ret = -EINVAL; - } + ret = hanbo_hid_profile_send(priv, channel, val & 0xFF); break; default: /* sysfs unreachable */ ret = -EOPNOTSUPP; @@ -472,6 +432,8 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, spin_unlock(&priv->status_report_request_lock); } mutex_unlock(&priv->status_report_request_mutex); + if (ret > 0) + return 0; return ret; } @@ -834,13 +796,14 @@ static int hanbo_probe(struct hid_device *hdev, const struct hid_device_id *id) hid_device_io_start(hdev); /* * The Razer Hanbo Chroma does not have a mandatory startup sequence. - * However there are things that can be done during bring up to make - * state tracking easier throughout the driver lifecycle. - * These are done in this init function. + * This function ensures a consistent startup for state tracking + * purposes. */ ret = hanbo_drv_init(hdev); - if (ret < 0) - hid_warn(hdev, "Driver init failed with %d\n", ret); + if (ret < 0) { + hid_err(hdev, "Driver init failed with %d\n", ret); + goto fail_and_close; + } priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, DRIVER_NAME, priv, &hanbo_chip_info, hanbo_groups); From 0441eafb73124783c82576e84850b061ebe0b6d9 Mon Sep 17 00:00:00 2001 From: Joseph East Date: Wed, 2 Apr 2025 20:38:46 +1030 Subject: [PATCH 5/9] Fix github rst rendering Signed-off-by: Joseph East --- Documentation/hwmon/razer_hanbo.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Documentation/hwmon/razer_hanbo.rst b/Documentation/hwmon/razer_hanbo.rst index 2bf945a..8b5ec1a 100644 --- a/Documentation/hwmon/razer_hanbo.rst +++ b/Documentation/hwmon/razer_hanbo.rst @@ -182,7 +182,5 @@ Debugfs entries ================ ======================= firmware_version Device firmware version +serial_number Device serial number ================ ======================= -============= ==================== -serial_number Device serial number -============= ==================== From 57dc48025510ad4d132448f81e683b5315569072 Mon Sep 17 00:00:00 2001 From: Joseph East Date: Wed, 2 Apr 2025 20:59:53 +1030 Subject: [PATCH 6/9] Spelling correction per checkpatch.pl Signed-off-by: Joseph East --- drivers/hwmon/razer_hanbo.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/hwmon/razer_hanbo.c b/drivers/hwmon/razer_hanbo.c index b2daebc..5626da8 100644 --- a/drivers/hwmon/razer_hanbo.c +++ b/drivers/hwmon/razer_hanbo.c @@ -373,7 +373,7 @@ static int hanbo_hwmon_write(struct device *dev, enum hwmon_sensor_types type, if (ret < 0) return ret; /* - * As writes generate acknowledgement reports the spinlock pattern + * As writes generate acknowledgment reports the spinlock pattern * is used here to satisfy that we see them through. */ spin_lock_bh(&priv->status_report_request_lock); @@ -668,7 +668,7 @@ static int hanbo_raw_event(struct hid_device *hdev, struct hid_report *report, if (priv->channel_info[FAN_CHANNEL].active_profile != CURVE_PROFILE_ID) priv->channel_info[FAN_CHANNEL].active_profile = data[4]; break; - /* Acknowledgement reports for commands */ + /* Acknowledgment reports for commands */ case PUMP_ACK_REPORT_ID: case PUMP_CURVE_ACK_REPORT_ID: case FAN_PROFILE_ACK_REPORT_ID: From 44de5bbe7f1f73297277c37d1d0bdef8501f25d5 Mon Sep 17 00:00:00 2001 From: Joseph East Date: Mon, 7 Apr 2025 22:43:23 +0930 Subject: [PATCH 7/9] Improve passive state updates in the driver * Include a profile_sticky property to prevent active_profile updates when curve mode is known to be running. * Passively detect curve mode by processing its ack report. * Remove stickiness when a profile ack report is processed. * Remove scaling hack, appears to be no longer needed in Linux 6.14? * Documentation updates Signed-off-by: Joseph East --- Documentation/hwmon/razer_hanbo.rst | 35 ++++++++++++++---------- drivers/hwmon/razer_hanbo.c | 41 ++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/Documentation/hwmon/razer_hanbo.rst b/Documentation/hwmon/razer_hanbo.rst index 8b5ec1a..e3bdf33 100644 --- a/Documentation/hwmon/razer_hanbo.rst +++ b/Documentation/hwmon/razer_hanbo.rst @@ -89,14 +89,13 @@ using the ``tempX_auto_pointY_pwm`` nodes in sysfs. e.g. to set fan curve point ``echo 40 > /sys/class/<...>/hwmonZ/temp2_auto_point2_pwm`` When writing to these nodes, the driver will accept values between 20-100 -inclusive (x14-x64) and clamp invalid values to the relevant extreme. Curve PWM -values must be equal to or greater than the previous point as the curve -progresses. Switching to profile 4, fan or pump, sanity checks the associated -curve before uploading to the AIO. An invalid curve is reported upon attempting -to switch to profile 4 via ``write error: Invalid argument``, in which case no -changes are made to the AIO. Should profile 4 be active and a curve point is -altered via sysfs you will need to set profile 4 again on that channel to upload -the new curve. +inclusive (x14-x64) and clamp invalid values to the relevant extreme. PWM values +must be monotonically increasing along the curve. Switching to profile 4, fan or +pump, sanity checks the associated curve before uploading to the AIO. An invalid +curve is reported upon attempting to switch to profile 4 as +``write error: Invalid argument``, in which case no changes are made to the AIO. +Should profile 4 be active and a curve point is altered via sysfs you will need +to set profile 4 again on that channel to upload the new curve. AIO Reference Temperature ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -124,12 +123,14 @@ Driver Lifecycle The Razer Hanbo Chroma does not provide sufficient reporting to reconstruct its complete internal state should the driver or other user of it happen to reset. -One side effect of this is that it is impossible to determine if profile 4 -specifically is actually running on the AIO; the driver had to have been active -at the time when the command was sent and be the origin of that command. This is -not the case for other profiles. If the driver initiated curve mode, it will -make profile 4 'sticky' when reading ``pwmX_enable`` until the driver is -commanded to change profile. +One side effect of this is that the state of profile 4 is implied on the AIO as +there is no way to query it. The driver had to have been active at the time, +issued the command, or captured its ACK report after something else did. +If the driver believes that profile 4 is active regardless of how, it will +present profile 4 when reading ``pwmX_enable``. + +This is not the case for other profiles which are explicitly defined in the +standard status reports. Similarly, the CPU reference temperature at ``temp2_input`` only reflects what the driver previously sent to the AIO in this session when read, not what the @@ -141,6 +142,12 @@ initialized with basic fan and pump curves. This is to prevent activation of profile 4 with unknown curve parameters. The driver does not set any profile upon being loaded. +As writing to sysfs often requires administrative level privileges, naturally +most usermode implementations write to the device directly using udev to provide +access. Changing the state of the AIO in this way won't necessarily be reflected +in the driver, though the driver will intercept reports from the AIO and attempt +to update its internal state to match. + The driver state is retained during sleep and resume but will be lost on shutdown. If one intends to use profile 4 as their default it should be manually reloaded every time the driver is started for accurate state tracking. diff --git a/drivers/hwmon/razer_hanbo.c b/drivers/hwmon/razer_hanbo.c index 5626da8..60ecb4c 100644 --- a/drivers/hwmon/razer_hanbo.c +++ b/drivers/hwmon/razer_hanbo.c @@ -30,7 +30,7 @@ /* Firmware command response signatures */ #define FIRMWARE_STATUS_REPORT_ID 0x02 #define PUMP_STATUS_REPORT_ID 0x13 -#define PUMP_ACK_REPORT_ID 0x15 +#define PUMP_PROFILE_ACK_REPORT_ID 0x15 #define PUMP_CURVE_ACK_REPORT_ID 0x19 #define FAN_STATUS_REPORT_ID 0x21 #define FAN_PROFILE_ACK_REPORT_ID 0x23 @@ -96,6 +96,7 @@ struct hanbo_pwm_channel { u8 attained_pwm; u8 active_profile; u8 pwm_points[CUSTOM_CURVE_POINTS]; + u8 profile_sticky; }; /* Global data structure for HID and hwmon functions */ @@ -248,6 +249,7 @@ static int hanbo_hid_profile_send(struct hanbo_data *priv, int channel, if (ret < 0) return ret; ret = hanbo_hid_write_expanded(priv, set_profile_cmd, SET_CURVE_CMD_LENGTH); + priv->channel_info[channel].profile_sticky = true; } else { /* sending a profile */ memcpy(set_profile_cmd, set_pump_fan_cmd_template, SET_PROFILE_CMD_LENGTH); /* Templates come with pump commands, replace with fan commands */ @@ -257,6 +259,7 @@ static int hanbo_hid_profile_send(struct hanbo_data *priv, int channel, /* Technically this value does nothing, kept as OEM software sends it */ set_profile_cmd[SET_PROFILE_PWM_OFFSET] = profile_base_duties[profile]; ret = hanbo_hid_write_expanded(priv, set_profile_cmd, SET_PROFILE_CMD_LENGTH); + priv->channel_info[channel].profile_sticky = false; } if (ret >= 0) priv->channel_info[channel].active_profile = profile; @@ -324,12 +327,7 @@ static int hanbo_hwmon_read(struct device *dev, enum hwmon_sensor_types type, case hwmon_pwm: switch (attr) { case hwmon_pwm_input: - /* - * Scaling hack for lm-sensors to show real-er PWM readings. - * The Hanbo generally reports base 10 values-as-hex, it does - * not utilise the complete 8-bit number space. - */ - *val = ((int)(priv->channel_info[channel].attained_pwm * 2) & 0xFF); + *val = ((int)(priv->channel_info[channel].attained_pwm) & 0xFF); break; case hwmon_pwm_enable: *val = priv->channel_info[channel].active_profile; @@ -655,7 +653,7 @@ static int hanbo_raw_event(struct hid_device *hdev, struct hid_report *report, priv->channel_info[PUMP_CHANNEL].tacho = get_unaligned_be16(data + 7); priv->channel_info[PUMP_CHANNEL].attained_pwm = data[10]; priv->channel_info[PUMP_CHANNEL].commanded_pwm = data[9]; - if (priv->channel_info[PUMP_CHANNEL].active_profile != CURVE_PROFILE_ID) + if (!priv->channel_info[PUMP_CHANNEL].profile_sticky) priv->channel_info[PUMP_CHANNEL].active_profile = data[3]; break; case FAN_STATUS_REPORT_ID: @@ -665,21 +663,36 @@ static int hanbo_raw_event(struct hid_device *hdev, struct hid_report *report, priv->channel_info[FAN_CHANNEL].tacho = get_unaligned_be16(data + 6); priv->channel_info[FAN_CHANNEL].attained_pwm = data[9]; priv->channel_info[FAN_CHANNEL].commanded_pwm = data[8]; - if (priv->channel_info[FAN_CHANNEL].active_profile != CURVE_PROFILE_ID) + if (!priv->channel_info[FAN_CHANNEL].profile_sticky) priv->channel_info[FAN_CHANNEL].active_profile = data[4]; break; /* Acknowledgment reports for commands */ - case PUMP_ACK_REPORT_ID: case PUMP_CURVE_ACK_REPORT_ID: + case FAN_CURVE_ACK_REPORT_ID: + case PUMP_PROFILE_ACK_REPORT_ID: case FAN_PROFILE_ACK_REPORT_ID: case CPU_TEMP_ACK_REPORT_ID: - case FAN_CURVE_ACK_REPORT_ID: case RGB_MODE_SET_ACK_REPORT_ID: ret = hanbo_hid_validate_header(REG_ACK_LENGTH, data, 3); if (ret < 0) { - hid_warn(hdev, "Received corrupted thermal ACK report"); + hid_warn(hdev, "Received corrupted mode ACK report"); goto fail_and_return; } + /* + * Passively update driver state if usermode apps are commanding + * the device. + */ + if (rid == PUMP_CURVE_ACK_REPORT_ID) { + priv->channel_info[PUMP_CHANNEL].active_profile = CURVE_PROFILE_ID; + priv->channel_info[PUMP_CHANNEL].profile_sticky = true; + } else if (rid == FAN_CURVE_ACK_REPORT_ID) { + priv->channel_info[FAN_CHANNEL].active_profile = CURVE_PROFILE_ID; + priv->channel_info[FAN_CHANNEL].profile_sticky = true; + } else if (rid == PUMP_PROFILE_ACK_REPORT_ID) { + priv->channel_info[PUMP_CHANNEL].profile_sticky = false; + } else if (rid == FAN_PROFILE_ACK_REPORT_ID) { + priv->channel_info[FAN_CHANNEL].profile_sticky = false; + } break; /* Here for completeness, unlikely these are triggered from driver */ case BRIGHTNESS_ACK_REPORT_ID: @@ -687,7 +700,7 @@ static int hanbo_raw_event(struct hid_device *hdev, struct hid_report *report, case RGB_MODE_STATUS_REPORT_ID: ret = hanbo_hid_validate_header(SHORT_ACK_LENGTH, data, 4); if (ret < 0) { - hid_warn(hdev, "Received corrupted RGB ACK report"); + hid_warn(hdev, "Received corrupted lighting ACK report"); goto fail_and_return; } break; @@ -739,6 +752,8 @@ static int hanbo_drv_init(struct hid_device *hdev) memcpy(priv->channel_info[FAN_CHANNEL].pwm_points, default_fan_curve, CUSTOM_CURVE_POINTS); memcpy(priv->channel_info[PUMP_CHANNEL].pwm_points, default_pump_curve, CUSTOM_CURVE_POINTS); + priv->channel_info[FAN_CHANNEL].profile_sticky = false; + priv->channel_info[PUMP_CHANNEL].profile_sticky = false; return ret; } From c4a63fa26c8235737fa15ce0043cf87f4457d447 Mon Sep 17 00:00:00 2001 From: Joseph East Date: Tue, 22 Apr 2025 22:13:15 +0930 Subject: [PATCH 8/9] Implement firmware field decoding. Signed-off-by: Joseph East --- drivers/hwmon/razer_hanbo.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/drivers/hwmon/razer_hanbo.c b/drivers/hwmon/razer_hanbo.c index 60ecb4c..59e7ceb 100644 --- a/drivers/hwmon/razer_hanbo.c +++ b/drivers/hwmon/razer_hanbo.c @@ -61,12 +61,11 @@ static const u8 ack_header_type_b[] = { 0x00, 0x02, 0x02, 0x01 }; #define SET_PROFILE_CMD_LENGTH 4 #define SET_CURVE_CMD_LENGTH 13 #define SET_CPU_TEMP_CMD_LENGTH 6 -#define FIRMWARE_VERSION_LENGTH 8 #define SERIAL_NUMBER_LENGTH 15 #define SET_PROFILE_ID_OFFSET 2 #define SET_PROFILE_PWM_OFFSET 3 #define SET_CPU_TEMP_PAYLOAD_OFFSET 2 -#define FIRMWARE_VERSION_OFFSET 26 +#define FIRMWARE_VERSION_OFFSET 29 #define SERIAL_NUMBER_OFFSET 3 #define CURVE_PAYLOAD_OFFSET 4 #define SHORT_ACK_LENGTH 2 @@ -117,7 +116,7 @@ struct hanbo_data { struct hanbo_pwm_channel channel_info[2]; /* Staging buffer for sending HID packets */ u8 *buffer; - u8 firmware_version[8]; + u8 firmware_version[6]; char serial_number[15]; unsigned long updated; /* jiffies */ }; @@ -575,11 +574,8 @@ static const struct hwmon_chip_info hanbo_chip_info = { static int firmware_version_show(struct seq_file *seqf, void *unused) { struct hanbo_data *priv = seqf->private; - int i; - for (i = 0; i < FIRMWARE_VERSION_LENGTH; i++) - seq_printf(seqf, "%02X", priv->firmware_version[i]); - seq_puts(seqf, "\n"); + seq_printf(seqf, "%s\n", priv->firmware_version); return 0; } @@ -601,7 +597,7 @@ static void hanbo_debugfs_init(struct hanbo_data *priv) { char name[64]; - if (priv->firmware_version[0] != 0x80) + if (priv->firmware_version[0] == '\0') return; /* When here, nothing to show in debugfs */ scnprintf(name, sizeof(name), "%s-%s", DRIVER_NAME, @@ -637,9 +633,12 @@ static int hanbo_raw_event(struct hid_device *hdev, struct hid_report *report, if (ret < 0) goto fail_and_return; int i; + char major = 0x30 + data[FIRMWARE_VERSION_OFFSET]; + char minor = 0x30 + (data[FIRMWARE_VERSION_OFFSET + 1] >> 4 & 0x0F); + char patch = 0x30 + (data[FIRMWARE_VERSION_OFFSET + 1] & 0x0F); - for (i = 0; i < FIRMWARE_VERSION_LENGTH; i++) - priv->firmware_version[i] = data[FIRMWARE_VERSION_OFFSET + i]; + snprintf(priv->firmware_version, sizeof(priv->firmware_version), + "%c.%c.%c", major, minor, patch); for (i = 0; i < SERIAL_NUMBER_LENGTH; i++) priv->serial_number[i] = data[SERIAL_NUMBER_OFFSET + i]; if (!completion_done(&priv->fw_version_processed)) @@ -729,6 +728,7 @@ static int hanbo_drv_init(struct hid_device *hdev) struct hanbo_data *priv = hid_get_drvdata(hdev); int ret; + priv->firmware_version[0] = '\0'; ret = hanbo_hid_write_expanded(priv, get_firmware_ver_cmd, GET_FIRMWARE_VER_CMD_LENGTH); if (ret < 0) From e5e08546074826439627c458875d0e9f022531a1 Mon Sep 17 00:00:00 2001 From: Joseph East Date: Wed, 23 Apr 2025 20:32:25 +0930 Subject: [PATCH 9/9] Fix serial number offset Signed-off-by: Joseph East --- drivers/hwmon/razer_hanbo.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hwmon/razer_hanbo.c b/drivers/hwmon/razer_hanbo.c index 59e7ceb..5c96a3d 100644 --- a/drivers/hwmon/razer_hanbo.c +++ b/drivers/hwmon/razer_hanbo.c @@ -66,7 +66,7 @@ static const u8 ack_header_type_b[] = { 0x00, 0x02, 0x02, 0x01 }; #define SET_PROFILE_PWM_OFFSET 3 #define SET_CPU_TEMP_PAYLOAD_OFFSET 2 #define FIRMWARE_VERSION_OFFSET 29 -#define SERIAL_NUMBER_OFFSET 3 +#define SERIAL_NUMBER_OFFSET 2 #define CURVE_PAYLOAD_OFFSET 4 #define SHORT_ACK_LENGTH 2 #define REG_ACK_LENGTH 3