diff --git a/FanMeter.c b/FanMeter.c new file mode 100644 index 0000000000..2ae84718dd --- /dev/null +++ b/FanMeter.c @@ -0,0 +1,44 @@ +/* +htop - FanMeter.c +(C) 2026 Murad Karammaev +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "config.h" // IWYU pragma: keep + +#include "FanMeter.h" + +#include "CRT.h" +#include "Platform.h" +#include "XUtils.h" + + +static const int FanMeter_attributes[] = { + METER_VALUE +}; + +static void FanMeter_updateValues(Meter* this) { + int fanspeed = Platform_getFanSpeed(); + if (fanspeed < 0) { + xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "N/A"); + } else { + xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "%d rpm", fanspeed); + } +} + +const MeterClass FanMeter_class = { + .super = { + .extends = Class(Meter), + .delete = Meter_delete + }, + .updateValues = FanMeter_updateValues, + .defaultMode = TEXT_METERMODE, + .supportedModes = (1 << TEXT_METERMODE) | (1 << LED_METERMODE), + .maxItems = 0, + .total = 0.0, + .attributes = FanMeter_attributes, + .name = "Fan", + .uiName = "Fan speed", + .caption = "Fan speed: ", +}; diff --git a/FanMeter.h b/FanMeter.h new file mode 100644 index 0000000000..603072524d --- /dev/null +++ b/FanMeter.h @@ -0,0 +1,15 @@ +#ifndef HEADER_FanMeter +#define HEADER_FanMeter +/* +htop - FanMeter.h +(C) 2026 Murad Karammaev +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "Meter.h" + + +extern const MeterClass FanMeter_class; + +#endif diff --git a/Makefile.am b/Makefile.am index 115a07e891..fe4c500e00 100644 --- a/Makefile.am +++ b/Makefile.am @@ -114,6 +114,7 @@ myhtopheaders = \ DynamicMeter.h \ DynamicScreen.h \ EnvScreen.h \ + FanMeter.h \ FileDescriptorMeter.h \ FunctionBar.h \ GPUMeter.h \ @@ -213,6 +214,7 @@ linux_platform_headers = \ zfs/ZfsCompressedArcMeter.h linux_platform_sources = \ + FanMeter.c \ GPUMeter.c \ generic/gettime.c \ generic/hostname.c \ diff --git a/linux/Platform.c b/linux/Platform.c index dd4121e8e6..a3e60c2e0d 100644 --- a/linux/Platform.c +++ b/linux/Platform.c @@ -29,6 +29,7 @@ in the source distribution for its full text. #include "DiskIOMeter.h" #include "FileDescriptorMeter.h" #include "GPUMeter.h" +#include "FanMeter.h" #include "HostnameMeter.h" #include "HugePageMeter.h" #include "LoadAverageMeter.h" @@ -280,6 +281,7 @@ const MeterClass* const Platform_meterTypes[] = { &OpenRCUserMeter_class, &FileDescriptorMeter_class, &GPUMeter_class, + &FanMeter_class, NULL }; @@ -441,6 +443,88 @@ void Platform_setGPUValues(Meter* this, double* totalUsage, unsigned long long* this->values[residueIndex] = residuePercentage; } +static int Platform_getFanSpeedFromHwmonDevice(openat_arg_t parent_dfd, const char *hwmon_device) { + DIR* dirp; + +#ifdef HAVE_OPENAT + openat_arg_t dfd = Compat_openat(parent_dfd, hwmon_device, O_RDONLY | O_CLOEXEC | O_DIRECTORY); + if (dfd == -1) + return -1; + + dirp = fdopendir(dfd); + if (!dirp) { + close(dfd); + return -1; + } +#else + char dfd[4096]; + if (xSnprintf(dfd, sizeof(dfd), "%s/%s", parent_dfd, hwmon_device) < 0) + return -1; + + dirp = opendir(dfd); + if (!dirp) + return -1; +#endif + + struct dirent* dir; + while ((dir = readdir(dirp)) != NULL) { + int fan_num = 0, scan_len = 0; + if (sscanf(dir->d_name, "fan%d%n", &fan_num, &scan_len) != 1 || + !String_eq(&dir->d_name[scan_len], "_input")) + continue; + + char buffer[32]; + ssize_t ret = Compat_readfileat(dfd, dir->d_name, buffer, sizeof(buffer)); + if (ret <= 0 || (size_t)ret > sizeof(buffer)) + continue; + + int fan_speed; + if (sscanf(buffer, "%d", &fan_speed) != 1) + continue; + + closedir(dirp); + return fan_speed; + } + + closedir(dirp); + return -1; +} + +/* Traverse hwmon sensors, find first fan, read and return its speed. */ +int Platform_getFanSpeed(void) { + DIR* dirp; + openat_arg_t dfd; + + dirp = opendir(HWMONDIR); + if (!dirp) + return -1; + +#ifdef HAVE_OPENAT + dfd = dirfd(dirp); + if (dfd == -1) { + closedir(dirp); + return -1; + } +#else + dfd = HWMONDIR; +#endif + + struct dirent* dir; + while ((dir = readdir(dirp)) != NULL) { + if (String_eq(dir->d_name, ".") || String_eq(dir->d_name, "..")) + continue; + + int fan_speed = Platform_getFanSpeedFromHwmonDevice(dfd, dir->d_name); + if (fan_speed >= 0) { + closedir(dirp); + return fan_speed; + } + } + + closedir(dirp); + return -1; +} + void Platform_setMemoryValues(Meter* this) { const Machine* host = this->host; const LinuxMachine* lhost = (const LinuxMachine*) host; diff --git a/linux/Platform.h b/linux/Platform.h index d918563429..8c9368703c 100644 --- a/linux/Platform.h +++ b/linux/Platform.h @@ -38,6 +38,8 @@ in the source distribution for its full text. #define PATH_MAX 4096 #endif +#define HWMONDIR "/sys/class/hwmon" + extern const ScreenDefaults Platform_defaultScreens[]; @@ -70,6 +72,8 @@ double Platform_setCPUValues(Meter* this, unsigned int cpu); void Platform_setGPUValues(Meter* this, double* totalUsage, unsigned long long* totalGPUTimeDiff); +int Platform_getFanSpeed(void); + void Platform_setMemoryValues(Meter* this); void Platform_setSwapValues(Meter* this);