diff --git a/CMakeLists.txt b/CMakeLists.txt index 03e1fc5..537a63a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() @@ -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. # @@ -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}) diff --git a/README.md b/README.md index 934763f..5f6d678 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/thinkfan.yaml b/examples/thinkfan.yaml index 9138d6e..38829f6 100644 --- a/examples/thinkfan.yaml +++ b/examples/thinkfan.yaml @@ -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 diff --git a/src/sensors.cpp b/src/sensors.cpp index c59ab83..98fbe5e 100644 --- a/src/sensors.cpp +++ b/src/sensors.cpp @@ -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 feature_names, + bool optional, std::vector 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 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 */ + } diff --git a/src/sensors.h b/src/sensors.h index 0f33476..72eabf4 100644 --- a/src/sensors.h +++ b/src/sensors.h @@ -32,6 +32,13 @@ #include #endif /* USE_NVML */ +#ifdef USE_LM_SENSORS +#include +#include +#include +#include +#endif /* USE_LM_SENSORS */ + namespace thinkfan { class ExpectedError; @@ -141,5 +148,44 @@ private: #endif /* USE_NVML */ +#ifdef USE_LM_SENSORS + +class LMSensorsDriver : public SensorDriver { +public: + LMSensorsDriver(string chip_name, std::vector feature_names, + bool optional, std::vector 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 feature_names_; + std::vector features_; + std::vector sub_features_; + + static std::once_flag lm_sensors_once_init_; +}; + +#endif /* USE_LM_SENSORS */ + } diff --git a/src/yamlconfig.cpp b/src/yamlconfig.cpp index dc5e18a..1ad6591 100644 --- a/src/yamlconfig.cpp +++ b/src/yamlconfig.cpp @@ -29,6 +29,10 @@ static const string kw_nvidia("nvml"); #ifdef USE_ATASMART static const string kw_atasmart("atasmart"); #endif +#ifdef USE_LM_SENSORS +static const string kw_chip("chip"); +static const string kw_ids("ids"); +#endif static const string kw_speed("speed"); static const string kw_upper("upper_limit"); static const string kw_lower("lower_limit"); @@ -391,6 +395,45 @@ struct convert> { #endif //USE_ATASMART +#ifdef USE_LM_SENSORS + +template<> +struct convert> { + static bool decode(const Node &node, wtf_ptr &sensor) + { + if (!node[kw_chip]) + return false; + + if (!node[kw_ids]) { + throw YamlError(get_mark_compat(node[kw_ids]), "No temperature inputs were specified."); + } + + string chip_name = node[kw_chip].as(); + vector feature_names = node[kw_ids].as>(); + + bool optional = node[kw_optional] ? node[kw_optional].as() : false; + + vector correction; + if (node[kw_correction]) { + correction = node[kw_correction].as>(); + } + if (!correction.empty()) { + if (correction.size() != feature_names.size()) { + throw YamlError( + get_mark_compat(node[kw_ids]), + MSG_CONF_CORRECTION_LEN(chip_name, correction.size(), feature_names.size())); + } + } + + sensor = make_wtf( + chip_name, feature_names, optional, correction); + return true; + } +}; + +#endif // USE_LM_SENSORS + + template<> struct convert>> { @@ -419,6 +462,12 @@ struct convert>> { sensors.push_back(std::move(tmp)); } #endif //USE_ATASMART +#ifdef USE_LM_SENSORS + else if ((*it)[kw_chip]) { + wtf_ptr tmp = it->as>(); + sensors.push_back(std::move(tmp)); + } +#endif // USE_LM_SENSORS else throw YamlError(get_mark_compat(*it), "Invalid sensor entry"); }