Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I6a6a696447266c0fad5bef78243a1999a52b0442
1099 lines
27 KiB
C
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);
|