Created
January 8, 2026 19:39
-
-
Save TheGammaSqueeze/5687c0ced0f59bdff283fe560a9a5a2f to your computer and use it in GitHub Desktop.
Fake Fan PWM for Mangmi Air X
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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