convert fan script from bash to rust #1

Merged
c4181 merged 1 commit from new-script into main 2025-11-19 17:18:21 +00:00
5 changed files with 1567 additions and 0 deletions
Showing only changes of commit 168c13015f - Show all commits

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/target
config.toml
# Jetbrains
.idea
*.iml

1309
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

15
Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "server-fan-controller"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
confy = "0.6.1"
nvml-wrapper = "0.10.0"
rust-ipmi = "0.1.1"
sysinfo = "0.31.2"
serde = { version = "1.0.208", features = ["derive"] }
log4rs = "1.3.0"
log = "0.4.22"

9
log4rs.yaml Normal file
View file

@ -0,0 +1,9 @@
appenders:
stdout:
kind: console
# encoder:
# kind: json
root:
level: info
appenders:
- stdout

228
src/main.rs Normal file
View file

@ -0,0 +1,228 @@
use log::{debug, error, info, trace};
use nvml_wrapper::enum_wrappers::device::TemperatureSensor;
use nvml_wrapper::Nvml;
use rust_ipmi::{IPMIClient, IPMIClientError};
use serde::{Deserialize, Serialize};
use std::cmp::max;
use std::fmt::{Display, Formatter};
use std::path::Path;
use std::time::Duration;
use std::{env, thread};
use sysinfo::Components;
fn main() {
let args: Vec<String> = env::args().collect();
let config_path = if args.len() > 1 { Path::new(&args[1] ) } else { Path::new("config.toml") };
let cfg: AppConfig = confy::load_path(Path::new(config_path)).expect("Couldn't read config");
let sleep_duration = Duration::from_secs(cfg.run_interval);
match Path::new("log4rs.yaml").exists() {
true => log4rs::init_file("log4rs.yaml", Default::default()).unwrap(),
false => println!("No log4rs.yaml file found. Logging will not be enabled")
}
loop {
let max_core_temp = get_max_core_temp();
let cpu_speed = calculate_cpu_curve_speed(max_core_temp as i32);
let max_gpu_temp = if cfg.monitor_gpus { get_max_gpu_temp() } else { 0 };
let gpu_speed = calculate_gpu_curve_speed(max_gpu_temp);
let fan_speed = max(cpu_speed, gpu_speed);
info!("Setting fan speed to {}", fan_speed);
match set_fan_speed(fan_speed, &cfg) {
Ok(_) => {
debug!("Successfully set fan speed");
thread::sleep(sleep_duration);
},
Err(_) => {
thread::sleep(Duration::from_secs(3))
}
};
}
}
fn calculate_cpu_curve_speed(temp: i32) -> FanSpeed {
let fan_speed = match temp {
t if t <= 35 => {
trace!("Setting fan speeds to 4.3k & 3.0k RPM");
FanSpeed::VeryLow
}
t if t <= 45 => {
trace!("Setting fan speeds to 5.3k & 3.4k RPM");
FanSpeed::Low
}
t if t <= 55 => {
trace!("Setting fan speeds to 5.6k & 3.8k RPM");
FanSpeed::Moderate
}
t if t <= 65 => {
trace!("Setting fan speeds to 5.9k & 4.1k RPM");
FanSpeed::High
}
t if t <= 75 => {
trace!("Setting fan speeds to 6.2k & 4.3k RPM");
FanSpeed::VeryHigh
}
t if t <= 85 => {
trace!("Setting fan speeds to 6.6k & 4.6k RPM");
FanSpeed::BeyondVeryHigh
}
t if t <= 95 => {
trace!("Setting fan speeds to 7.5k & 5.0k RPM");
FanSpeed::OneStepFromHell
}
_ => {
trace!("Setting fan speeds to 8.0k & 5.5k RPM");
FanSpeed::MaxLoad
}
};
fan_speed
}
fn calculate_gpu_curve_speed(temp: u32) -> FanSpeed {
let fan_speed = match temp {
t if t <= 35 => {
FanSpeed::VeryLow
}
t if t <= 45 => {
FanSpeed::Low
}
t if t <= 55 => {
FanSpeed::Moderate
}
t if t <= 65 => {
FanSpeed::VeryHigh
}
t if t <= 75 => {
FanSpeed::BeyondVeryHigh
}
t if t <= 80 => {
FanSpeed::OneStepFromHell
}
t if t <= 85 => {
FanSpeed::MaxLoad
}
_ => {
FanSpeed::GpuHigh
}
};
fan_speed
}
fn set_fan_speed(speed: FanSpeed, cfg: &AppConfig) -> Result<bool, IPMIClientError> {
let mut client: IPMIClient = match IPMIClient::new(&cfg.ipmi_config.ip) {
Ok(v) => v,
Err(e) => {
error!("Failed to construct IPMI Client because: {e}");
return Err(e)
}
};
// establish a session with the BMC using the credentials specified
match client.establish_connection(&cfg.ipmi_config.user, &cfg.ipmi_config.password) {
Ok(_) => {}
Err(e) => {
error!("Failed to establish IPMI connection because: {e}");
return Err(e)
}
};
// send a command to the BMC using raw values
match client.send_raw_request(0x30, 0x30, Some(vec![0x02, 0xff, speed as u8])) {
Ok(_) => Ok(true),
Err(e) => {
error!("Failed to send IPMI request because: {e}");
Err(e)
}
}
}
fn get_max_core_temp() -> f32 {
let components = Components::new_with_refreshed_list();
let mut max_temp:f32 = 0.0;
for component in components.iter() {
if component.label().starts_with("coretemp Core") {
if component.temperature() > max_temp {
max_temp = component.temperature();
}
} else if component.label().contains("coretemp Package id 0") {
info!("CPU Package 0 is {}C", component.temperature())
} else if component.label().starts_with("coretemp Package id 1") {
info!("CPU Package 1 is {}C", component.temperature())
}
}
max_temp
}
fn get_max_gpu_temp() -> u32 {
let nvml = Nvml::init().unwrap();
let count = nvml.device_count().unwrap();
let mut max_temp:u32 = 0;
for index in 0..count {
let device = nvml.device_by_index(index).unwrap();
let name = device.name().unwrap();
let temp = device.temperature(TemperatureSensor::Gpu).unwrap();
if temp > max_temp {
max_temp = temp;
}
info!("GPU {} is {}C", name, temp);
}
max_temp
}
#[derive(Ord, PartialOrd, Eq, PartialEq, Debug)]
#[repr(u8)]
enum FanSpeed {
VeryLow = 24, // 0x18 in decimal
Low = 29, // 0x1d in decimal
Moderate = 31, // 0x1f in decimal
High = 32, // 0x20 in decimal
VeryHigh = 34, // 0x22 in decimal
BeyondVeryHigh = 36, // 0x24 in decimal
OneStepFromHell = 38, // 0x26 in decimal
MaxLoad = 43, // 0x2b in decimal
GpuHigh = 50,
}
impl Display for FanSpeed {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}%, {:?}", self.value(), self)
}
}
impl FanSpeed {
fn value(&self) -> u8 {
unsafe { *(self as *const Self as *const u8) }
}
fn as_hex(&self) -> String {
format!("{:#x}", self.value())
}
}
#[derive(Debug, Serialize, Deserialize)]
struct AppConfig {
monitor_gpus: bool,
run_interval: u64,
ipmi_config: IpmiConfig,
}
impl Default for AppConfig {
fn default() -> Self {
panic!("Could not find config file")
}
}
#[derive(Debug, Serialize, Deserialize)]
struct IpmiConfig {
ip: String,
user: String,
password: String,
}