Skip to content

Commit

Permalink
Added support for LM Sensors.
Browse files Browse the repository at this point in the history
This uses `libsensors` as a source of temperature values, avoiding many issues with the current hwmon driver class.
  • Loading branch information
koutheir authored and vmatare committed Oct 26, 2021
1 parent ab466dd commit 8a844b9
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 0 deletions.
20 changes: 20 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ endif()

pkg_check_modules(ATASMART "libatasmart")

find_library(LM_SENSORS_LIB NAMES "libsensors.so" "libsensors.so.5")
find_path(LM_SENSORS_INC NAMES "sensors/sensors.h")

if(SYSTEMD_FOUND)
set(PID_FILE "/run/thinkfan.pid")
else()
Expand All @@ -39,6 +42,11 @@ option(USE_ATASMART "Enable reading temperatures from HDDs via S.M.A.R.T" OFF)
#
option(USE_NVML "Get temperatures directly from nVidia GPUs via their proprietary NVML API" ON)

#
# Defaults to ON.
#
option(USE_LM_SENSORS "Get temperatures from LM sensors" ON)

#
# The shiny new YAML config parser. Depends on yaml-cpp.
#
Expand Down Expand Up @@ -100,6 +108,18 @@ if(USE_NVML)
target_link_libraries(thinkfan PRIVATE dl)
endif(USE_NVML)

if(USE_LM_SENSORS)
if(LM_SENSORS_LIB MATCHES "LM_SENSORS_LIB-NOTFOUND")
message(FATAL_ERROR "USE_LM_SENSORS enabled but libsensors not found. Please install libsensors-dev!")
elseif(LM_SENSORS_INC MATCHES "LM_SENSORS_INC-NOTFOUND")
message(FATAL_ERROR "USE_LM_SENSORS enabled but sensors/sensors.h not found. Please install libsensors-dev!")
else()
target_compile_definitions(thinkfan PRIVATE -DUSE_LM_SENSORS)
target_include_directories(thinkfan PRIVATE ${LM_SENSORS_INC})
target_link_libraries(thinkfan PRIVATE ${LM_SENSORS_LIB})
endif()
endif(USE_LM_SENSORS)

if(USE_YAML)
target_compile_definitions(thinkfan PRIVATE -DUSE_YAML)
target_include_directories(thinkfan PRIVATE ${YAML_CPP_INCLUDE_DIRS})
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ To compile thinkfan, you will need to have the following things installed:
this only when you really need it, since libatasmart is unreasonably
CPU-intensive.

`USE_LM_SENSORS:BOOL` (default: `ON`)
Use LM sensors to read temperatures directly from Linux drivers.
The `libsensors` library needs to be installed for this feature, probably
with required headers and development files (e.g., `libsensors-dev`).

`USE_YAML:BOOL` (default: `ON`)
Support config file in the new, more flexible YAML format. The old
config format will be deprecated after the thinkfan 1.0 release. New
Expand Down
28 changes: 28 additions & 0 deletions examples/thinkfan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,34 @@
# sensor.

sensors:
# LM Sensors
# ==========
# Temperatures can be read directly from Linux drivers through the LM sensors.
#
# To configure this, install "lm-sensors" and "libsensors", then
# run "sensors-detect", then run "sensors".
# To build thinkfan from sources, you'll also need to install "libsensors-dev"
# or equivalent package for your distribution.
#
# For example, the following output of "sensors":
# ...
# thinkpad-isa-0000
# Adapter: ISA adapter
# fan1: 2618 RPM
# fan2: 2553 RPM
# CPU: +63.0 C
# GPU 1: +55.0 C
# temp3: +68.0 C
# temp4: +0.0 C
# temp5: +60.0 C
# temp6: +64.0 C
# temp7: +67.0 C
# temp8: +0.0 C
# ...
# would result in the following configuration:
- chip: thinkpad-isa-0000
ids: [ CPU, "GPU 1", temp3, temp4, temp5, temp6, temp7, temp8 ]

# hwmon: Full path to a temperature file (single sensor).
# =======================================================
# Disadvantage is that the index in "hwmon0" depends on the load order of
Expand Down
183 changes: 183 additions & 0 deletions src/sensors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -341,4 +341,187 @@ void NvmlSensorDriver::read_temps_(TemperatureState &global_temps) const
#endif /* USE_NVML */


#ifdef USE_LM_SENSORS

/*----------------------------------------------------------------------------
| LMSensorsDriver: Driver for sensors provided by LM sensors's `libsensors`. |
----------------------------------------------------------------------------*/

// Closest integer value to zero Kelvin.
static const int MIN_CELSIUS_TEMP = -273;

std::once_flag LMSensorsDriver::lm_sensors_once_init_;

LMSensorsDriver::LMSensorsDriver(
string chip_name, std::vector<string> feature_names,
bool optional, std::vector<int> correction)
: SensorDriver(optional),
chip_name_(chip_name), chip_(nullptr), feature_names_(feature_names)
{
LMSensorsDriver::ensure_lm_sensors_is_initialized();
chip_ = LMSensorsDriver::find_chip_by_name(chip_name_);
path_ = chip_->path;

size_t len = feature_names_.size();
for (size_t i = 0; i < len; ++i) {
const string& feature_name = feature_names_[i];

auto feature = LMSensorsDriver::find_feature_by_name(
*chip_, chip_name_, feature_name);
if (!feature) {
throw SystemError("LM sensors chip '" + chip_name
+ "' does not have the feature '" + feature_name + "'");
}
features_.push_back(feature);

auto sub_feature = ::sensors_get_subfeature(
chip_, feature, ::SENSORS_SUBFEATURE_TEMP_INPUT);
if (!sub_feature) {
throw SystemError("LM sensors feature '" + feature_name
+ "' of the chip '" + chip_name_
+ "' does not have a temperature input sensor");
}
sub_features_.push_back(sub_feature);

log(TF_DBG) << "Initialized LM sensors temperature input of feature '"
+ feature_name + "' of chip '" + chip_name_ + "'." << flush;
}

if (correction.empty()) {
correction.resize(feature_names_.size(), 0);
}

set_num_temps(feature_names_.size());
set_correction(correction);
}

LMSensorsDriver::~LMSensorsDriver() noexcept(false) {}

void LMSensorsDriver::ensure_lm_sensors_is_initialized() {
int r = 0;
std::call_once(LMSensorsDriver::lm_sensors_once_init_,
LMSensorsDriver::initialize_lm_sensors, &r);
if (r != 0) {
const char *msg = ::sensors_strerror(r);
throw SystemError(string("Failed to initialize LM sensors driver: ") + msg);
}
}

void LMSensorsDriver::initialize_lm_sensors(int* result) {
::sensors_parse_error = LMSensorsDriver::parse_error_call_back;
::sensors_parse_error_wfn = LMSensorsDriver::parse_error_wfn_call_back;
::sensors_fatal_error = LMSensorsDriver::fatal_error_call_back;

if ((*result = ::sensors_init(nullptr)) == 0) {
atexit(::sensors_cleanup);
}

log(TF_DBG) << "Initialized LM sensors." << flush;
}

const ::sensors_chip_name* LMSensorsDriver::find_chip_by_name(
const string& chip_name)
{
int state = 0;
for (;;) {
auto chip = ::sensors_get_detected_chips(nullptr, &state);
if (!chip) break;

if (chip_name == LMSensorsDriver::get_chip_name(*chip)) return chip;
}

throw SystemError("LM sensors chip '" + chip_name + "' was not found");
}

string LMSensorsDriver::get_chip_name(const ::sensors_chip_name& chip) {
int len = sensors_snprintf_chip_name(nullptr, 0, &chip);
if (len < 0) {
const char *msg = ::sensors_strerror(len);
throw SystemError(string("Failed to get LM sensors chip name: ") + msg);
}

std::vector<char> buffer((size_t)(len + 1));
int r = sensors_snprintf_chip_name(buffer.data(), (size_t)(len + 1), &chip);
if (r < 0) {
const char *msg = ::sensors_strerror(r);
throw SystemError(string("Failed to get LM sensors chip name: ") + msg);
} else if (r >= (len + 1)) {
throw SystemError("LM sensors chip name is too long");
}
return string(buffer.data(), r);
}

const ::sensors_feature* LMSensorsDriver::find_feature_by_name(
const ::sensors_chip_name& chip, const string& chip_name,
const string& feature_name)
{
int state = 0;
for (;;) {
auto feature = ::sensors_get_features(&chip, &state);
if (!feature) break;

auto label = ::sensors_get_label(&chip, feature);
bool label_matches = (feature_name == label);
free(label);

if (label_matches) return feature;
}
return nullptr;
}

void LMSensorsDriver::parse_error_call_back(const char *err, int line_no) {
log(TF_ERR) << "LM sensors parsing error: " << err << " in line "
<< std::to_string(line_no);
}

void LMSensorsDriver::parse_error_wfn_call_back(
const char *err, const char *file_name, int line_no)
{
log(TF_ERR) << "LM sensors parsing error: " << err << " in file '"
<< file_name << "' at line " << std::to_string(line_no);
}

void LMSensorsDriver::fatal_error_call_back(const char *proc, const char *err) {
log(TF_ERR) << "LM sensors fatal error in " << proc << ": " << err;
exit(EXIT_FAILURE);
}

void LMSensorsDriver::read_temps_(TemperatureState &global_temps) const {
size_t len = sub_features_.size();
for (size_t i = 0; i < len; ++i) {
auto sub_feature = sub_features_[i];

double real_value = MIN_CELSIUS_TEMP;
int r = ::sensors_get_value(chip_, sub_feature->number, &real_value);

int integer_value;
if (r == 0) {
integer_value = int(real_value) + correction_[i];
} else {
// NOTICE:
// If this happens, then all remaining temperature sources reported
// by the current driver instance are skipped.
//
// Sources of temperatures that are not always available should be
// configured on their own "- chip" entry, and marked optional.
integer_value = MIN_CELSIUS_TEMP;

const char *msg = ::sensors_strerror(r);
throw SystemError(
string("temperature input value of feature '")
+ feature_names_[i] + "' of chip '" + chip_name_
+ "' is unavailable: " + msg);
}

if (integer_value < MIN_CELSIUS_TEMP) {
// Make sure the reported value is physically valid.
integer_value = MIN_CELSIUS_TEMP;
}

global_temps.add_temp(integer_value);
}
}

#endif /* USE_LM_SENSORS */

}
46 changes: 46 additions & 0 deletions src/sensors.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
#include <nvidia/gdk/nvml.h>
#endif /* USE_NVML */

#ifdef USE_LM_SENSORS
#include <sensors/sensors.h>
#include <sensors/error.h>
#include <atomic>
#include <mutex>
#endif /* USE_LM_SENSORS */

namespace thinkfan {

class ExpectedError;
Expand Down Expand Up @@ -141,5 +148,44 @@ class NvmlSensorDriver : public SensorDriver {
#endif /* USE_NVML */


#ifdef USE_LM_SENSORS

class LMSensorsDriver : public SensorDriver {
public:
LMSensorsDriver(string chip_name, std::vector<string> feature_names,
bool optional, std::vector<int> correction = {});
virtual ~LMSensorsDriver();

protected:
virtual void read_temps_(TemperatureState &global_temps) const override;

// LM sensors helpers.
static void ensure_lm_sensors_is_initialized();
static void initialize_lm_sensors(int* result);
static const ::sensors_chip_name* find_chip_by_name(
const string& chip_name);
static const ::sensors_feature* find_feature_by_name(
const ::sensors_chip_name& chip, const string& chip_name,
const string& feature_name);
static string get_chip_name(const ::sensors_chip_name& chip);

// LM sensors call backs.
static void parse_error_call_back(const char *err, int line_no);
static void parse_error_wfn_call_back(const char *err, const char *file_name, int line_no);
static void fatal_error_call_back(const char *proc, const char *err);

private:
const string chip_name_;
const ::sensors_chip_name* chip_;

const std::vector<string> feature_names_;
std::vector<const ::sensors_feature*> features_;
std::vector<const ::sensors_subfeature*> sub_features_;

static std::once_flag lm_sensors_once_init_;
};

#endif /* USE_LM_SENSORS */

}

Loading

0 comments on commit 8a844b9

Please sign in to comment.