$dir, 'pwm' => $pwm, 'enable' => $dir . '/pwm1_enable', 'rpm' => $dir . '/fan1_input', 'name' => first_readable([$dir . '/name']) ?: 'cooling_fan', ]; } return [ 'base' => '', 'pwm' => '', 'enable' => '', 'rpm' => '', 'name' => '', ]; } function local_temp(): float { $raw = first_readable([ '/sys/class/thermal/thermal_zone0/temp', '/sys/devices/virtual/thermal/thermal_zone0/temp', ]); if ($raw !== '' && is_numeric($raw)) { return round(((float)$raw) / 1000, 2); } $vc = sh(['/usr/bin/vcgencmd', 'measure_temp'], false, 3)['out']; if (preg_match('/([0-9.]+)/', $vc, $m)) { return round((float)$m[1], 2); } return 0.0; } function local_target(float $temp): int { if ($temp >= 80) { return 255; } if ($temp <= 50) { return 0; } $ratio = ($temp - 50) / 30; return max(0, min(255, (int)round($ratio * 255))); } function local_ramped_pwm(int $current, int $desired, float $temp): int { if ($temp >= 80) { return $desired; } if ($desired > $current) { return min($desired, $current + 2); } if ($desired < $current) { return max($desired, $current - 2); } return $desired; } function local_mem(): array { $rows = []; foreach (@file('/proc/meminfo', FILE_IGNORE_NEW_LINES) ?: [] as $line) { if (preg_match('/^([^:]+):\s+(\d+)/', $line, $m)) { $rows[$m[1]] = (int)$m[2]; } } $total = (int)($rows['MemTotal'] ?? 0); $available = (int)($rows['MemAvailable'] ?? 0); $used = max(0, $total - $available); return [ 'total_mb' => (int)round($total / 1024), 'used_mb' => (int)round($used / 1024), 'free_mb' => (int)round($available / 1024), ]; } function local_disk(string $path = '/'): array { $total = @disk_total_space($path) ?: 0; $free = @disk_free_space($path) ?: 0; $used = max(0, $total - $free); return [ 'total_kb' => (int)round($total / 1024), 'used_kb' => (int)round($used / 1024), 'free_kb' => (int)round($free / 1024), ]; } function local_uptime_seconds(): int { $raw = trim((string)@file_get_contents('/proc/uptime')); if ($raw === '') { return 0; } return (int)floor((float)explode(' ', $raw)[0]); } function read_int_file(string $file): int { if ($file === '' || !is_readable($file)) { return 0; } $raw = trim((string)@file_get_contents($file)); return is_numeric($raw) ? (int)$raw : 0; } function write_sys_value(string $path, int $value): bool { if ($path === '') { return false; } return @file_put_contents($path, $value . "\n", LOCK_EX) !== false; } function dmesg_log(string $message): void { $line = '<6>fanpanel: ' . $message . "\n"; @file_put_contents('/dev/kmsg', $line, FILE_APPEND); } function dmesg_log_throttled(string $key, string $message, int $seconds = 30): void { $file = '/tmp/fanpanel_dmesg_' . preg_replace('/[^a-zA-Z0-9_.-]/', '_', $key) . '.last'; $now = time(); $last = is_file($file) ? (int)trim((string)@file_get_contents($file)) : 0; if (($now - $last) < $seconds) { return; } @file_put_contents($file, (string)$now, LOCK_EX); dmesg_log($message); } try { $state = get_control_state(); $mode = (string)($state['mode'] ?? 'auto'); $manualPwm = max(0, min(255, (int)($state['manual_pwm'] ?? 120))); if (!in_array($mode, ['auto', 'manual', 'off'], true)) { $mode = 'auto'; } $paths = local_fan_paths(); $temp = local_temp(); $currentPwm = read_int_file($paths['pwm']); $desired = match ($mode) { 'manual' => $manualPwm, 'off' => 0, default => local_target($temp), }; $target = $mode === 'auto' ? local_ramped_pwm($currentPwm, $desired, $temp) : $desired; $enableOk = write_sys_value($paths['enable'], $mode === 'off' ? 0 : 1); $pwmOk = write_sys_value($paths['pwm'], $target); usleep(80000); $actualPwm = read_int_file($paths['pwm']); $rpm = read_int_file($paths['rpm']); $load = sys_getloadavg() ?: [0, 0, 0]; $mem = local_mem(); $disk = local_disk('/'); $cpuPower = cpu_power_status(); $battery = battery_status(); $ok = $enableOk && $pwmOk; add_sensor_log([ 'cpu_temp_c' => $temp, 'fan_rpm' => $rpm, 'fan_efficiency' => fan_efficiency($rpm, $temp, $cpuPower['watts'] ?? null), 'rp1_temp_c' => rp1_temp_c(), 'cpu_voltage' => $cpuPower['voltage'], 'cpu_watts' => $cpuPower['watts'], 'battery_voltage' => $battery['voltage'], 'battery_percent' => $battery['percent'], 'pwm_value' => $actualPwm, 'pwm_percent' => round($actualPwm / 255 * 100, 2), 'pwm_mode' => $mode, 'cpu_load_1' => round((float)$load[0], 2), 'cpu_load_5' => round((float)$load[1], 2), 'cpu_load_15' => round((float)$load[2], 2), 'mem_total_mb' => $mem['total_mb'], 'mem_used_mb' => $mem['used_mb'], 'mem_free_mb' => $mem['free_mb'], 'disk_total_kb' => $disk['total_kb'], 'disk_used_kb' => $disk['used_kb'], 'disk_free_kb' => $disk['free_kb'], 'uptime_seconds' => local_uptime_seconds(), 'hostname' => gethostname() ?: null, ]); send_battery_low_push_if_needed($battery); if (!$ok) { add_fan_action( 'apply_policy_failed', $mode, $target, 'enable=' . ($enableOk ? 'ok' : 'fail') . ', pwm=' . ($pwmOk ? 'ok' : 'fail') . ', path=' . ($paths['pwm'] ?: 'N/A'), false ); } exit($ok ? 0 : 1); } catch (Throwable $e) { try { add_fan_action( 'apply_policy_exception', null, null, $e->getMessage(), false ); } catch (Throwable) { } fwrite(STDERR, $e->getMessage() . PHP_EOL); exit(1); }