// setm4fan.c get / set fan speed on Mac mini (Mac16,10) via AppleSMC
// Compile: clang -framework IOKit -o setfan setfan.c

#include <IOKit/IOKitLib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

// SMC structures (matching successful getfan.c / smcdump.c)
typedef struct { char major, minor, build, reserved[1]; UInt16 release; } SMCVersion;
typedef struct { UInt16 version, length; UInt32 cpuPLimit, gpuPLimit, memPLimit; } SMCPLimitData;
typedef struct { UInt32 dataSize, dataType; UInt8 dataAttributes; } SMCKeyInfoData;

typedef struct {
    UInt32            key;
    SMCVersion        vers;
    SMCPLimitData     pLimitData;
    SMCKeyInfoData    keyInfo;
    UInt8             result;
    UInt8             status;
    UInt8             data8;
    UInt32            data32;
    UInt8             bytes[32];
} SMCParamStruct;

enum {
    kSMCHandleYPCEvent   = 2,
    kSMCGetKeyInfo       = 9,
    kSMCReadKey          = 5,
    kSMCWriteKey         = 6,
};

// Convert 4-char key to UInt32
static UInt32 s2u(const char *s) {
    UInt32 v = 0;
    for (int i = 0; i < 4; i++) v = (v << 8) | (unsigned char)s[i];
    return v;
}

// Pass-through call with error reporting
static int smc_call(io_connect_t conn, SMCParamStruct *in, SMCParamStruct *out,
                    IOReturn *iorc, UInt8 *smcResult) {
    size_t sz = sizeof(SMCParamStruct);
    *iorc = IOConnectCallStructMethod(conn, kSMCHandleYPCEvent, in, sz, out, &sz);
    if (*iorc != kIOReturnSuccess) return -1;
    *smcResult = out->result;
    if (*smcResult != 0) return -2;
    return 0;
}

// Get key info (size + type)
static int get_key_info(io_connect_t conn, const char *key, SMCKeyInfoData *ki,
                        IOReturn *iorc, UInt8 *smcr) {
    SMCParamStruct p = {0};
    p.data8 = kSMCGetKeyInfo;
    p.key   = s2u(key);
    int ret = smc_call(conn, &p, &p, iorc, smcr);
    if (ret == 0) *ki = p.keyInfo;
    return ret;
}

// Read generic bytes
static int read_bytes(io_connect_t conn, const char *key, void *data, size_t len,
                      IOReturn *iorc, UInt8 *smcr) {
    SMCKeyInfoData ki;
    if (get_key_info(conn, key, &ki, iorc, smcr) != 0) return -1;
    if (ki.dataSize != len) return -2;

    SMCParamStruct p = {0};
    p.data8   = kSMCReadKey;
    p.key     = s2u(key);
    p.keyInfo = ki;
    int ret = smc_call(conn, &p, &p, iorc, smcr);
    if (ret == 0) memcpy(data, p.bytes, len);
    return ret;
}

static int read_float(io_connect_t conn, const char *key, float *val,
                      IOReturn *iorc, UInt8 *smcr) {
    return read_bytes(conn, key, val, 4, iorc, smcr);
}

static int read_ui8(io_connect_t conn, const char *key, UInt8 *val,
                    IOReturn *iorc, UInt8 *smcr) {
    return read_bytes(conn, key, val, 1, iorc, smcr);
}

// Write generic bytes
static int write_bytes(io_connect_t conn, const char *key, const void *data, size_t len,
                       IOReturn *iorc, UInt8 *smcr) {
    SMCKeyInfoData ki;
    if (get_key_info(conn, key, &ki, iorc, smcr) != 0) return -1;
    if (ki.dataSize != len) return -2;

    SMCParamStruct p = {0};
    p.data8   = kSMCWriteKey;
    p.key     = s2u(key);
    p.keyInfo = ki;
    memcpy(p.bytes, data, len);
    return smc_call(conn, &p, &p, iorc, smcr);
}

static int write_ui8(io_connect_t conn, const char *key, UInt8 val,
                     IOReturn *iorc, UInt8 *smcr) {
    return write_bytes(conn, key, &val, 1, iorc, smcr);
}

static int write_float(io_connect_t conn, const char *key, float val,
                       IOReturn *iorc, UInt8 *smcr) {
    return write_bytes(conn, key, &val, 4, iorc, smcr);
}

static void print_error(const char *reason, IOReturn iorc, UInt8 smcr) {
    fprintf(stderr, "  %s (IOReturn=0x%x, SMCresult=0x%x)\n", reason, iorc, smcr);
    if (iorc == kIOReturnNotPrivileged || smcr == 0x84) // 0x84 = SMC error: not privileged
        fprintf(stderr, "    Hint: run as root (sudo) or add com.apple.system.smc entitlement.\n");
}

static io_connect_t open_smc(void) {
    io_service_t srv = IOServiceGetMatchingService(kIOMainPortDefault,
                                                   IOServiceMatching("AppleSMC"));
    if (!srv) return 0;
    io_connect_t conn;
    if (IOServiceOpen(srv, mach_task_self(), 0, &conn)) {
        IOObjectRelease(srv);
        return 0;
    }
    IOObjectRelease(srv);
    return conn;
}

int main(int argc, char **argv) {
    // --version / --help
    if (argc == 2) {
        if (strcmp(argv[1], "--version") == 0) {
            printf("setfan 1.0 (failsafe)\n");
            return 0;
        }
        if (strcmp(argv[1], "--help") == 0) {
            printf("Usage: setfan [RPM|auto]\n");
            printf("  no args    print current fan speed\n");
            printf("  RPM        set fan target speed (manual mode)\n");
            printf("  auto       return fan control to automatic\n");
            printf("  --help     show this message\n");
            printf("  --version  print version\n\n");
            printf("Write operations require root (sudo) or com.apple.system.smc entitlement.\n");
            return 0;
        }
    }

    if (geteuid() != 0) {
        fprintf(stderr, "Warning: not running as root. Write operations will likely fail.\n"
                        "         Try again with 'sudo %s'.\n\n",
                argv[0]);
    }

    io_connect_t conn = open_smc();
    if (!conn) {
        fprintf(stderr, "Error: Cannot open AppleSMC\n");
        return 1;
    }

    IOReturn iorc = 0;
    UInt8    smcr = 0;

    // Read fan count
    UInt8 nfans = 0;
    if (read_ui8(conn, "FNum", &nfans, &iorc, &smcr) != 0) {
        fprintf(stderr, "Error: Cannot read fan count\n");
        IOServiceClose(conn);
        return 1;
    }
    if (nfans == 0) {
        printf("No fans found.\n");
        IOServiceClose(conn);
        return 0;
    }

    // ---- no args: print current speed
    if (argc == 1) {
        for (UInt8 i = 0; i < nfans; i++) {
            char key[5];
            snprintf(key, sizeof(key), "F%dAc", i);
            float rpm = 0;
            if (read_float(conn, key, &rpm, &iorc, &smcr) == 0)
                printf("Fan %d: %.0f RPM\n", i, rpm);
            else
                printf("Fan %d: read error\n", i);
        }
        IOServiceClose(conn);
        return 0;
    }

    if (argc != 2) {
        fprintf(stderr, "Error: Wrong number of arguments\n");
        IOServiceClose(conn);
        return 1;
    }

    // ---- "auto"
    if (strcmp(argv[1], "auto") == 0) {
        int ok = 1;
        for (UInt8 i = 0; i < nfans; i++) {
            char key[5];
            snprintf(key, sizeof(key), "F%dMd", i);
            if (write_ui8(conn, key, 0, &iorc, &smcr) != 0) {
                fprintf(stderr, "Error: Failed to set auto mode for fan %u.\n", i);
                print_error("Set auto mode", iorc, smcr);
                ok = 0;
            }
        }
        if (ok) printf("Fan control set to automatic.\n");
        IOServiceClose(conn);
        return ok ? 0 : 1;
    }

    // ---- numeric RPM
    char *end;
    float rpm = strtof(argv[1], &end);
    if (end == argv[1] || *end != '\0' || rpm < 0) {
        fprintf(stderr, "Error: Invalid RPM \"%s\"\n", argv[1]);
        IOServiceClose(conn);
        return 1;
    }

    int ok = 1;
    for (UInt8 i = 0; i < nfans; i++) {
        char key[5];

        // Enable manual mode (1)
        snprintf(key, sizeof(key), "F%dMd", i);
        if (write_ui8(conn, key, 1, &iorc, &smcr) != 0) {
            fprintf(stderr, "Error: Failed to enable manual mode for fan %u.\n", i);
            print_error("Enable manual mode", iorc, smcr);
            ok = 0;
            continue;
        }

        // Set target RPM
        snprintf(key, sizeof(key), "F%dTg", i);
        if (write_float(conn, key, rpm, &iorc, &smcr) != 0) {
            fprintf(stderr, "Error: Failed to set target RPM for fan %u.\n", i);
            print_error("Set target RPM", iorc, smcr);
            ok = 0;
            // Fan stays in manual mode
        }
    }

    if (ok) printf("Fan set to %.0f RPM (manual)\n", rpm);
    IOServiceClose(conn);
    return ok ? 0 : 1;
}

