|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +fand is a small Linux fan-control daemon written in C. It polls hwmon temperature |
| 4 | +sensors from `/sys`, maps them to PWM duty cycles via piecewise-linear curves, and |
| 5 | +writes the results to hwmon PWM devices. Config is YAML; the only external |
| 6 | +dependency is libyaml. |
| 7 | + |
| 8 | +## Build & test |
| 9 | + |
| 10 | +```sh |
| 11 | +make # builds ./main (installed as fand) |
| 12 | +make test # builds ./test_runner and runs it; all assertions must pass |
| 13 | +make install # installs binary + systemd unit (run as root) |
| 14 | +``` |
| 15 | + |
| 16 | +- Toolchain: `gcc -Wall -Wpedantic -g`, links `-lyaml -lm` (order matters: libs |
| 17 | + after sources). Requires C11 (uses `__VA_OPT__`). |
| 18 | +- CI (`.github/workflows/test.yml`) runs exactly `make test` on every push/PR — |
| 19 | + keep it green. Tests run unprivileged and must never touch real hwmon |
| 20 | + devices (they use in-memory objects and temp files under `/tmp`). |
| 21 | +- Run `make test` before committing anything C. |
| 22 | + |
| 23 | +## Code layout |
| 24 | + |
| 25 | +| File | Responsibility | |
| 26 | +|---|---| |
| 27 | +| `daemon.c` | `main()`, signal handling, main loop, config validation | |
| 28 | +| `config.c` | libyaml event-based parser; builds the full object tree | |
| 29 | +| `hwmon.c` | `hwmon_resolve_path()` — stable paths (`.../hwmon`), glob expansion | |
| 30 | +| `sensor.c` | `tempN_input` reader (millidegrees ÷ 1000 + offset; NAN on error) | |
| 31 | +| `curve.c` | Piecewise-linear curve, clamps 0–255 | |
| 32 | +| `fan.c` | `pwmN` writer with hysteresis, `pwmN_enable` manual/auto switching | |
| 33 | +| `zone.c` | Zone = sensors + fans; max sensor value drives all zone fans | |
| 34 | +| `common.h` | Constants (`MAX_ZONES`, `MAX_ZONE_SIZE`, `MAX_PATH`), version, log macros | |
| 35 | +| `test.c` | Hand-rolled test harness (ASSERT/ASSERT_EQ), no framework | |
| 36 | + |
| 37 | +## Invariants (do not break) |
| 38 | + |
| 39 | +- `pwmN_enable` semantics: `1` = manual control (fand owns the fan), `5` = |
| 40 | + hardware auto. `fand_config_enable()` sets 1 at start, `fand_config_disable()` |
| 41 | + must restore 5 on every exit path (shutdown, SIGHUP reload, fatal config |
| 42 | + error) so fans are never left in manual control by a dead daemon. |
| 43 | +- Curve inputs must be strictly increasing; `curve_create()` enforces this and |
| 44 | + returns NULL. Keep that validation if you touch `curve.c`. |
| 45 | +- Zones with zero fans or zero sensors are silently skipped by the parser. |
| 46 | +- Config parse failures must degrade gracefully: skip the invalid object with a |
| 47 | + `DBG()` message, don't crash. `fand_config_load()` returns NULL only when the |
| 48 | + file is unreadable/unparseable or no zones survive. |
| 49 | +- `hwmon_resolve_path()` must keep working for: explicit `hwmonN` paths |
| 50 | + (backward compat), paths ending in `/hwmon` (scan for first `hwmonN` child), |
| 51 | + and glob patterns. Callers own the returned allocation. |
| 52 | +- All sensor values are thousandths of a degree (e.g. 52300 = 52.3°C). |
| 53 | + |
| 54 | +## Conventions |
| 55 | + |
| 56 | +- C11, 4-space indent, `snake_case` functions, `static` for file-local |
| 57 | + functions, `struct`-per-subsystem headers. |
| 58 | +- Diagnostics go through `DBG()` (stderr). There is no log-level system; |
| 59 | + `DEBUG 1` in `common.h` is hardcoded. |
| 60 | +- No new runtime dependencies. Keep it libyaml + libc + libm. |
| 61 | +- Tests: `static void test_xxx(void)` in `test.c` using `ASSERT`/`ASSERT_EQ`, |
| 62 | + registered at the end of `main()`. Follow the existing pattern — no |
| 63 | + frameworks. |
| 64 | +- Bump `FAND_VERSION` in `common.h` and `pkgver` in `PKGBUILD` together when |
| 65 | + releasing (they have drifted: 0.1.1 vs 1.3). |
| 66 | + |
| 67 | +## Runtime facts |
| 68 | + |
| 69 | +- Default config path is `fand.conf` in the CWD; pass the path as argv[1]. |
| 70 | +- Signals: `SIGTERM`/`SIGINT` = graceful shutdown (restores hardware control); |
| 71 | + `SIGHUP` = reload config (bad new config → keep the old one running). |
| 72 | +- Main loop polls every `poll_interval` seconds (default 1); per-fan |
| 73 | + `hysteresis` skips PWM writes within the threshold to reduce sysfs churn. |
| 74 | +- systemd unit `fand.service` expects `/usr/local/bin/fand /etc/fand.conf`. |
0 commit comments