From 088254c0c0a74fca3e13ed91122db7b78487a996 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 6 Oct 2025 09:13:04 +0300 Subject: [PATCH] initial commit Signed-off-by: NotAShelf Change-Id: I6a6a6964fed03491314c60f5b010e83516acafb0 --- Makefile | 49 +++ deepcool.c | 1087 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1136 insertions(+) create mode 100644 Makefile create mode 100644 deepcool.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ec1e969 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: GPL-2.0-only +obj-m += deepcool.o + + +VERSION := 0.1.0 +TARGET := $(shell uname -r) +KVERSION := $(shell uname -r) +KDIR := /lib/modules/$(KVERSION)/build +DKMS_ROOT_PATH := /usr/src/deepcool-$(VERSION) + +all: + @$(MAKE) -C $(KDIR) M=$(CURDIR) modules + +clean: + @$(MAKE) -C $(KDIR) M=$(CURDIR) clean + +dkms-install: + mkdir $(DKMS_ROOT_PATH) + cp $(CURDIR)/dkms.conf $(DKMS_ROOT_PATH) + cp $(CURDIR)/Makefile $(DKMS_ROOT_PATH) + cp $(CURDIR)/deepcool.c $(DKMS_ROOT_PATH) + + sed -e "s/@CFLGS@/${MCFLAGS}/" \ + -e "s/@VERSION@/$(VERSION)/" \ + -i $(DKMS_ROOT_PATH)/dkms.conf + + dkms add deepcool/$(VERSION) + dkms build deepcool/$(VERSION) + dkms install deepcool/$(VERSION) + +dkms-uninstall: + dkms remove zenpower/$(VERSION) --all + rm -rf $(DKMS_ROOT_PATH) + + +help: + @echo "" + @echo "Targets:" + @echo " all - Build the kernel module (default)" + @echo " clean - Remove build artifacts" + @echo " install - Install the module to the system" + @echo " uninstall - Remove the module from the system" + @echo " load - Load the module" + @echo " unload - Unload the module" + @echo " reload - Unload and reload the module" + @echo "" + @echo "Current kernel version: $(KVERSION)" + +.PHONY: all clean install uninstall load unload reload help diff --git a/deepcool.c b/deepcool.c new file mode 100644 index 0000000..653545f --- /dev/null +++ b/deepcool.c @@ -0,0 +1,1087 @@ +// SPDX-License-Identifier: GPL-2.0-or-only +/* + * Deepcool Digital Linux Kernel Driver + * + * Driver for Deepcool Digital USB HID devices (CPU coolers, AIOs, cases) + * Provides sysfs interface for monitoring and controlling display modes + * + * Copyright (C) 2025 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRIVER_NAME "deepcool_digital" +#define DRIVER_VERSION "0.1.0" + +// Vendor and Product IDs +#define DEEPCOOL_VENDOR_ID 0x3633 +#define CH510_VENDOR_ID 0x34D3 +#define CH510_PRODUCT_ID 0x1100 + +// Update intervals +#define DEFAULT_UPDATE_INTERVAL_MS 1000 +#define MIN_UPDATE_INTERVAL_MS 100 +#define MAX_UPDATE_INTERVAL_MS 2000 +#define AUTO_MODE_INTERVAL_MS 5000 + +// Temperature limits +#define TEMP_WARNING_C 80 +#define TEMP_WARNING_F 176 +#define TEMP_LIMIT_C 90 +#define TEMP_LIMIT_F 194 + +// Device series identifiers +enum deepcool_series { + DEEPCOOL_AK_SERIES, + DEEPCOOL_LS_SERIES, + DEEPCOOL_AG_SERIES, + DEEPCOOL_LQ_SERIES, + DEEPCOOL_LD_SERIES, + DEEPCOOL_LP_SERIES, + DEEPCOOL_CH_SERIES, + DEEPCOOL_CH_SERIES_GEN2, + DEEPCOOL_CH510, + DEEPCOOL_AK400_PRO, + DEEPCOOL_AK620_PRO, + DEEPCOOL_UNKNOWN, +}; + +// Display modes +enum deepcool_mode { + MODE_DEFAULT = 0, + MODE_AUTO, + MODE_CPU_TEMP, + MODE_CPU_USAGE, + MODE_CPU_POWER, + MODE_CPU_FREQ, + MODE_CPU_FAN, + MODE_GPU_TEMP, + MODE_GPU_USAGE, + MODE_GPU_POWER, + MODE_CPU, + MODE_GPU, + MODE_PSU, + MODE_MAX, +}; + +// Represents a device +struct deepcool_device { + struct hid_device *hdev; + struct device *hwmon_dev; + struct delayed_work update_work; + struct mutex lock; + + // Device identification + enum deepcool_series series; + u16 product_id; + + // Configuration + enum deepcool_mode mode; + enum deepcool_mode secondary_mode; + unsigned int update_interval_ms; + bool fahrenheit; + bool alarm_enabled; + u8 rotation; + + // CPU monitoring state + u64 prev_idle_time; + u64 prev_total_time; + u64 prev_energy_uj; + unsigned long prev_jiffies; + + // Cached sensor values + s32 cpu_temp; + u8 cpu_usage; + u16 cpu_power; + u16 cpu_freq; + + // Auto mode state + unsigned long auto_mode_last_change; + enum deepcool_mode auto_mode_current; + + // Device capabilities + bool supports_secondary_mode; + bool supports_rotation; + bool supports_alarm; + bool supports_fahrenheit; +}; + +// Map mode names +static const char *mode_names[MODE_MAX] = { + [MODE_DEFAULT] = "default", + [MODE_AUTO] = "auto", + [MODE_CPU_TEMP] = "cpu_temp", + [MODE_CPU_USAGE] = "cpu_usage", + [MODE_CPU_POWER] = "cpu_power", + [MODE_CPU_FREQ] = "cpu_freq", + [MODE_CPU_FAN] = "cpu_fan", + [MODE_GPU_TEMP] = "gpu_temp", + [MODE_GPU_USAGE] = "gpu_usage", + [MODE_GPU_POWER] = "gpu_power", + [MODE_CPU] = "cpu", + [MODE_GPU] = "gpu", + [MODE_PSU] = "psu", +}; + +static void deepcool_update_work(struct work_struct *work); +static int deepcool_send_packet(struct deepcool_device *ddata); + +// CPU Monitoring + +static s32 deepcool_read_cpu_temp(void) { + struct thermal_zone_device *tz; + int temp = 0; + int ret; + + // Try to find CPU thermal zone + // Keyword: try + // This sometimes fails unexpectedly and I do not know why + tz = thermal_zone_get_zone_by_name("x86_pkg_temp"); + if (IS_ERR(tz)) { + tz = thermal_zone_get_zone_by_name("cpu_thermal"); + if (IS_ERR(tz)) + return 0; + } + + ret = thermal_zone_get_temp(tz, &temp); + if (ret < 0) + return 0; + + return temp / 1000; +} + +static void deepcool_read_cpu_usage(struct deepcool_device *ddata) { + u64 idle_time = 0, total_time = 0; + u64 delta_idle, delta_total; + int cpu; + + // Sum up idle and total time across all CPUs + for_each_possible_cpu(cpu) { + struct kernel_cpustat *kcs = &kcpustat_cpu(cpu); + + idle_time += kcs->cpustat[CPUTIME_IDLE]; + idle_time += kcs->cpustat[CPUTIME_IOWAIT]; + + total_time += kcs->cpustat[CPUTIME_USER]; + total_time += kcs->cpustat[CPUTIME_NICE]; + total_time += kcs->cpustat[CPUTIME_SYSTEM]; + total_time += kcs->cpustat[CPUTIME_IDLE]; + total_time += kcs->cpustat[CPUTIME_IOWAIT]; + total_time += kcs->cpustat[CPUTIME_IRQ]; + total_time += kcs->cpustat[CPUTIME_SOFTIRQ]; + total_time += kcs->cpustat[CPUTIME_STEAL]; + } + + if (ddata->prev_total_time == 0) { + ddata->prev_idle_time = idle_time; + ddata->prev_total_time = total_time; + ddata->cpu_usage = 0; + return; + } + + delta_idle = idle_time - ddata->prev_idle_time; + delta_total = total_time - ddata->prev_total_time; + + if (delta_total > 0) + ddata->cpu_usage = 100 - (u8)((delta_idle * 100) / delta_total); + else + ddata->cpu_usage = 0; + + ddata->prev_idle_time = idle_time; + ddata->prev_total_time = total_time; +} + +static u16 deepcool_read_cpu_freq(void) { + unsigned int freq_khz = 0; + unsigned int max_freq = 0; + int cpu; + + // Find the highest CPU frequency across all cores + for_each_possible_cpu(cpu) { + freq_khz = cpufreq_quick_get(cpu); + if (freq_khz > max_freq) + max_freq = freq_khz; + } + + // Convert kHz to MHz + return max_freq / 1000; +} + +static u64 deepcool_read_cpu_energy(void) { + struct file *fp; + char buf[64]; + u64 energy = 0; + loff_t pos = 0; + int ret; + + // TODO: confirm if this path can change + fp = filp_open("/sys/class/powercap/intel-rapl/intel-rapl:0/energy_uj", + O_RDONLY, 0); + if (IS_ERR(fp)) + return 0; + + ret = kernel_read(fp, buf, sizeof(buf) - 1, &pos); + if (ret > 0) { + buf[ret] = '\0'; + ret = kstrtou64(buf, 10, &energy); + if (ret < 0) + energy = 0; + } + + filp_close(fp, NULL); + return energy; +} + +static void deepcool_calc_cpu_power(struct deepcool_device *ddata) { + u64 current_energy; + u64 delta_energy; + unsigned long current_jiffies; + unsigned long delta_ms; + u64 max_energy_uj = 262143328850ULL; // default max for Intel RAPL + + current_energy = deepcool_read_cpu_energy(); + current_jiffies = jiffies; + + if (ddata->prev_energy_uj == 0 || current_energy == 0) { + ddata->prev_energy_uj = current_energy; + ddata->prev_jiffies = current_jiffies; + ddata->cpu_power = 0; + return; + } + + delta_ms = jiffies_to_msecs(current_jiffies - ddata->prev_jiffies); + if (delta_ms == 0) { + ddata->cpu_power = 0; + return; + } + + // Handle counter rollover + if (current_energy > ddata->prev_energy_uj) + delta_energy = current_energy - ddata->prev_energy_uj; + else + delta_energy = (max_energy_uj + current_energy) - ddata->prev_energy_uj; + + // Calculate power: W = ΔμJ / (Δms * 1000) + ddata->cpu_power = (u16)(delta_energy / (delta_ms * 1000)); + + ddata->prev_energy_uj = current_energy; + ddata->prev_jiffies = current_jiffies; +} + +static void deepcool_update_sensors(struct deepcool_device *ddata) { + ddata->cpu_temp = deepcool_read_cpu_temp(); + deepcool_read_cpu_usage(ddata); + ddata->cpu_freq = deepcool_read_cpu_freq(); + deepcool_calc_cpu_power(ddata); +} + +// Auto Mode Management +static enum deepcool_mode +deepcool_next_auto_mode(struct deepcool_device *ddata) { + enum deepcool_mode next; + + switch (ddata->series) { + case DEEPCOOL_AK_SERIES: + // Cycle: cpu_temp -> cpu_usage -> cpu_temp + if (ddata->auto_mode_current == MODE_CPU_TEMP) + next = MODE_CPU_USAGE; + else + next = MODE_CPU_TEMP; + break; + + case DEEPCOOL_LS_SERIES: + // cpu_temp -> cpu_power -> cpu_temp + if (ddata->auto_mode_current == MODE_CPU_TEMP) + next = MODE_CPU_POWER; + else + next = MODE_CPU_TEMP; + break; + + case DEEPCOOL_AG_SERIES: + // cpu_temp -> cpu_usage -> cpu_temp + if (ddata->auto_mode_current == MODE_CPU_TEMP) + next = MODE_CPU_USAGE; + else + next = MODE_CPU_TEMP; + break; + + case DEEPCOOL_AK620_PRO: + case DEEPCOOL_AK400_PRO: + // Always show all metrics simultaneously + next = MODE_AUTO; + break; + + default: + next = MODE_CPU_TEMP; + break; + } + + return next; +} + +static void deepcool_handle_auto_mode(struct deepcool_device *ddata) { + unsigned long now = jiffies; + + if (time_after(now, ddata->auto_mode_last_change + + msecs_to_jiffies(AUTO_MODE_INTERVAL_MS))) { + ddata->auto_mode_current = deepcool_next_auto_mode(ddata); + ddata->auto_mode_last_change = now; + } +} + +// Device communication + +static int deepcool_send_ak620_pro_packet(struct deepcool_device *ddata) { + u8 data[64] = {0}; + u16 checksum = 0; + u16 power, freq; + u8 temp; + int i, ret; + + // Packet header + data[0] = 16; + data[1] = 104; + data[2] = 1; + data[3] = 4; + data[4] = 13; + data[5] = 1; + data[6] = 2; + data[7] = 8; + + // Power consumption (big-endian) + power = cpu_to_be16(ddata->cpu_power); + data[8] = (power >> 8) & 0xFF; + data[9] = power & 0xFF; + + // Temp + temp = ddata->cpu_temp; + if (ddata->fahrenheit) { + temp = (temp * 9 / 5) + 32; + data[10] = 1; + } else { + data[10] = 0; + } + + // Convert to float representation + // TODO: this can be made more robust + data[11] = 0; + data[12] = 0; + data[13] = temp; + data[14] = 0; + + // Utilization + data[15] = ddata->cpu_usage; + + // Frequency (big-endian) + freq = cpu_to_be16(ddata->cpu_freq); + data[16] = (freq >> 8) & 0xFF; + data[17] = freq & 0xFF; + + // Calculate checksum + for (i = 1; i <= 17; i++) + checksum += data[i]; + data[18] = checksum % 256; + data[19] = 22; + + // Send HID output report + ret = hid_hw_output_report(ddata->hdev, data, sizeof(data)); + if (ret < 0) { + hid_err(ddata->hdev, "Failed to send packet: %d\n", ret); + return ret; + } + + return 0; +} + +static int deepcool_send_ak_series_packet(struct deepcool_device *ddata) { + u8 data[64] = {0}; + u16 checksum = 0; + enum deepcool_mode current_mode; + u8 mode_byte; + int i, ret; + + // Determine current mode + if (ddata->mode == MODE_AUTO) { + deepcool_handle_auto_mode(ddata); + current_mode = ddata->auto_mode_current; + } else { + current_mode = ddata->mode; + } + + // Initialize packet header + data[0] = 16; + data[1] = 106; + data[2] = 1; + data[3] = 3; + + // Mode byte + switch (current_mode) { + case MODE_CPU_TEMP: + mode_byte = 0; + break; + case MODE_CPU_USAGE: + mode_byte = 1; + break; + default: + mode_byte = 0; + break; + } + data[4] = mode_byte; + + // Alarm flag + data[5] = ddata->alarm_enabled ? 1 : 0; + + // Temperature/usage value + if (current_mode == MODE_CPU_TEMP) { + u8 temp = ddata->cpu_temp; + if (ddata->fahrenheit) + temp = (temp * 9 / 5) + 32; + data[6] = temp; + } else { + data[6] = ddata->cpu_usage; + } + + // Calculate checksum + for (i = 1; i <= 6; i++) + checksum += data[i]; + data[7] = checksum % 256; + data[8] = 22; + + // Send HID output report + ret = hid_hw_output_report(ddata->hdev, data, sizeof(data)); + if (ret < 0) { + hid_err(ddata->hdev, "Failed to send packet: %d\n", ret); + return ret; + } + + return 0; +} + +static int deepcool_send_ls_series_packet(struct deepcool_device *ddata) { + u8 data[64] = {0}; + u16 checksum = 0; + enum deepcool_mode current_mode; + u8 mode_byte; + u16 value; + int i, ret; + + // Determine current mode + if (ddata->mode == MODE_AUTO) { + deepcool_handle_auto_mode(ddata); + current_mode = ddata->auto_mode_current; + } else { + current_mode = ddata->mode; + } + + // Initialize packet header + data[0] = 16; + data[1] = 175; + data[2] = 3; + + // Mode byte + switch (current_mode) { + case MODE_CPU_TEMP: + mode_byte = 0; + break; + case MODE_CPU_POWER: + mode_byte = 2; + break; + default: + mode_byte = 0; + break; + } + data[3] = mode_byte; + + // Alarm flag + data[4] = ddata->alarm_enabled ? 1 : 0; + + // Value (big-endian) + if (current_mode == MODE_CPU_TEMP) { + u8 temp = ddata->cpu_temp; + if (ddata->fahrenheit) + temp = (temp * 9 / 5) + 32; + value = cpu_to_be16(temp); + } else { + value = cpu_to_be16(ddata->cpu_power); + } + data[5] = (value >> 8) & 0xFF; + data[6] = value & 0xFF; + + // Calculate checksum + for (i = 1; i <= 6; i++) + checksum += data[i]; + data[7] = checksum % 256; + data[8] = 22; + + // Send HID output report + ret = hid_hw_output_report(ddata->hdev, data, sizeof(data)); + if (ret < 0) { + hid_err(ddata->hdev, "Failed to send packet: %d\n", ret); + return ret; + } + + return 0; +} + +static int deepcool_send_ag_series_packet(struct deepcool_device *ddata) { + u8 data[64] = {0}; + u16 checksum = 0; + enum deepcool_mode current_mode; + u8 mode_byte; + int i, ret; + + // Determine current mode + if (ddata->mode == MODE_AUTO) { + deepcool_handle_auto_mode(ddata); + current_mode = ddata->auto_mode_current; + } else { + current_mode = ddata->mode; + } + + // Initialize packet header + data[0] = 16; + data[1] = 170; + data[2] = 1; + data[3] = 3; + + // Mode byte + switch (current_mode) { + case MODE_CPU_TEMP: + mode_byte = 0; + break; + case MODE_CPU_USAGE: + mode_byte = 1; + break; + default: + mode_byte = 0; + break; + } + data[4] = mode_byte; + + // Alarm flag + data[5] = ddata->alarm_enabled ? 1 : 0; + + // Temperature/usage value + if (current_mode == MODE_CPU_TEMP) { + data[6] = ddata->cpu_temp; + } else { + data[6] = ddata->cpu_usage; + } + + // Calculate checksum + for (i = 1; i <= 6; i++) + checksum += data[i]; + data[7] = checksum % 256; + data[8] = 22; // termination byte + + // Send HID output report + ret = hid_hw_output_report(ddata->hdev, data, sizeof(data)); + if (ret < 0) { + hid_err(ddata->hdev, "Failed to send packet: %d\n", ret); + return ret; + } + + return 0; +} + +static int deepcool_send_packet(struct deepcool_device *ddata) { + int ret; + + switch (ddata->series) { + case DEEPCOOL_AK620_PRO: + case DEEPCOOL_AK400_PRO: + ret = deepcool_send_ak620_pro_packet(ddata); + break; + case DEEPCOOL_AK_SERIES: + ret = deepcool_send_ak_series_packet(ddata); + break; + case DEEPCOOL_LS_SERIES: + ret = deepcool_send_ls_series_packet(ddata); + break; + case DEEPCOOL_AG_SERIES: + ret = deepcool_send_ag_series_packet(ddata); + break; + default: + hid_warn(ddata->hdev, "Unsupported device series\n"); + ret = -EOPNOTSUPP; + break; + } + + return ret; +} + +// Work queue handler +static void deepcool_update_work(struct work_struct *work) { + struct deepcool_device *ddata = + container_of(work, struct deepcool_device, update_work.work); + + mutex_lock(&ddata->lock); + + // Update sensor readings + deepcool_update_sensors(ddata); + + // Send updated packet to device + deepcool_send_packet(ddata); + + mutex_unlock(&ddata->lock); + + // Schedule next update + schedule_delayed_work(&ddata->update_work, + msecs_to_jiffies(ddata->update_interval_ms)); +} + +// Sysfs attrs + +static ssize_t mode_show(struct device *dev, struct device_attribute *attr, + char *buf) { + struct deepcool_device *ddata = dev_get_drvdata(dev); + const char *mode_str; + + mutex_lock(&ddata->lock); + mode_str = mode_names[ddata->mode]; + mutex_unlock(&ddata->lock); + + return sprintf(buf, "%s\n", mode_str); +} + +static ssize_t mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) { + struct deepcool_device *ddata = dev_get_drvdata(dev); + char mode_str[32]; + enum deepcool_mode new_mode = MODE_DEFAULT; + int i; + + if (count >= sizeof(mode_str)) + return -EINVAL; + + memcpy(mode_str, buf, count); + mode_str[count] = '\0'; + + if (mode_str[count - 1] == '\n') + mode_str[count - 1] = '\0'; + + // Find matching mode + for (i = 0; i < MODE_MAX; i++) { + if (mode_names[i] && strcmp(mode_str, mode_names[i]) == 0) { + new_mode = i; + break; + } + } + + if (i == MODE_MAX) + return -EINVAL; + + mutex_lock(&ddata->lock); + ddata->mode = new_mode; + if (new_mode == MODE_AUTO) { + ddata->auto_mode_current = MODE_CPU_TEMP; + ddata->auto_mode_last_change = jiffies; + } + mutex_unlock(&ddata->lock); + + return count; +} + +static ssize_t update_interval_show(struct device *dev, + struct device_attribute *attr, char *buf) { + struct deepcool_device *ddata = dev_get_drvdata(dev); + unsigned int interval; + + mutex_lock(&ddata->lock); + interval = ddata->update_interval_ms; + mutex_unlock(&ddata->lock); + + return sprintf(buf, "%u\n", interval); +} + +static ssize_t update_interval_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) { + struct deepcool_device *ddata = dev_get_drvdata(dev); + unsigned int interval; + int ret; + + ret = kstrtouint(buf, 10, &interval); + if (ret) + return ret; + + if (interval < MIN_UPDATE_INTERVAL_MS || interval > MAX_UPDATE_INTERVAL_MS) + return -EINVAL; + + mutex_lock(&ddata->lock); + ddata->update_interval_ms = interval; + mutex_unlock(&ddata->lock); + + // Restart work with new interval + cancel_delayed_work_sync(&ddata->update_work); + schedule_delayed_work(&ddata->update_work, msecs_to_jiffies(interval)); + + return count; +} + +static ssize_t fahrenheit_show(struct device *dev, + struct device_attribute *attr, char *buf) { + struct deepcool_device *ddata = dev_get_drvdata(dev); + bool fahrenheit; + + mutex_lock(&ddata->lock); + fahrenheit = ddata->fahrenheit; + mutex_unlock(&ddata->lock); + + return sprintf(buf, "%d\n", fahrenheit ? 1 : 0); +} + +static ssize_t fahrenheit_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) { + struct deepcool_device *ddata = dev_get_drvdata(dev); + bool value; + int ret; + + ret = kstrtobool(buf, &value); + if (ret) + return ret; + + if (value && !ddata->supports_fahrenheit) + return -EOPNOTSUPP; + + mutex_lock(&ddata->lock); + ddata->fahrenheit = value; + mutex_unlock(&ddata->lock); + + return count; +} + +static ssize_t alarm_show(struct device *dev, struct device_attribute *attr, + char *buf) { + struct deepcool_device *ddata = dev_get_drvdata(dev); + bool alarm; + + mutex_lock(&ddata->lock); + alarm = ddata->alarm_enabled; + mutex_unlock(&ddata->lock); + + return sprintf(buf, "%d\n", alarm ? 1 : 0); +} + +static ssize_t alarm_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) { + struct deepcool_device *ddata = dev_get_drvdata(dev); + bool value; + int ret; + + ret = kstrtobool(buf, &value); + if (ret) + return ret; + + if (value && !ddata->supports_alarm) + return -EOPNOTSUPP; + + mutex_lock(&ddata->lock); + ddata->alarm_enabled = value; + mutex_unlock(&ddata->lock); + + return count; +} + +static ssize_t cpu_temp_show(struct device *dev, struct device_attribute *attr, + char *buf) { + struct deepcool_device *ddata = dev_get_drvdata(dev); + s32 temp; + + mutex_lock(&ddata->lock); + temp = ddata->cpu_temp; + if (ddata->fahrenheit) + temp = (temp * 9 / 5) + 32; + mutex_unlock(&ddata->lock); + + return sprintf(buf, "%d\n", temp); +} + +static ssize_t cpu_usage_show(struct device *dev, struct device_attribute *attr, + char *buf) { + struct deepcool_device *ddata = dev_get_drvdata(dev); + u8 usage; + + mutex_lock(&ddata->lock); + usage = ddata->cpu_usage; + mutex_unlock(&ddata->lock); + + return sprintf(buf, "%u\n", usage); +} + +static ssize_t cpu_power_show(struct device *dev, struct device_attribute *attr, + char *buf) { + struct deepcool_device *ddata = dev_get_drvdata(dev); + u16 power; + + mutex_lock(&ddata->lock); + power = ddata->cpu_power; + mutex_unlock(&ddata->lock); + + return sprintf(buf, "%u\n", power); +} + +static ssize_t cpu_freq_show(struct device *dev, struct device_attribute *attr, + char *buf) { + struct deepcool_device *ddata = dev_get_drvdata(dev); + u16 freq; + + mutex_lock(&ddata->lock); + freq = ddata->cpu_freq; + mutex_unlock(&ddata->lock); + + return sprintf(buf, "%u\n", freq); +} + +static DEVICE_ATTR_RW(mode); +static DEVICE_ATTR_RW(update_interval); +static DEVICE_ATTR_RW(fahrenheit); +static DEVICE_ATTR_RW(alarm); +static DEVICE_ATTR_RO(cpu_temp); +static DEVICE_ATTR_RO(cpu_usage); +static DEVICE_ATTR_RO(cpu_power); +static DEVICE_ATTR_RO(cpu_freq); + +static struct attribute *deepcool_attrs[] = {&dev_attr_mode.attr, + &dev_attr_update_interval.attr, + &dev_attr_fahrenheit.attr, + &dev_attr_alarm.attr, + &dev_attr_cpu_temp.attr, + &dev_attr_cpu_usage.attr, + &dev_attr_cpu_power.attr, + &dev_attr_cpu_freq.attr, + NULL}; + +ATTRIBUTE_GROUPS(deepcool); + +// Device identification + +static enum deepcool_series deepcool_identify_device(u16 product_id) { + switch (product_id) { + case 1 ... 4: + return DEEPCOOL_AK_SERIES; + case 5: + return DEEPCOOL_AK400_PRO; + case 6: + return DEEPCOOL_LS_SERIES; + case 7: + return DEEPCOOL_AK620_PRO; + case 8: + return DEEPCOOL_AG_SERIES; + case 9: + return DEEPCOOL_LQ_SERIES; + case 10 ... 11: + return DEEPCOOL_LD_SERIES; + case 12 ... 14: + return DEEPCOOL_LP_SERIES; + case 15 ... 17: + return DEEPCOOL_CH_SERIES; + case 18 ... 21: + return DEEPCOOL_CH_SERIES_GEN2; + case CH510_PRODUCT_ID: + return DEEPCOOL_CH510; + default: + return DEEPCOOL_UNKNOWN; + } +} + +static void deepcool_set_capabilities(struct deepcool_device *ddata) { + // Set default capabilities based on device series + switch (ddata->series) { + case DEEPCOOL_AK_SERIES: + ddata->supports_alarm = true; + ddata->supports_fahrenheit = true; + ddata->supports_secondary_mode = false; + ddata->supports_rotation = false; + break; + + case DEEPCOOL_LS_SERIES: + ddata->supports_alarm = true; + ddata->supports_fahrenheit = true; + ddata->supports_secondary_mode = false; + ddata->supports_rotation = false; + break; + + case DEEPCOOL_AG_SERIES: + ddata->supports_alarm = true; + ddata->supports_fahrenheit = false; + ddata->supports_secondary_mode = false; + ddata->supports_rotation = false; + break; + + case DEEPCOOL_LP_SERIES: + ddata->supports_alarm = false; + ddata->supports_fahrenheit = true; + ddata->supports_secondary_mode = true; + ddata->supports_rotation = true; + break; + + case DEEPCOOL_AK620_PRO: + case DEEPCOOL_AK400_PRO: + ddata->supports_alarm = false; // hard-coded alarm + ddata->supports_fahrenheit = true; + ddata->supports_secondary_mode = false; + ddata->supports_rotation = false; + break; + + default: + ddata->supports_alarm = false; + ddata->supports_fahrenheit = false; + ddata->supports_secondary_mode = false; + ddata->supports_rotation = false; + break; + } +} + +// HID Driver Callbacks +static int deepcool_probe(struct hid_device *hdev, + const struct hid_device_id *id) { + struct deepcool_device *ddata; + int ret; + + ddata = devm_kzalloc(&hdev->dev, sizeof(*ddata), GFP_KERNEL); + if (!ddata) + return -ENOMEM; + + ddata->hdev = hdev; + ddata->product_id = hdev->product; + ddata->series = deepcool_identify_device(hdev->product); + + if (ddata->series == DEEPCOOL_UNKNOWN) { + hid_err(hdev, "Unknown Deepcool device: 0x%04x\n", hdev->product); + return -ENODEV; + } + + // Set device capabilities + deepcool_set_capabilities(ddata); + + // Initialize default configuration + ddata->mode = MODE_AUTO; + ddata->auto_mode_current = MODE_CPU_TEMP; + ddata->auto_mode_last_change = jiffies; + ddata->secondary_mode = MODE_DEFAULT; + ddata->update_interval_ms = DEFAULT_UPDATE_INTERVAL_MS; + ddata->fahrenheit = false; + ddata->alarm_enabled = false; + ddata->rotation = 0; + + mutex_init(&ddata->lock); + hid_set_drvdata(hdev, ddata); + + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, "HID parse failed: %d\n", ret); + return ret; + } + + ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); + if (ret) { + hid_err(hdev, "HID hw start failed: %d\n", ret); + return ret; + } + + ret = hid_hw_open(hdev); + if (ret) { + hid_err(hdev, "HID hw open failed: %d\n", ret); + goto err_hw_stop; + } + + // Create sysfs attributes + ret = sysfs_create_group(&hdev->dev.kobj, &deepcool_group); + if (ret) { + hid_err(hdev, "Failed to create sysfs group: %d\n", ret); + goto err_hw_close; + } + + // Initialize work queue + INIT_DELAYED_WORK(&ddata->update_work, deepcool_update_work); + + // Start update work + schedule_delayed_work(&ddata->update_work, + msecs_to_jiffies(ddata->update_interval_ms)); + + hid_info(hdev, "Deepcool Digital device initialized (series: %d)\n", + ddata->series); + + return 0; + +err_hw_close: + hid_hw_close(hdev); +err_hw_stop: + hid_hw_stop(hdev); + return ret; +} + +static void deepcool_remove(struct hid_device *hdev) { + struct deepcool_device *ddata = hid_get_drvdata(hdev); + + // Stop work queue + cancel_delayed_work_sync(&ddata->update_work); + + // Remove sysfs attributes + sysfs_remove_group(&hdev->dev.kobj, &deepcool_group); + + hid_hw_close(hdev); + hid_hw_stop(hdev); + + hid_info(hdev, "Deepcool Digital device removed\n"); +} + +// HID Device ID Table +static const struct hid_device_id deepcool_devices[] = { + /* Deepcool Digital devices (vendor ID 0x3633) */ + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 1)}, // AK Series + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 2)}, + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 3)}, + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 4)}, + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 5)}, // AK400 PRO + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 6)}, // LS Series + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 7)}, // AK620 PRO + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 8)}, // AG Series + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 9)}, // LQ Series + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 10)}, // LD Series + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 11)}, + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 12)}, // LP Series + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 13)}, + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 14)}, + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 15)}, // CH Series + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 16)}, + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 17)}, + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 18)}, // CH Series Gen2 + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 19)}, + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 20)}, + {HID_USB_DEVICE(DEEPCOOL_VENDOR_ID, 21)}, + {HID_USB_DEVICE(CH510_VENDOR_ID, + CH510_PRODUCT_ID)}, // CH510 (different vendor ID) + {}}; +MODULE_DEVICE_TABLE(hid, deepcool_devices); + +// Represents a HID device +static struct hid_driver deepcool_driver = { + .name = DRIVER_NAME, + .id_table = deepcool_devices, + .probe = deepcool_probe, + .remove = deepcool_remove, + .driver = + { + .groups = deepcool_groups, + }, +}; +module_hid_driver(deepcool_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("NotAShelf "); +MODULE_DESCRIPTION("Deepcool Digital USB HID driver"); +MODULE_VERSION(DRIVER_VERSION);