831 lines
20 KiB
C
831 lines
20 KiB
C
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||
|
/*
|
||
|
* Reverse-engineered NZXT RGB & Fan Controller/Smart Device v2 driver.
|
||
|
*
|
||
|
* Copyright (c) 2021 Aleksandr Mezin
|
||
|
*/
|
||
|
|
||
|
#include <linux/hid.h>
|
||
|
#include <linux/hwmon.h>
|
||
|
#include <linux/math.h>
|
||
|
#include <linux/module.h>
|
||
|
#include <linux/mutex.h>
|
||
|
#include <linux/spinlock.h>
|
||
|
#include <linux/wait.h>
|
||
|
|
||
|
#include <asm/byteorder.h>
|
||
|
#include <asm/unaligned.h>
|
||
|
|
||
|
/*
|
||
|
* The device has only 3 fan channels/connectors. But all HID reports have
|
||
|
* space reserved for up to 8 channels.
|
||
|
*/
|
||
|
#define FAN_CHANNELS 3
|
||
|
#define FAN_CHANNELS_MAX 8
|
||
|
|
||
|
#define UPDATE_INTERVAL_DEFAULT_MS 1000
|
||
|
|
||
|
/* These strings match labels on the device exactly */
|
||
|
static const char *const fan_label[] = {
|
||
|
"FAN 1",
|
||
|
"FAN 2",
|
||
|
"FAN 3",
|
||
|
};
|
||
|
|
||
|
static const char *const curr_label[] = {
|
||
|
"FAN 1 Current",
|
||
|
"FAN 2 Current",
|
||
|
"FAN 3 Current",
|
||
|
};
|
||
|
|
||
|
static const char *const in_label[] = {
|
||
|
"FAN 1 Voltage",
|
||
|
"FAN 2 Voltage",
|
||
|
"FAN 3 Voltage",
|
||
|
};
|
||
|
|
||
|
enum {
|
||
|
INPUT_REPORT_ID_FAN_CONFIG = 0x61,
|
||
|
INPUT_REPORT_ID_FAN_STATUS = 0x67,
|
||
|
};
|
||
|
|
||
|
enum {
|
||
|
FAN_STATUS_REPORT_SPEED = 0x02,
|
||
|
FAN_STATUS_REPORT_VOLTAGE = 0x04,
|
||
|
};
|
||
|
|
||
|
enum {
|
||
|
FAN_TYPE_NONE = 0,
|
||
|
FAN_TYPE_DC = 1,
|
||
|
FAN_TYPE_PWM = 2,
|
||
|
};
|
||
|
|
||
|
struct unknown_static_data {
|
||
|
/*
|
||
|
* Some configuration data? Stays the same after fan speed changes,
|
||
|
* changes in fan configuration, reboots and driver reloads.
|
||
|
*
|
||
|
* The same data in multiple report types.
|
||
|
*
|
||
|
* Byte 12 seems to be the number of fan channels, but I am not sure.
|
||
|
*/
|
||
|
u8 unknown1[14];
|
||
|
} __packed;
|
||
|
|
||
|
/*
|
||
|
* The device sends this input report in response to "detect fans" command:
|
||
|
* a 2-byte output report { 0x60, 0x03 }.
|
||
|
*/
|
||
|
struct fan_config_report {
|
||
|
/* report_id should be INPUT_REPORT_ID_FAN_CONFIG = 0x61 */
|
||
|
u8 report_id;
|
||
|
/* Always 0x03 */
|
||
|
u8 magic;
|
||
|
struct unknown_static_data unknown_data;
|
||
|
/* Fan type as detected by the device. See FAN_TYPE_* enum. */
|
||
|
u8 fan_type[FAN_CHANNELS_MAX];
|
||
|
} __packed;
|
||
|
|
||
|
/*
|
||
|
* The device sends these reports at a fixed interval (update interval) -
|
||
|
* one report with type = FAN_STATUS_REPORT_SPEED, and one report with type =
|
||
|
* FAN_STATUS_REPORT_VOLTAGE per update interval.
|
||
|
*/
|
||
|
struct fan_status_report {
|
||
|
/* report_id should be INPUT_REPORT_ID_STATUS = 0x67 */
|
||
|
u8 report_id;
|
||
|
/* FAN_STATUS_REPORT_SPEED = 0x02 or FAN_STATUS_REPORT_VOLTAGE = 0x04 */
|
||
|
u8 type;
|
||
|
struct unknown_static_data unknown_data;
|
||
|
/* Fan type as detected by the device. See FAN_TYPE_* enum. */
|
||
|
u8 fan_type[FAN_CHANNELS_MAX];
|
||
|
|
||
|
union {
|
||
|
/* When type == FAN_STATUS_REPORT_SPEED */
|
||
|
struct {
|
||
|
/*
|
||
|
* Fan speed, in RPM. Zero for channels without fans
|
||
|
* connected.
|
||
|
*/
|
||
|
__le16 fan_rpm[FAN_CHANNELS_MAX];
|
||
|
/*
|
||
|
* Fan duty cycle, in percent. Non-zero even for
|
||
|
* channels without fans connected.
|
||
|
*/
|
||
|
u8 duty_percent[FAN_CHANNELS_MAX];
|
||
|
/*
|
||
|
* Exactly the same values as duty_percent[], non-zero
|
||
|
* for disconnected fans too.
|
||
|
*/
|
||
|
u8 duty_percent_dup[FAN_CHANNELS_MAX];
|
||
|
/* "Case Noise" in db */
|
||
|
u8 noise_db;
|
||
|
} __packed fan_speed;
|
||
|
/* When type == FAN_STATUS_REPORT_VOLTAGE */
|
||
|
struct {
|
||
|
/*
|
||
|
* Voltage, in millivolts. Non-zero even when fan is
|
||
|
* not connected.
|
||
|
*/
|
||
|
__le16 fan_in[FAN_CHANNELS_MAX];
|
||
|
/*
|
||
|
* Current, in milliamperes. Near-zero when
|
||
|
* disconnected.
|
||
|
*/
|
||
|
__le16 fan_current[FAN_CHANNELS_MAX];
|
||
|
} __packed fan_voltage;
|
||
|
} __packed;
|
||
|
} __packed;
|
||
|
|
||
|
#define OUTPUT_REPORT_SIZE 64
|
||
|
|
||
|
enum {
|
||
|
OUTPUT_REPORT_ID_INIT_COMMAND = 0x60,
|
||
|
OUTPUT_REPORT_ID_SET_FAN_SPEED = 0x62,
|
||
|
};
|
||
|
|
||
|
enum {
|
||
|
INIT_COMMAND_SET_UPDATE_INTERVAL = 0x02,
|
||
|
INIT_COMMAND_DETECT_FANS = 0x03,
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
* This output report sets pwm duty cycle/target fan speed for one or more
|
||
|
* channels.
|
||
|
*/
|
||
|
struct set_fan_speed_report {
|
||
|
/* report_id should be OUTPUT_REPORT_ID_SET_FAN_SPEED = 0x62 */
|
||
|
u8 report_id;
|
||
|
/* Should be 0x01 */
|
||
|
u8 magic;
|
||
|
/* To change fan speed on i-th channel, set i-th bit here */
|
||
|
u8 channel_bit_mask;
|
||
|
/*
|
||
|
* Fan duty cycle/target speed in percent. For voltage-controlled fans,
|
||
|
* the minimal voltage (duty_percent = 1) is about 9V.
|
||
|
* Setting duty_percent to 0 (if the channel is selected in
|
||
|
* channel_bit_mask) turns off the fan completely (regardless of the
|
||
|
* control mode).
|
||
|
*/
|
||
|
u8 duty_percent[FAN_CHANNELS_MAX];
|
||
|
} __packed;
|
||
|
|
||
|
struct drvdata {
|
||
|
struct hid_device *hid;
|
||
|
struct device *hwmon;
|
||
|
|
||
|
u8 fan_duty_percent[FAN_CHANNELS];
|
||
|
u16 fan_rpm[FAN_CHANNELS];
|
||
|
bool pwm_status_received;
|
||
|
|
||
|
u16 fan_in[FAN_CHANNELS];
|
||
|
u16 fan_curr[FAN_CHANNELS];
|
||
|
bool voltage_status_received;
|
||
|
|
||
|
u8 fan_type[FAN_CHANNELS];
|
||
|
bool fan_config_received;
|
||
|
|
||
|
/*
|
||
|
* wq is used to wait for *_received flags to become true.
|
||
|
* All accesses to *_received flags and fan_* arrays are performed with
|
||
|
* wq.lock held.
|
||
|
*/
|
||
|
wait_queue_head_t wq;
|
||
|
/*
|
||
|
* mutex is used to:
|
||
|
* 1) Prevent concurrent conflicting changes to update interval and pwm
|
||
|
* values (after sending an output hid report, the corresponding field
|
||
|
* in drvdata must be updated, and only then new output reports can be
|
||
|
* sent).
|
||
|
* 2) Synchronize access to output_buffer (well, the buffer is here,
|
||
|
* because synchronization is necessary anyway - so why not get rid of
|
||
|
* a kmalloc?).
|
||
|
*/
|
||
|
struct mutex mutex;
|
||
|
long update_interval;
|
||
|
u8 output_buffer[OUTPUT_REPORT_SIZE];
|
||
|
};
|
||
|
|
||
|
static long scale_pwm_value(long val, long orig_max, long new_max)
|
||
|
{
|
||
|
if (val <= 0)
|
||
|
return 0;
|
||
|
|
||
|
/*
|
||
|
* Positive values should not become zero: 0 completely turns off the
|
||
|
* fan.
|
||
|
*/
|
||
|
return max(1L, DIV_ROUND_CLOSEST(min(val, orig_max) * new_max, orig_max));
|
||
|
}
|
||
|
|
||
|
static void handle_fan_config_report(struct drvdata *drvdata, void *data, int size)
|
||
|
{
|
||
|
struct fan_config_report *report = data;
|
||
|
int i;
|
||
|
|
||
|
if (size < sizeof(struct fan_config_report))
|
||
|
return;
|
||
|
|
||
|
if (report->magic != 0x03)
|
||
|
return;
|
||
|
|
||
|
spin_lock(&drvdata->wq.lock);
|
||
|
|
||
|
for (i = 0; i < FAN_CHANNELS; i++)
|
||
|
drvdata->fan_type[i] = report->fan_type[i];
|
||
|
|
||
|
drvdata->fan_config_received = true;
|
||
|
wake_up_all_locked(&drvdata->wq);
|
||
|
spin_unlock(&drvdata->wq.lock);
|
||
|
}
|
||
|
|
||
|
static void handle_fan_status_report(struct drvdata *drvdata, void *data, int size)
|
||
|
{
|
||
|
struct fan_status_report *report = data;
|
||
|
int i;
|
||
|
|
||
|
if (size < sizeof(struct fan_status_report))
|
||
|
return;
|
||
|
|
||
|
spin_lock(&drvdata->wq.lock);
|
||
|
|
||
|
/*
|
||
|
* The device sends INPUT_REPORT_ID_FAN_CONFIG = 0x61 report in response
|
||
|
* to "detect fans" command. Only accept other data after getting 0x61,
|
||
|
* to make sure that fan detection is complete. In particular, fan
|
||
|
* detection resets pwm values.
|
||
|
*/
|
||
|
if (!drvdata->fan_config_received) {
|
||
|
spin_unlock(&drvdata->wq.lock);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < FAN_CHANNELS; i++) {
|
||
|
if (drvdata->fan_type[i] == report->fan_type[i])
|
||
|
continue;
|
||
|
|
||
|
/*
|
||
|
* This should not happen (if my expectations about the device
|
||
|
* are correct).
|
||
|
*
|
||
|
* Even if the userspace sends fan detect command through
|
||
|
* hidraw, fan config report should arrive first.
|
||
|
*/
|
||
|
hid_warn_once(drvdata->hid,
|
||
|
"Fan %d type changed unexpectedly from %d to %d",
|
||
|
i, drvdata->fan_type[i], report->fan_type[i]);
|
||
|
drvdata->fan_type[i] = report->fan_type[i];
|
||
|
}
|
||
|
|
||
|
switch (report->type) {
|
||
|
case FAN_STATUS_REPORT_SPEED:
|
||
|
for (i = 0; i < FAN_CHANNELS; i++) {
|
||
|
drvdata->fan_rpm[i] =
|
||
|
get_unaligned_le16(&report->fan_speed.fan_rpm[i]);
|
||
|
drvdata->fan_duty_percent[i] =
|
||
|
report->fan_speed.duty_percent[i];
|
||
|
}
|
||
|
|
||
|
drvdata->pwm_status_received = true;
|
||
|
wake_up_all_locked(&drvdata->wq);
|
||
|
break;
|
||
|
|
||
|
case FAN_STATUS_REPORT_VOLTAGE:
|
||
|
for (i = 0; i < FAN_CHANNELS; i++) {
|
||
|
drvdata->fan_in[i] =
|
||
|
get_unaligned_le16(&report->fan_voltage.fan_in[i]);
|
||
|
drvdata->fan_curr[i] =
|
||
|
get_unaligned_le16(&report->fan_voltage.fan_current[i]);
|
||
|
}
|
||
|
|
||
|
drvdata->voltage_status_received = true;
|
||
|
wake_up_all_locked(&drvdata->wq);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
spin_unlock(&drvdata->wq.lock);
|
||
|
}
|
||
|
|
||
|
static umode_t nzxt_smart2_hwmon_is_visible(const void *data,
|
||
|
enum hwmon_sensor_types type,
|
||
|
u32 attr, int channel)
|
||
|
{
|
||
|
switch (type) {
|
||
|
case hwmon_pwm:
|
||
|
switch (attr) {
|
||
|
case hwmon_pwm_input:
|
||
|
case hwmon_pwm_enable:
|
||
|
return 0644;
|
||
|
|
||
|
default:
|
||
|
return 0444;
|
||
|
}
|
||
|
|
||
|
case hwmon_chip:
|
||
|
switch (attr) {
|
||
|
case hwmon_chip_update_interval:
|
||
|
return 0644;
|
||
|
|
||
|
default:
|
||
|
return 0444;
|
||
|
}
|
||
|
|
||
|
default:
|
||
|
return 0444;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static int nzxt_smart2_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
|
||
|
u32 attr, int channel, long *val)
|
||
|
{
|
||
|
struct drvdata *drvdata = dev_get_drvdata(dev);
|
||
|
int res = -EINVAL;
|
||
|
|
||
|
if (type == hwmon_chip) {
|
||
|
switch (attr) {
|
||
|
case hwmon_chip_update_interval:
|
||
|
*val = drvdata->update_interval;
|
||
|
return 0;
|
||
|
|
||
|
default:
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
spin_lock_irq(&drvdata->wq.lock);
|
||
|
|
||
|
switch (type) {
|
||
|
case hwmon_pwm:
|
||
|
/*
|
||
|
* fancontrol:
|
||
|
* 1) remembers pwm* values when it starts
|
||
|
* 2) needs pwm*_enable to be 1 on controlled fans
|
||
|
* So make sure we have correct data before allowing pwm* reads.
|
||
|
* Returning errors for pwm of fan speed read can even cause
|
||
|
* fancontrol to shut down. So the wait is unavoidable.
|
||
|
*/
|
||
|
switch (attr) {
|
||
|
case hwmon_pwm_enable:
|
||
|
res = wait_event_interruptible_locked_irq(drvdata->wq,
|
||
|
drvdata->fan_config_received);
|
||
|
if (res)
|
||
|
goto unlock;
|
||
|
|
||
|
*val = drvdata->fan_type[channel] != FAN_TYPE_NONE;
|
||
|
break;
|
||
|
|
||
|
case hwmon_pwm_mode:
|
||
|
res = wait_event_interruptible_locked_irq(drvdata->wq,
|
||
|
drvdata->fan_config_received);
|
||
|
if (res)
|
||
|
goto unlock;
|
||
|
|
||
|
*val = drvdata->fan_type[channel] == FAN_TYPE_PWM;
|
||
|
break;
|
||
|
|
||
|
case hwmon_pwm_input:
|
||
|
res = wait_event_interruptible_locked_irq(drvdata->wq,
|
||
|
drvdata->pwm_status_received);
|
||
|
if (res)
|
||
|
goto unlock;
|
||
|
|
||
|
*val = scale_pwm_value(drvdata->fan_duty_percent[channel],
|
||
|
100, 255);
|
||
|
break;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case hwmon_fan:
|
||
|
/*
|
||
|
* It's not strictly necessary to wait for *_received in the
|
||
|
* remaining cases (fancontrol doesn't care about them). But I'm
|
||
|
* doing it to have consistent behavior.
|
||
|
*/
|
||
|
if (attr == hwmon_fan_input) {
|
||
|
res = wait_event_interruptible_locked_irq(drvdata->wq,
|
||
|
drvdata->pwm_status_received);
|
||
|
if (res)
|
||
|
goto unlock;
|
||
|
|
||
|
*val = drvdata->fan_rpm[channel];
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case hwmon_in:
|
||
|
if (attr == hwmon_in_input) {
|
||
|
res = wait_event_interruptible_locked_irq(drvdata->wq,
|
||
|
drvdata->voltage_status_received);
|
||
|
if (res)
|
||
|
goto unlock;
|
||
|
|
||
|
*val = drvdata->fan_in[channel];
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case hwmon_curr:
|
||
|
if (attr == hwmon_curr_input) {
|
||
|
res = wait_event_interruptible_locked_irq(drvdata->wq,
|
||
|
drvdata->voltage_status_received);
|
||
|
if (res)
|
||
|
goto unlock;
|
||
|
|
||
|
*val = drvdata->fan_curr[channel];
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
unlock:
|
||
|
spin_unlock_irq(&drvdata->wq.lock);
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
static int send_output_report(struct drvdata *drvdata, const void *data,
|
||
|
size_t data_size)
|
||
|
{
|
||
|
int ret;
|
||
|
|
||
|
if (data_size > sizeof(drvdata->output_buffer))
|
||
|
return -EINVAL;
|
||
|
|
||
|
memcpy(drvdata->output_buffer, data, data_size);
|
||
|
|
||
|
if (data_size < sizeof(drvdata->output_buffer))
|
||
|
memset(drvdata->output_buffer + data_size, 0,
|
||
|
sizeof(drvdata->output_buffer) - data_size);
|
||
|
|
||
|
ret = hid_hw_output_report(drvdata->hid, drvdata->output_buffer,
|
||
|
sizeof(drvdata->output_buffer));
|
||
|
return ret < 0 ? ret : 0;
|
||
|
}
|
||
|
|
||
|
static int set_pwm(struct drvdata *drvdata, int channel, long val)
|
||
|
{
|
||
|
int ret;
|
||
|
u8 duty_percent = scale_pwm_value(val, 255, 100);
|
||
|
|
||
|
struct set_fan_speed_report report = {
|
||
|
.report_id = OUTPUT_REPORT_ID_SET_FAN_SPEED,
|
||
|
.magic = 1,
|
||
|
.channel_bit_mask = 1 << channel
|
||
|
};
|
||
|
|
||
|
ret = mutex_lock_interruptible(&drvdata->mutex);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
report.duty_percent[channel] = duty_percent;
|
||
|
ret = send_output_report(drvdata, &report, sizeof(report));
|
||
|
if (ret)
|
||
|
goto unlock;
|
||
|
|
||
|
/*
|
||
|
* pwmconfig and fancontrol scripts expect pwm writes to take effect
|
||
|
* immediately (i. e. read from pwm* sysfs should return the value
|
||
|
* written into it). The device seems to always accept pwm values - even
|
||
|
* when there is no fan connected - so update pwm status without waiting
|
||
|
* for a report, to make pwmconfig and fancontrol happy. Worst case -
|
||
|
* if the device didn't accept new pwm value for some reason (never seen
|
||
|
* this in practice) - it will be reported incorrectly only until next
|
||
|
* update. This avoids "fan stuck" messages from pwmconfig, and
|
||
|
* fancontrol setting fan speed to 100% during shutdown.
|
||
|
*/
|
||
|
spin_lock_bh(&drvdata->wq.lock);
|
||
|
drvdata->fan_duty_percent[channel] = duty_percent;
|
||
|
spin_unlock_bh(&drvdata->wq.lock);
|
||
|
|
||
|
unlock:
|
||
|
mutex_unlock(&drvdata->mutex);
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Workaround for fancontrol/pwmconfig trying to write to pwm*_enable even if it
|
||
|
* already is 1 and read-only. Otherwise, fancontrol won't restore pwm on
|
||
|
* shutdown properly.
|
||
|
*/
|
||
|
static int set_pwm_enable(struct drvdata *drvdata, int channel, long val)
|
||
|
{
|
||
|
long expected_val;
|
||
|
int res;
|
||
|
|
||
|
spin_lock_irq(&drvdata->wq.lock);
|
||
|
|
||
|
res = wait_event_interruptible_locked_irq(drvdata->wq,
|
||
|
drvdata->fan_config_received);
|
||
|
if (res) {
|
||
|
spin_unlock_irq(&drvdata->wq.lock);
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
expected_val = drvdata->fan_type[channel] != FAN_TYPE_NONE;
|
||
|
|
||
|
spin_unlock_irq(&drvdata->wq.lock);
|
||
|
|
||
|
return (val == expected_val) ? 0 : -EOPNOTSUPP;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Control byte | Actual update interval in seconds
|
||
|
* 0xff | 65.5
|
||
|
* 0xf7 | 63.46
|
||
|
* 0x7f | 32.74
|
||
|
* 0x3f | 16.36
|
||
|
* 0x1f | 8.17
|
||
|
* 0x0f | 4.07
|
||
|
* 0x07 | 2.02
|
||
|
* 0x03 | 1.00
|
||
|
* 0x02 | 0.744
|
||
|
* 0x01 | 0.488
|
||
|
* 0x00 | 0.25
|
||
|
*/
|
||
|
static u8 update_interval_to_control_byte(long interval)
|
||
|
{
|
||
|
if (interval <= 250)
|
||
|
return 0;
|
||
|
|
||
|
return clamp_val(1 + DIV_ROUND_CLOSEST(interval - 488, 256), 0, 255);
|
||
|
}
|
||
|
|
||
|
static long control_byte_to_update_interval(u8 control_byte)
|
||
|
{
|
||
|
if (control_byte == 0)
|
||
|
return 250;
|
||
|
|
||
|
return 488 + (control_byte - 1) * 256;
|
||
|
}
|
||
|
|
||
|
static int set_update_interval(struct drvdata *drvdata, long val)
|
||
|
{
|
||
|
u8 control = update_interval_to_control_byte(val);
|
||
|
u8 report[] = {
|
||
|
OUTPUT_REPORT_ID_INIT_COMMAND,
|
||
|
INIT_COMMAND_SET_UPDATE_INTERVAL,
|
||
|
0x01,
|
||
|
0xe8,
|
||
|
control,
|
||
|
0x01,
|
||
|
0xe8,
|
||
|
control,
|
||
|
};
|
||
|
int ret;
|
||
|
|
||
|
ret = send_output_report(drvdata, report, sizeof(report));
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
drvdata->update_interval = control_byte_to_update_interval(control);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int init_device(struct drvdata *drvdata, long update_interval)
|
||
|
{
|
||
|
int ret;
|
||
|
static const u8 detect_fans_report[] = {
|
||
|
OUTPUT_REPORT_ID_INIT_COMMAND,
|
||
|
INIT_COMMAND_DETECT_FANS,
|
||
|
};
|
||
|
|
||
|
ret = send_output_report(drvdata, detect_fans_report,
|
||
|
sizeof(detect_fans_report));
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
return set_update_interval(drvdata, update_interval);
|
||
|
}
|
||
|
|
||
|
static int nzxt_smart2_hwmon_write(struct device *dev,
|
||
|
enum hwmon_sensor_types type, u32 attr,
|
||
|
int channel, long val)
|
||
|
{
|
||
|
struct drvdata *drvdata = dev_get_drvdata(dev);
|
||
|
int ret;
|
||
|
|
||
|
switch (type) {
|
||
|
case hwmon_pwm:
|
||
|
switch (attr) {
|
||
|
case hwmon_pwm_enable:
|
||
|
return set_pwm_enable(drvdata, channel, val);
|
||
|
|
||
|
case hwmon_pwm_input:
|
||
|
return set_pwm(drvdata, channel, val);
|
||
|
|
||
|
default:
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
case hwmon_chip:
|
||
|
switch (attr) {
|
||
|
case hwmon_chip_update_interval:
|
||
|
ret = mutex_lock_interruptible(&drvdata->mutex);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
ret = set_update_interval(drvdata, val);
|
||
|
|
||
|
mutex_unlock(&drvdata->mutex);
|
||
|
return ret;
|
||
|
|
||
|
default:
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
default:
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static int nzxt_smart2_hwmon_read_string(struct device *dev,
|
||
|
enum hwmon_sensor_types type, u32 attr,
|
||
|
int channel, const char **str)
|
||
|
{
|
||
|
switch (type) {
|
||
|
case hwmon_fan:
|
||
|
*str = fan_label[channel];
|
||
|
return 0;
|
||
|
case hwmon_curr:
|
||
|
*str = curr_label[channel];
|
||
|
return 0;
|
||
|
case hwmon_in:
|
||
|
*str = in_label[channel];
|
||
|
return 0;
|
||
|
default:
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static const struct hwmon_ops nzxt_smart2_hwmon_ops = {
|
||
|
.is_visible = nzxt_smart2_hwmon_is_visible,
|
||
|
.read = nzxt_smart2_hwmon_read,
|
||
|
.read_string = nzxt_smart2_hwmon_read_string,
|
||
|
.write = nzxt_smart2_hwmon_write,
|
||
|
};
|
||
|
|
||
|
static const struct hwmon_channel_info *nzxt_smart2_channel_info[] = {
|
||
|
HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT | HWMON_F_LABEL,
|
||
|
HWMON_F_INPUT | HWMON_F_LABEL,
|
||
|
HWMON_F_INPUT | HWMON_F_LABEL),
|
||
|
HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT | HWMON_PWM_MODE | HWMON_PWM_ENABLE,
|
||
|
HWMON_PWM_INPUT | HWMON_PWM_MODE | HWMON_PWM_ENABLE,
|
||
|
HWMON_PWM_INPUT | HWMON_PWM_MODE | HWMON_PWM_ENABLE),
|
||
|
HWMON_CHANNEL_INFO(in, HWMON_I_INPUT | HWMON_I_LABEL,
|
||
|
HWMON_I_INPUT | HWMON_I_LABEL,
|
||
|
HWMON_I_INPUT | HWMON_I_LABEL),
|
||
|
HWMON_CHANNEL_INFO(curr, HWMON_C_INPUT | HWMON_C_LABEL,
|
||
|
HWMON_C_INPUT | HWMON_C_LABEL,
|
||
|
HWMON_C_INPUT | HWMON_C_LABEL),
|
||
|
HWMON_CHANNEL_INFO(chip, HWMON_C_UPDATE_INTERVAL),
|
||
|
NULL
|
||
|
};
|
||
|
|
||
|
static const struct hwmon_chip_info nzxt_smart2_chip_info = {
|
||
|
.ops = &nzxt_smart2_hwmon_ops,
|
||
|
.info = nzxt_smart2_channel_info,
|
||
|
};
|
||
|
|
||
|
static int nzxt_smart2_hid_raw_event(struct hid_device *hdev,
|
||
|
struct hid_report *report, u8 *data, int size)
|
||
|
{
|
||
|
struct drvdata *drvdata = hid_get_drvdata(hdev);
|
||
|
u8 report_id = *data;
|
||
|
|
||
|
switch (report_id) {
|
||
|
case INPUT_REPORT_ID_FAN_CONFIG:
|
||
|
handle_fan_config_report(drvdata, data, size);
|
||
|
break;
|
||
|
|
||
|
case INPUT_REPORT_ID_FAN_STATUS:
|
||
|
handle_fan_status_report(drvdata, data, size);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int __maybe_unused nzxt_smart2_hid_reset_resume(struct hid_device *hdev)
|
||
|
{
|
||
|
struct drvdata *drvdata = hid_get_drvdata(hdev);
|
||
|
|
||
|
/*
|
||
|
* Userspace is still frozen (so no concurrent sysfs attribute access
|
||
|
* is possible), but raw_event can already be called concurrently.
|
||
|
*/
|
||
|
spin_lock_bh(&drvdata->wq.lock);
|
||
|
drvdata->fan_config_received = false;
|
||
|
drvdata->pwm_status_received = false;
|
||
|
drvdata->voltage_status_received = false;
|
||
|
spin_unlock_bh(&drvdata->wq.lock);
|
||
|
|
||
|
return init_device(drvdata, drvdata->update_interval);
|
||
|
}
|
||
|
|
||
|
static int nzxt_smart2_hid_probe(struct hid_device *hdev,
|
||
|
const struct hid_device_id *id)
|
||
|
{
|
||
|
struct drvdata *drvdata;
|
||
|
int ret;
|
||
|
|
||
|
drvdata = devm_kzalloc(&hdev->dev, sizeof(struct drvdata), GFP_KERNEL);
|
||
|
if (!drvdata)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
drvdata->hid = hdev;
|
||
|
hid_set_drvdata(hdev, drvdata);
|
||
|
|
||
|
init_waitqueue_head(&drvdata->wq);
|
||
|
|
||
|
mutex_init(&drvdata->mutex);
|
||
|
devm_add_action(&hdev->dev, (void (*)(void *))mutex_destroy,
|
||
|
&drvdata->mutex);
|
||
|
|
||
|
ret = hid_parse(hdev);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
ret = hid_hw_open(hdev);
|
||
|
if (ret)
|
||
|
goto out_hw_stop;
|
||
|
|
||
|
hid_device_io_start(hdev);
|
||
|
|
||
|
init_device(drvdata, UPDATE_INTERVAL_DEFAULT_MS);
|
||
|
|
||
|
drvdata->hwmon =
|
||
|
hwmon_device_register_with_info(&hdev->dev, "nzxtsmart2", drvdata,
|
||
|
&nzxt_smart2_chip_info, NULL);
|
||
|
if (IS_ERR(drvdata->hwmon)) {
|
||
|
ret = PTR_ERR(drvdata->hwmon);
|
||
|
goto out_hw_close;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
|
||
|
out_hw_close:
|
||
|
hid_hw_close(hdev);
|
||
|
|
||
|
out_hw_stop:
|
||
|
hid_hw_stop(hdev);
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static void nzxt_smart2_hid_remove(struct hid_device *hdev)
|
||
|
{
|
||
|
struct drvdata *drvdata = hid_get_drvdata(hdev);
|
||
|
|
||
|
hwmon_device_unregister(drvdata->hwmon);
|
||
|
|
||
|
hid_hw_close(hdev);
|
||
|
hid_hw_stop(hdev);
|
||
|
}
|
||
|
|
||
|
static const struct hid_device_id nzxt_smart2_hid_id_table[] = {
|
||
|
{ HID_USB_DEVICE(0x1e71, 0x2006) }, /* NZXT Smart Device V2 */
|
||
|
{ HID_USB_DEVICE(0x1e71, 0x200d) }, /* NZXT Smart Device V2 */
|
||
|
{ HID_USB_DEVICE(0x1e71, 0x200f) }, /* NZXT Smart Device V2 */
|
||
|
{ HID_USB_DEVICE(0x1e71, 0x2009) }, /* NZXT RGB & Fan Controller */
|
||
|
{ HID_USB_DEVICE(0x1e71, 0x200e) }, /* NZXT RGB & Fan Controller */
|
||
|
{ HID_USB_DEVICE(0x1e71, 0x2010) }, /* NZXT RGB & Fan Controller */
|
||
|
{},
|
||
|
};
|
||
|
|
||
|
static struct hid_driver nzxt_smart2_hid_driver = {
|
||
|
.name = "nzxt-smart2",
|
||
|
.id_table = nzxt_smart2_hid_id_table,
|
||
|
.probe = nzxt_smart2_hid_probe,
|
||
|
.remove = nzxt_smart2_hid_remove,
|
||
|
.raw_event = nzxt_smart2_hid_raw_event,
|
||
|
#ifdef CONFIG_PM
|
||
|
.reset_resume = nzxt_smart2_hid_reset_resume,
|
||
|
#endif
|
||
|
};
|
||
|
|
||
|
static int __init nzxt_smart2_init(void)
|
||
|
{
|
||
|
return hid_register_driver(&nzxt_smart2_hid_driver);
|
||
|
}
|
||
|
|
||
|
static void __exit nzxt_smart2_exit(void)
|
||
|
{
|
||
|
hid_unregister_driver(&nzxt_smart2_hid_driver);
|
||
|
}
|
||
|
|
||
|
MODULE_DEVICE_TABLE(hid, nzxt_smart2_hid_id_table);
|
||
|
MODULE_AUTHOR("Aleksandr Mezin <mezin.alexander@gmail.com>");
|
||
|
MODULE_DESCRIPTION("Driver for NZXT RGB & Fan Controller/Smart Device V2");
|
||
|
MODULE_LICENSE("GPL");
|
||
|
|
||
|
/*
|
||
|
* With module_init()/module_hid_driver() and the driver built into the kernel:
|
||
|
*
|
||
|
* Driver 'nzxt_smart2' was unable to register with bus_type 'hid' because the
|
||
|
* bus was not initialized.
|
||
|
*/
|
||
|
late_initcall(nzxt_smart2_init);
|
||
|
module_exit(nzxt_smart2_exit);
|