deepcool/deepcool.c
NotAShelf 4fa33bab1f
use kzalloc for packet buffers; free after sending
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a696447266c0fad5bef78243a1999a52b0442
2025-10-06 21:57:30 +03:00

1099 lines
27 KiB
C

// 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 <linux/cpufreq.h>
#include <linux/cpumask.h>
#include <linux/hid.h>
#include <linux/hwmon-sysfs.h>
#include <linux/hwmon.h>
#include <linux/jiffies.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/string.h>
#include <linux/thermal.h>
#include <linux/workqueue.h>
#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 = kzalloc(64, GFP_KERNEL);
u16 checksum = 0;
u16 power, freq;
u8 temp;
int i, ret;
if (!data)
return -ENOMEM;
// 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, 64);
kfree(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 = kzalloc(64, GFP_KERNEL);
u16 checksum = 0;
enum deepcool_mode current_mode;
u8 mode_byte;
int i, ret;
if (!data)
return -ENOMEM;
// 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, 64);
kfree(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 = kzalloc(64, GFP_KERNEL);
u16 checksum = 0;
enum deepcool_mode current_mode;
u8 mode_byte;
u16 value;
int i, ret;
if (!data)
return -ENOMEM;
// 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, 64);
kfree(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 = kzalloc(64, GFP_KERNEL);
u16 checksum = 0;
enum deepcool_mode current_mode;
u8 mode_byte;
int i, ret;
if (!data)
return -ENOMEM;
// 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, 64);
kfree(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 <raf@notashelf.dev>");
MODULE_DESCRIPTION("Deepcool Digital USB HID driver");
MODULE_VERSION(DRIVER_VERSION);