From c5336db56bede0ca512b6aa44f91aaf403eaf094 Mon Sep 17 00:00:00 2001 From: Andreas Date: Thu, 13 Nov 2025 00:47:47 +0100 Subject: [PATCH] Initial revision fan control via sysfs and PWM --- fancontrol/Cargo.lock | 932 ++++++++++++++++++++++++++++ fancontrol/Cargo.toml | 23 + fancontrol/README.md | 10 + fancontrol/config.toml | 25 + fancontrol/instinct_control.service | 18 + fancontrol/src/main.rs | 474 ++++++++++++++ 6 files changed, 1482 insertions(+) create mode 100644 fancontrol/Cargo.lock create mode 100644 fancontrol/Cargo.toml create mode 100644 fancontrol/README.md create mode 100644 fancontrol/config.toml create mode 100644 fancontrol/instinct_control.service create mode 100644 fancontrol/src/main.rs diff --git a/fancontrol/Cargo.lock b/fancontrol/Cargo.lock new file mode 100644 index 0000000..ce34e5e --- /dev/null +++ b/fancontrol/Cargo.lock @@ -0,0 +1,932 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cc" +version = "1.2.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "config" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml 0.8.23", + "yaml-rust2", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fancontrol" +version = "0.1.0" +dependencies = [ + "chrono", + "config", + "serde", + "signal-hook", + "tempdir", + "toml 0.5.11", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pest" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64", + "bitflags", + "serde", + "serde_derive", +] + +[[package]] +name = "rust-ini" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand", + "remove_dir_all", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "yaml-rust2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/fancontrol/Cargo.toml b/fancontrol/Cargo.toml new file mode 100644 index 0000000..0cd14a4 --- /dev/null +++ b/fancontrol/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "fancontrol" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "amd_gpu_control" +path = "src/main.rs" + +[[bin]] +name = "test_app" +path = "src/test_app.rs" + + + +[dependencies] +config = "0.14" +toml = "0.5" +tempdir = "0.3" +serde = { version = "1.0", features = ["derive"] } +#tokio = { version = "1", features = ["full"] } +chrono = "0.4" +signal-hook = "0.3" diff --git a/fancontrol/README.md b/fancontrol/README.md new file mode 100644 index 0000000..fb3b786 --- /dev/null +++ b/fancontrol/README.md @@ -0,0 +1,10 @@ +# Pwm fan control for Instinct cards + +## Setup + - Build with `cargo build --release` + - copy `target/release/amd_gpu_control` to `/usr/local/bin/` + - copy the systemd service file `instinct_control.service` to `/etc/systemd/system/` + - copy the config file `config.toml` to `/etc/instinct_control/` and customize + - verify pwm devices by hand + - ensure only root can write the config to prevent path injection + - `systemctl enable instinct_control.service` diff --git a/fancontrol/config.toml b/fancontrol/config.toml new file mode 100644 index 0000000..17d04f0 --- /dev/null +++ b/fancontrol/config.toml @@ -0,0 +1,25 @@ +# one device section per card to be controlled +[[device]] +# name for log display +name = "Higher Instinct" +# pci id, determine with lspci +pci_id = "0b:00.0" +# parameters for fan control, device to be used and index of the pwm channel in the +# corresponding hmon device +fan_control = {device = "nct6775.656", pwm_number = 1} +#fan_control = {ipmi_command = "external command line"} # currently not implemented. + +#[[device]] +#name = "Lower Instinct" +#pci_id = "0b:00.0" +#fan_control_device = "nct6775.656" +#fan_control_pwm_number = 1 + +# [global] +# # Delay before fan is spun down when temperature falls below threshold for current level +# spindown_hysteresis_seconds = 30 +# # If specifying any of the following, give all 4 of them and make sure pwm_steps has one entry more than the others +# pwm_steps = [51, 102, 153, 180, 204, 225, 255] +# thresholds_t_edge = [70.0, 80.0, 90.0, 92.0, 94.0, 96.0] +# thresholds_t_junction = [75.0, 92.0, 94.0, 97.0, 98.0, 100.0] +# thresholds_t_memory = [75.0, 85.0, 90.0, 91.0, 92.0, 94.0] diff --git a/fancontrol/instinct_control.service b/fancontrol/instinct_control.service new file mode 100644 index 0000000..9873fa2 --- /dev/null +++ b/fancontrol/instinct_control.service @@ -0,0 +1,18 @@ +[Unit] +Description=AMD Instinct custom fan control +After=sysinit.target + +[Service] +User=root +Group=root + + +ExecStart=/usr/local/bin/amd_gpu_control --config /etc/instinct_control/config.toml + + +Restart=on-failure +RestartSec=2 + +[Install] +# Enable auto-start at boot +WantedBy=multi-user.target diff --git a/fancontrol/src/main.rs b/fancontrol/src/main.rs new file mode 100644 index 0000000..de1c6f4 --- /dev/null +++ b/fancontrol/src/main.rs @@ -0,0 +1,474 @@ +use config::{Config as ConfigBuilder, File}; +use serde::Deserialize; +use signal_hook::{consts, flag}; +use std::env; +use std::fs; +use std::io::Write; +use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; + +#[derive(Deserialize, Debug)] +struct Config { + #[serde(default)] + global: GlobalConfig, + device: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +struct GlobalConfig { + #[serde(default = "default_hysteresis")] + spindown_hysteresis_seconds: u32, + #[serde(default = "default_pwm_steps")] + pwm_steps: Vec, + #[serde(default = "default_thresholds_t_edge")] + thresholds_t_edge: Vec, + #[serde(default = "default_thresholds_t_junction")] + thresholds_t_junction: Vec, + #[serde(default = "default_thresholds_t_memory")] + thresholds_t_memory: Vec, +} + +impl Default for GlobalConfig { + fn default() -> Self { + Self { + spindown_hysteresis_seconds: 30, + pwm_steps: vec![51, 102, 153, 180, 204, 225, 255], + thresholds_t_edge: vec![70.0, 80.0, 90.0, 92.0, 94.0, 96.0], + thresholds_t_junction: vec![75.0, 92.0, 94.0, 97.0, 98.0, 100.0], + thresholds_t_memory: vec![75.0, 85.0, 90.0, 91.0, 92.0, 94.0], + } + } +} +// somewhat boilerplaty, but want to support partial specification of global options while keeping defaults in one place. +fn default_hysteresis() -> u32 { + GlobalConfig::default().spindown_hysteresis_seconds +} + +fn default_pwm_steps() -> Vec { + GlobalConfig::default().pwm_steps +} + +fn default_thresholds_t_edge() -> Vec { + GlobalConfig::default().thresholds_t_edge +} + +fn default_thresholds_t_junction() -> Vec { + GlobalConfig::default().thresholds_t_junction +} + +fn default_thresholds_t_memory() -> Vec { + GlobalConfig::default().thresholds_t_memory +} + +fn check_global_config(gc: &GlobalConfig) -> bool { + if !gc.pwm_steps.iter().is_sorted() { + eprintln!("pwm steps not monotonic"); + return false; + } + if !gc.thresholds_t_edge.iter().is_sorted() { + eprintln!("t_edge thresholds not sorted"); + return false; + } + if !gc.thresholds_t_junction.iter().is_sorted() { + eprintln!("t_junction thresholds not sorted"); + return false; + } + if !gc.thresholds_t_memory.iter().is_sorted() { + eprintln!("t_memory thresholds not sorted"); + return false; + } + if gc.thresholds_t_edge.len() != gc.thresholds_t_junction.len() + || gc.thresholds_t_junction.len() != gc.thresholds_t_memory.len() + || gc.thresholds_t_edge.len() + 1 != gc.pwm_steps.len() + { + eprintln!("Temperature threshold vectors need to be all of same length and number of pwm steps needs to be one more."); + return false; + } + true +} + +#[derive(Deserialize, Debug)] +struct DeviceConfig { + name: String, + pci_id: String, + fan_control: FanControlType, +} + +#[derive(Deserialize, Debug)] +#[serde(untagged)] +pub enum FanControlType { + Ipmi { ipmi_command: String }, + Pwm { device: String, pwm_number: u32 }, +} + +#[derive(Debug)] +struct ExpandedDeviceConfig { + device: String, + power: PowerPath, + pwm: PwmPath, + temperature: TemperatureFiles, +} + +#[derive(Default, Debug)] +struct TemperatureFiles { + junction: String, + overall: String, + memory: String, +} + +#[derive(Deserialize, Debug)] +struct PwmPath { + file: String, +} + +#[derive(Deserialize, Debug)] +struct PowerPath { + file: String, +} + +struct PwmControlState { + value: u8, // 1 manual control +} + +const MANUAL_CONTROL_STATE: PwmControlState = PwmControlState { value: 1 }; + +fn find_hwmon_for_device(pci_addr_or_device_name: &str) -> Result { + let hwmon_dir = Path::new("/sys/class/hwmon"); + + for entry in fs::read_dir(hwmon_dir).map_err(|e| format!("HWMON read error: {}", e))? { + let entry = entry.map_err(|e| format!("HWMON entry error: {}", e))?; + let path = entry.path(); + + if !path.is_dir() { + continue; + } + + let device_path = path.join("device"); + if !device_path.exists() { + continue; + } + + let target = fs::read_link(&device_path).map_err(|e| format!("Link read error: {}", e))?; + + if target.to_string_lossy().contains(pci_addr_or_device_name) { + return Ok(path + .to_str() + .map(|s| s.to_string()) + .ok_or("Invalid hwmon name")?); + } + } + + Err(format!( + "HWMON not found for device: {}", + pci_addr_or_device_name + )) +} + +fn determine_min_pwm_setpoint( + global_options: &GlobalConfig, + t_edge: f32, + t_junction: f32, + t_memory: f32, + previous_setpoint: u8, +) -> u8 { + fn lookup_pwm_level(value: f32, levels: &Vec, thresholds: &Vec) -> u8 { + let mut level = levels[0]; + for (i, &th) in thresholds.iter().enumerate() { + if value >= th { + level = levels[i + 1]; + } else { + break; + } + } + level + } + + let l_edge = lookup_pwm_level( + t_edge, + &global_options.pwm_steps, + &global_options.thresholds_t_edge, + ); + let l_junction = lookup_pwm_level( + t_junction, + &global_options.pwm_steps, + &global_options.thresholds_t_junction, + ); + let l_memory = lookup_pwm_level( + t_memory, + &global_options.pwm_steps, + &global_options.thresholds_t_memory, + ); + let l_previous_minus_one = *global_options + .pwm_steps + .iter() + .filter(|x| **x < previous_setpoint) + .max() + .unwrap_or(&0); + [l_edge, l_junction, l_memory, l_previous_minus_one] + .into_iter() + .max() + .unwrap() +} + +fn monitor_temperatures( + config: ExpandedDeviceConfig, + global_options: GlobalConfig, + shutdown_flag: Arc, +) { + let initial_power_limit = read_power_limit(&config.power.file).unwrap_or(0.0); + let mut current_power_limit = initial_power_limit; + + let junction_temp = read_temperature(&config.temperature.junction).unwrap_or(105.0); + let overall_temp = read_temperature(&config.temperature.overall).unwrap_or(105.0); + let memory_temp = read_temperature(&config.temperature.memory).unwrap_or(94.0); + + let mut fan_level = + determine_min_pwm_setpoint(&global_options, overall_temp, junction_temp, memory_temp, 0); + let mut time_fan_level_set: Instant = Instant::now(); + + // Set initial fan speed + let original_pwm_mode = enable_pwm_control_and_set_init(&config.pwm.file, fan_level).unwrap(); + + loop { + if shutdown_flag.load(Ordering::SeqCst) { + // Set fan to 100% on exit + restore_pwm_mode(&config.pwm.file, original_pwm_mode).unwrap(); + break; + } + + let junction_temp = read_temperature(&config.temperature.junction).unwrap_or(105.0); + let overall_temp = read_temperature(&config.temperature.overall).unwrap_or(105.0); + let memory_temp = read_temperature(&config.temperature.memory).unwrap_or(94.0); + + // Check if any temperature is above critical thresholds + if junction_temp >= 102.0 || memory_temp >= 94.0 { + current_power_limit = current_power_limit - 3.0; + println!( + "High temperature detected, reducing power limit by 3 watt to {}", + current_power_limit + ); + set_power_limit(&config.power.file, current_power_limit).unwrap(); + } + let new_fan_level = determine_min_pwm_setpoint( + &global_options, + overall_temp, + junction_temp, + memory_temp, + fan_level, + ); + if new_fan_level < fan_level + && time_fan_level_set.elapsed() + < Duration::from_secs(global_options.spindown_hysteresis_seconds.into()) + { + //println!("Temperature lower but waiting for hysteresis time to expire before applying"); + } else if new_fan_level == fan_level { + // reset hysteris + time_fan_level_set = Instant::now(); + } else { + println!("Adjusting PWM from {} to {}", fan_level, new_fan_level); + fan_level = new_fan_level; + time_fan_level_set = Instant::now(); + set_pwm(&config.pwm.file, fan_level).unwrap(); + } + thread::sleep(Duration::from_millis(1000)); + } +} + +fn read_temperature(path: &str) -> Result { + let content = fs::read_to_string(path)?; + let value: f32 = content + .trim() + .parse() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + Ok(value as f32 * 1e-3) +} + +fn enable_pwm_control_and_set_init( + path: &str, + value: u8, +) -> Result { + println!("Reading old status from {}", format!("{}_enable", path)); + let old_status_text = fs::read_to_string(format!("{}_enable", path))?; + let old_status_val: u8 = old_status_text.trim().parse().map_err(|_e| { + std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse old pwm status.") + })?; + println!( + "Read old status {} from {}", + old_status_val, + format!("{}_enable", path) + ); + let old_status = PwmControlState { + value: old_status_val, + }; + + std::fs::write( + format!("{}_enable", path), + format!("{}", MANUAL_CONTROL_STATE.value), + )?; + if let Err(e) = std::fs::write(path, format!("{}", value)) { + eprintln!("Failed to write initial PWM value, restoring previous control state."); + std::fs::write(format!("{}_enable", path), format!("{}", old_status.value))?; + return Err(e.into()); + } + println!("Took over fan control and set PWM to {}", value); + Ok(old_status) +} + +fn restore_pwm_mode(path: &str, previous_mode: PwmControlState) -> Result<(), std::io::Error> { + println!("Restoring PWM control mode to {}", previous_mode.value); + std::fs::write( + format!("{}_enable", path), + format!("{}", previous_mode.value), + ) +} + +fn set_pwm(path: &str, value: u8) -> Result<(), std::io::Error> { + //let mut file = fs::File::create(path)?; + std::fs::write(path, format!("{}", value)) +} + +fn read_power_limit(path: &str) -> Result { + let content = fs::read_to_string(path)?; + let value: f32 = content + .trim() + .parse() + .map(|x: f32| x * 1e-6) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + Ok(value) +} + +fn set_power_limit(path: &str, value: f32) -> Result<(), std::io::Error> { + let mut file = fs::File::create(path)?; + writeln!(file, "{}", (value * 1e6) as u32) +} + +fn find_temperature_sensors(card_hwmon: &str) -> Result { + let mut sensor_idx: [u8; 3] = Default::default(); // index 0 unused by sysfs + (0..20).for_each(|i| { + let label_path = format!("{:}/temp{}_label", card_hwmon, i); + if let Ok(label) = fs::read_to_string(&label_path) { + let label = label.trim(); + println!("Read label {:} for temp {:}", &label, i); + match label { + "edge" => sensor_idx[0] = i, + "junction" => sensor_idx[1] = i, + "mem" => sensor_idx[2] = i, + _ => (), + } + } + }); + println!("Determined sensor indices {:?}", sensor_idx); + let overall = format!("{:}/temp{:}_input", card_hwmon, sensor_idx[0]); + let junction = format!("{:}/temp{:}_input", card_hwmon, sensor_idx[1]); + let memory = format!("{:}/temp{:}_input", card_hwmon, sensor_idx[2]); + if !Path::new(&overall).exists() { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Overall temp file {:} not found", overall), + )) + } else if !Path::new(&junction).exists() { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Overall temp file {:} not found", junction), + )) + } else if !Path::new(&memory).exists() { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Overall temp file {:} not found", memory), + )) + } else { + Ok(TemperatureFiles { + junction, + overall, + memory, + }) + } +} + +fn check_and_expand_config(config: &Config) -> Vec { + config + .device + .iter() + .map(|dev| -> ExpandedDeviceConfig { + println!("Checking config for {:}...", &dev.name); + let card_hwmon = + find_hwmon_for_device(&dev.pci_id).expect("Hwmon not found, aborting..."); + let (fan_device, fan_number) = match &dev.fan_control { + FanControlType::Pwm { device, pwm_number } => (device, pwm_number), + _ => unimplemented!("IPMI fan control not yet fully implementd."), + }; + + let pwm_hwmon = find_hwmon_for_device(&fan_device) + .expect("Hwmon not found for fan control device, aborting..."); + let power_file = format!("{:}/power1_cap", card_hwmon); + let pwm_file = format!("{:}/pwm{:}", pwm_hwmon, fan_number); + assert!( + Path::new(&power_file).exists(), + "Power limit control file {:} not found.", + power_file + ); + assert!( + Path::new(&pwm_file).exists(), + "Pwm control file {:} not found.", + pwm_file + ); + let temperature = + find_temperature_sensors(&card_hwmon).expect("Temperature sensor not found."); + ExpandedDeviceConfig { + device: dev.name.clone(), + power: PowerPath { file: power_file }, + pwm: PwmPath { file: pwm_file }, + temperature, + } + }) + .collect() +} + +fn main() { + let shutdown_flag = Arc::new(AtomicBool::new(false)); + + // Do clean shutdown on all catchable termination signals + let signals = [ + consts::SIGABRT, + consts::SIGALRM, + consts::SIGBUS, + consts::SIGINT, + consts::SIGQUIT, + consts::SIGTERM, + ]; + for sig in signals.iter() { + flag::register(*sig, shutdown_flag.clone()).unwrap(); + } + + let args: Vec = env::args().collect(); + let mut config_path = "config.toml".to_string(); + + for i in 0..args.len() { + if args[i] == "--config" && i + 1 < args.len() { + config_path = args[i + 1].clone(); + } + } + + let settings = ConfigBuilder::builder() + .add_source(File::with_name(&config_path)) + .build() + .unwrap(); + + let config: Config = settings.try_deserialize().unwrap(); + println!("Config {:?}", config); + assert!(check_global_config(&config.global), "Global options invalid. Make sure thresholds are monotonic and pwm steps one more than thresholds."); + let expanded_configs = check_and_expand_config(&config); + let join_handles = expanded_configs + .into_iter() + .map(|ec| { + println!("Spawning fan control thread for device {:}", &ec.device); + let gc = config.global.clone(); + let shutdown_flag = shutdown_flag.clone(); + std::thread::spawn(|| monitor_temperatures(ec, gc, shutdown_flag)) + }) + .collect::>(); + join_handles.into_iter().for_each(|h| h.join().unwrap()); +}