Initial revision fan control via sysfs and PWM

This commit is contained in:
2025-11-13 00:47:47 +01:00
parent fb21f1cda1
commit c0f67b890f
6 changed files with 1483 additions and 0 deletions

932
fancontrol/Cargo.lock generated Normal file
View File

@@ -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",
]

23
fancontrol/Cargo.toml Normal file
View File

@@ -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"

10
fancontrol/README.md Normal file
View File

@@ -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`

25
fancontrol/config.toml Normal file
View File

@@ -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]

View File

@@ -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

475
fancontrol/src/main.rs Normal file
View File

@@ -0,0 +1,475 @@
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<DeviceConfig>,
}
#[derive(Deserialize, Debug, Clone)]
struct GlobalConfig {
#[serde(default = "default_hysteresis")]
spindown_hysteresis_seconds: u32,
#[serde(default = "default_pwm_steps")]
pwm_steps: Vec<u8>,
#[serde(default = "default_thresholds_t_edge")]
thresholds_t_edge: Vec<f32>,
#[serde(default = "default_thresholds_t_junction")]
thresholds_t_junction: Vec<f32>,
#[serde(default = "default_thresholds_t_memory")]
thresholds_t_memory: Vec<f32>,
}
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<u8> {
GlobalConfig::default().pwm_steps
}
fn default_thresholds_t_edge() -> Vec<f32> {
GlobalConfig::default().thresholds_t_edge
}
fn default_thresholds_t_junction() -> Vec<f32> {
GlobalConfig::default().thresholds_t_junction
}
fn default_thresholds_t_memory() -> Vec<f32> {
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<String, String> {
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<u8>, thresholds: &Vec<f32>) -> 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<AtomicBool>,
) {
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<f32, std::io::Error> {
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<PwmControlState, std::io::Error> {
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<f32, std::io::Error> {
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<TemperatureFiles, std::io::Error> {
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<ExpandedDeviceConfig> {
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::SIGCONT,
consts::SIGINT,
consts::SIGQUIT,
consts::SIGTERM,
];
for sig in signals.iter() {
flag::register(*sig, shutdown_flag.clone()).unwrap();
}
let args: Vec<String> = 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::<Vec<_>>();
join_handles.into_iter().for_each(|h| h.join().unwrap());
}