Skip to content

Instantly share code, notes, and snippets.

@TheGammaSqueeze
Created January 8, 2026 19:39
Show Gist options
  • Select an option

  • Save TheGammaSqueeze/5687c0ced0f59bdff283fe560a9a5a2f to your computer and use it in GitHub Desktop.

Select an option

Save TheGammaSqueeze/5687c0ced0f59bdff283fe560a9a5a2f to your computer and use it in GitHub Desktop.
Fake Fan PWM for Mangmi Air X
// fan_fake_pwm.c
//
// Purpose
// Minimal-overhead "fake PWM" for a fan sysfs attribute that effectively supports
// only two discrete values: 0 (OFF) and 255 (ON).
//
// Behaviour
// 1) One-time screen check via Android system properties:
// - If sys.screen.state == "off" OR debug.tracing.screen_brightness == "0.0",
// the fan is forced OFF and the process exits.
// 2) If the screen is on, a tight loop alternates between 255 and 0.
// The duty cycle is expressed as "strength" (0..100) and implemented by:
// - ON for on_ms milliseconds
// - OFF for off_ms milliseconds, where off_ms is derived from strength
// 3) A gradual ramp is supported from a starting strength to a target strength.
// The ramp proceeds one strength unit per fixed number of PWM cycles until the
// target is reached.
// 4) On SIGINT/SIGTERM/SIGHUP/SIGQUIT, the process terminates without forcing the
// fan OFF. Fan shutdown is expected to be handled by the supervising control plane
// (for example, screen-off triggers and explicit "off" mode actions).
//
// Efficiency and correctness notes
// - Opens the sysfs node once and uses pwrite(..., offset=0) for each update.
// This avoids per-iteration open/close overhead while ensuring sysfs writes
// always start at offset 0.
// - Uses clock_nanosleep(CLOCK_MONOTONIC) to provide stable sleep behaviour.
// - Avoids logging in the control loop to minimise jitter and overhead.
#include <fcntl.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <sys/system_properties.h>
#define PWM_PATH "/sys/devices/platform/soc/soc:fan/hwmon/hwmon0/pwm1"
// Ramp tuning:
// One strength step is applied after this many complete PWM cycles (ON+OFF).
// Lower values ramp faster, higher values ramp more slowly.
#define RAMP_CYCLES_PER_STEP 5
static volatile sig_atomic_t g_running = 1;
static void handle_signal(int sig) {
(void)sig;
g_running = 0;
}
static int prop_get(const char *key, char *out, size_t out_sz) {
if (!key || !out || out_sz == 0) return -1;
out[0] = '\0';
int n = __system_property_get(key, out);
if (n < 0) return -1;
if ((size_t)n >= out_sz) out[out_sz - 1] = '\0';
return n;
}
static int write_pwm_pwrite(int fd, const char *s, size_t len) {
// sysfs attributes generally expect writes at offset 0
ssize_t w = pwrite(fd, s, len, 0);
if (w < 0) return -1;
return (w == (ssize_t)len) ? 0 : -1;
}
static void sleep_ns(long ns) {
if (ns <= 0) return;
struct timespec ts;
ts.tv_sec = ns / 1000000000L;
ts.tv_nsec = ns % 1000000000L;
// Restart on EINTR to keep timing stable under signal delivery.
while (clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, &ts) == EINTR) {
if (!g_running) break;
}
}
static int clamp_int(int v, int lo, int hi) {
if (v < lo) return lo;
if (v > hi) return hi;
return v;
}
static long compute_off_ns(int on_ms, int strength) {
// strength is expected to be within 1..100.
// off_ms = on_ms * (100 - strength) / strength
if (strength >= 100) return 0;
if (strength <= 0) return 0;
int off_ms = (on_ms * (100 - strength)) / strength;
return (long)off_ms * 1000000L;
}
int main(int argc, char **argv) {
// Arguments:
// argv[1] = target_strength (0..100)
// argv[2] = on_ms (>=1)
// argv[3] = start_strength (0..100, optional; defaults to target_strength)
//
// Examples:
// fan_fake_pwm 100 70
// fan_fake_pwm 100 70 80 (ramps 80 -> 100)
// fan_fake_pwm 80 20 100 (ramps 100 -> 80)
int target_strength = 100;
int on_ms = 70;
int start_strength = -1;
if (argc >= 2) target_strength = atoi(argv[1]);
if (argc >= 3) on_ms = atoi(argv[2]);
if (argc >= 4) start_strength = atoi(argv[3]);
target_strength = clamp_int(target_strength, 0, 100);
if (on_ms < 1) on_ms = 1;
if (start_strength < 0) start_strength = target_strength;
start_strength = clamp_int(start_strength, 0, 100);
// One-time screen check only
char screen[PROP_VALUE_MAX];
char bright[PROP_VALUE_MAX];
prop_get("sys.screen.state", screen, sizeof(screen));
prop_get("debug.tracing.screen_brightness", bright, sizeof(bright));
// If screen is off (or brightness is effectively off), force fan OFF and exit.
if (strcmp(screen, "off") == 0 || strcmp(bright, "0.0") == 0) {
int fd0 = open(PWM_PATH, O_WRONLY | O_CLOEXEC);
if (fd0 >= 0) {
(void)write_pwm_pwrite(fd0, "0\n", 2);
close(fd0);
}
return 0;
}
// Signal handling for termination
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handle_signal;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);
int fd = open(PWM_PATH, O_WRONLY | O_CLOEXEC);
if (fd < 0) return 1;
const char *ON_STR = "255\n";
const size_t ON_LEN = 4;
const char *OFF_STR = "0\n";
const size_t OFF_LEN = 2;
long on_ns = (long)on_ms * 1000000L;
// Strength 0 is treated as "always off and exit".
if (target_strength <= 0) {
(void)write_pwm_pwrite(fd, OFF_STR, OFF_LEN);
close(fd);
return 0;
}
int cur_strength = start_strength;
int direction = 0;
if (cur_strength < target_strength) direction = 1;
else if (cur_strength > target_strength) direction = -1;
// If the starting strength is 0 but a non-zero target is required, begin at 1.
// This avoids division by zero while still ramping smoothly.
if (cur_strength <= 0) cur_strength = 1;
long off_ns = compute_off_ns(on_ms, cur_strength);
// Ramp counter is maintained outside of the critical write/sleep path.
unsigned ramp_countdown = RAMP_CYCLES_PER_STEP;
while (g_running) {
// Apply ramp steps at a fixed cadence measured in PWM cycles.
if (direction != 0) {
if (ramp_countdown == 0) {
int next = cur_strength + direction;
if ((direction > 0 && next >= target_strength) ||
(direction < 0 && next <= target_strength)) {
cur_strength = target_strength;
direction = 0;
} else {
cur_strength = next;
}
if (cur_strength <= 0) cur_strength = 1;
off_ns = compute_off_ns(on_ms, cur_strength);
ramp_countdown = RAMP_CYCLES_PER_STEP;
} else {
ramp_countdown--;
}
}
// 255 ON
if (write_pwm_pwrite(fd, ON_STR, ON_LEN) != 0) break;
sleep_ns(on_ns);
// 0 OFF
if (write_pwm_pwrite(fd, OFF_STR, OFF_LEN) != 0) break;
sleep_ns(off_ns);
}
// Termination path: do not force a final OFF write here.
close(fd);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment