#!/usr/bin/env python3
"""
fancurve — GPU fan curve via RDNA3 OverDrive firmware interface
Programs a 5-point fan curve into the GPU firmware (gpu_od/fan_ctrl).
Hardware: MSI MAG B550 Tomahawk / Radeon RX 7900 XT
Install: sudo install -m755 fancurve.py /usr/local/bin/fancurve
"""

import os, sys, time, signal, logging, re, subprocess

logging.basicConfig(level=logging.INFO,
    format='%(asctime)s [fancurve] %(levelname)s %(message)s', datefmt='%H:%M:%S')
log = logging.getLogger()

GPU_DEV      = '/sys/devices/pci0000:00/0000:00:03.1/0000:2b:00.0/0000:2c:00.0/0000:2d:00.0'
FAN_CTRL     = GPU_DEV + '/gpu_od/fan_ctrl'
FAN_CURVE_F  = FAN_CTRL + '/fan_curve'
FAN_ZERO_RPM = FAN_CTRL + '/fan_zero_rpm_enable'
FAN_MIN_PWM  = FAN_CTRL + '/fan_minimum_pwm'
OD_COMMIT    = GPU_DEV  + '/pp_od_clk_voltage'
HWMON_GPU    = '/sys/class/hwmon/hwmon2'
HWMON_CPU    = '/sys/class/hwmon/hwmon3'

CURVE = [
    (30,  23),
    (55,  23),
    (72,  45),
    (83,  65),
    (95,  90),
]
ZERO_RPM = 0
MIN_PWM  = 23
INTERVAL = 30

def read_int(path):
    with open(path) as f: return int(f.read().strip())

def write(path, val):
    try:
        with open(path, 'w') as f: f.write(str(val) + '\n')
    except PermissionError:
        subprocess.run(['sudo','-n','tee',path], input=str(val)+'\n', text=True, capture_output=True)

def apply_curve():
    write(FAN_ZERO_RPM, ZERO_RPM)
    write(FAN_MIN_PWM, MIN_PWM)
    for i,(t,d) in enumerate(CURVE): write(FAN_CURVE_F, f'{i} {t} {d}')
    write(OD_COMMIT, 'c')

def curve_ok():
    try:
        txt = open(FAN_CURVE_F).read()
        return all(f'{i}: {t}C {d}%' in txt for i,(t,d) in enumerate(CURVE))
    except: return False

def restore():
    try:
        write(FAN_ZERO_RPM, 1)
        write(OD_COMMIT, 'r')
        log.info("Fan curve reset to firmware defaults")
    except Exception as e: log.error(f"Restore failed: {e}")

running = True
def on_signal(sig, frame):
    global running; running = False
signal.signal(signal.SIGTERM, on_signal)
signal.signal(signal.SIGINT, on_signal)

for p in [FAN_CURVE_F, FAN_ZERO_RPM, OD_COMMIT]:
    if not os.path.exists(p):
        log.error(f"Not found: {p}"); sys.exit(1)

log.info("=== fancurve starting (RDNA3 OD firmware mode) ===")
log.info(f"Curve: {CURVE}")
try:
    apply_curve()
    log.info("Fan curve applied and committed")
    while running:
        time.sleep(INTERVAL)
        if not running: break
        if not curve_ok():
            log.info("Curve reset detected — re-applying"); apply_curve()
        try:
            j = read_int(HWMON_GPU+'/temp2_input')/1000
            e = read_int(HWMON_GPU+'/temp1_input')/1000
            c = read_int(HWMON_CPU+'/temp1_input')/1000 if os.path.exists(HWMON_CPU+'/temp1_input') else 0
            try: rpm = str(read_int(HWMON_GPU+'/fan1_input'))+'RPM'
            except: rpm = 'RPM=N/A'
            log.info(f"junction={j:.0f}°C edge={e:.0f}°C cpu={c:.0f}°C {rpm}")
        except Exception as ex: log.warning(f"Sensor: {ex}")
finally:
    restore()
    log.info("fancurve stopped")
