├── .envrc ├── .github └── workflows │ ├── build.yml │ ├── release.yml │ └── tag.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── nix ├── module.nix ├── package.nix └── shell.nix └── src ├── battery.rs ├── cli ├── debug.rs └── mod.rs ├── config ├── load.rs ├── mod.rs └── types.rs ├── core.rs ├── cpu.rs ├── daemon.rs ├── engine.rs ├── main.rs ├── monitor.rs └── util ├── error.rs ├── mod.rs └── sysfs.rs /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Install Rust 17 | uses: dtolnay/rust-toolchain@stable 18 | 19 | - name: Cache dependencies 20 | uses: Swatinem/rust-cache@v2 21 | 22 | - name: Run tests 23 | run: cargo test 24 | 25 | build: 26 | name: Build 27 | strategy: 28 | matrix: 29 | include: 30 | - os: ubuntu-latest 31 | target: x86_64-unknown-linux-gnu 32 | # TODO: eventually we'd like to gate certain features 33 | # behind target OS, at which point this may be usable. 34 | #- os: macos-latest 35 | # target: x86_64-apple-darwin 36 | #- os: windows-latest 37 | # target: x86_64-pc-windows-msvc 38 | 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | - uses: actions/checkout@v3 42 | 43 | - name: Install Rust 44 | uses: dtolnay/rust-toolchain@stable 45 | with: 46 | targets: ${{ matrix.target }} 47 | 48 | - name: Cache dependencies 49 | uses: Swatinem/rust-cache@v2 50 | 51 | - name: Build 52 | run: cargo build --release --target ${{ matrix.target }} 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release Builds 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | outputs: 15 | upload_url: ${{ steps.create_release.outputs.upload_url }} 16 | release_id: ${{ steps.create_release.outputs.id }} 17 | steps: 18 | - name: Create Release 19 | id: create_release 20 | uses: softprops/action-gh-release@v2 21 | with: 22 | draft: false 23 | prerelease: false 24 | generate_release_notes: true 25 | 26 | build-release: 27 | needs: create-release 28 | strategy: 29 | matrix: 30 | include: 31 | - os: ubuntu-latest 32 | target: x86_64-unknown-linux-gnu 33 | name: superfreq-linux-amd64 34 | cross: false 35 | - os: ubuntu-latest 36 | target: aarch64-unknown-linux-gnu 37 | name: superfreq-linux-arm64 38 | cross: true 39 | 40 | runs-on: ${{ matrix.os }} 41 | steps: 42 | - uses: actions/checkout@v3 43 | 44 | - name: Install Rust 45 | uses: dtolnay/rust-toolchain@stable 46 | with: 47 | targets: ${{ matrix.target }} 48 | 49 | - name: Cache dependencies 50 | uses: Swatinem/rust-cache@v2 51 | 52 | - name: Setup cross-compilation (Linux ARM64) 53 | if: matrix.cross && matrix.os == 'ubuntu-latest' 54 | run: | 55 | sudo apt-get update 56 | sudo apt-get install -y gcc-aarch64-linux-gnu 57 | 58 | - name: Install cross 59 | if: matrix.cross 60 | uses: taiki-e/install-action@v2 61 | with: 62 | tool: cross 63 | 64 | - name: Build binary (native) 65 | if: ${{ !matrix.cross }} 66 | run: cargo build --release --target ${{ matrix.target }} 67 | 68 | - name: Build binary (cross) 69 | if: ${{ matrix.cross }} 70 | run: cross build --release --target ${{ matrix.target }} 71 | 72 | - name: Prepare binary 73 | run: | 74 | cp target/${{ matrix.target }}/release/tempus ${{ matrix.name }} 75 | 76 | - name: Upload Release Asset 77 | uses: softprops/action-gh-release@v2 78 | with: 79 | files: ${{ matrix.name }} 80 | 81 | generate-checksums: 82 | needs: [create-release, build-release] 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v3 86 | 87 | - name: Download Assets 88 | uses: robinraju/release-downloader@v1 89 | with: 90 | tag: ${{ github.ref_name }} 91 | fileName: "superfreq-*" 92 | out-file-path: "." 93 | 94 | - name: Generate checksums 95 | run: | 96 | sha256sum superfreq-* > SHA256SUMS 97 | 98 | - name: Upload Checksums 99 | uses: softprops/action-gh-release@v2 100 | with: 101 | token: ${{ secrets.GITHUB_TOKEN }} 102 | files: SHA256SUMS 103 | 104 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Create Tag from Crate Version 2 | 3 | concurrency: tag 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: [ "main" ] 9 | 10 | jobs: 11 | tag: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: cachix/install-nix-action@master 15 | with: 16 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Read Version 22 | run: | 23 | echo -n "version=v" >> "$GITHUB_ENV" 24 | nix run nixpkgs#fq -- -r ".package.version" Cargo.toml >> "$GITHUB_ENV" 25 | cat "$GITHUB_ENV" 26 | 27 | - name: Create Tag 28 | run: | 29 | set -x 30 | git tag $version 31 | git push --tags || : 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target* 2 | result* 3 | .direnv 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.7" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell", 61 | "windows-sys", 62 | ] 63 | 64 | [[package]] 65 | name = "anyhow" 66 | version = "1.0.98" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 69 | 70 | [[package]] 71 | name = "bitflags" 72 | version = "2.9.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 75 | 76 | [[package]] 77 | name = "cfg-if" 78 | version = "1.0.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 81 | 82 | [[package]] 83 | name = "cfg_aliases" 84 | version = "0.2.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 87 | 88 | [[package]] 89 | name = "clap" 90 | version = "4.5.38" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" 93 | dependencies = [ 94 | "clap_builder", 95 | "clap_derive", 96 | ] 97 | 98 | [[package]] 99 | name = "clap_builder" 100 | version = "4.5.38" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" 103 | dependencies = [ 104 | "anstream", 105 | "anstyle", 106 | "clap_lex", 107 | "strsim", 108 | ] 109 | 110 | [[package]] 111 | name = "clap_derive" 112 | version = "4.5.32" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 115 | dependencies = [ 116 | "heck", 117 | "proc-macro2", 118 | "quote", 119 | "syn", 120 | ] 121 | 122 | [[package]] 123 | name = "clap_lex" 124 | version = "0.7.4" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 127 | 128 | [[package]] 129 | name = "colorchoice" 130 | version = "1.0.3" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 133 | 134 | [[package]] 135 | name = "ctrlc" 136 | version = "3.4.7" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" 139 | dependencies = [ 140 | "nix", 141 | "windows-sys", 142 | ] 143 | 144 | [[package]] 145 | name = "dirs" 146 | version = "6.0.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 149 | dependencies = [ 150 | "dirs-sys", 151 | ] 152 | 153 | [[package]] 154 | name = "dirs-sys" 155 | version = "0.5.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 158 | dependencies = [ 159 | "libc", 160 | "option-ext", 161 | "redox_users", 162 | "windows-sys", 163 | ] 164 | 165 | [[package]] 166 | name = "env_filter" 167 | version = "0.1.3" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 170 | dependencies = [ 171 | "log", 172 | "regex", 173 | ] 174 | 175 | [[package]] 176 | name = "env_logger" 177 | version = "0.11.8" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 180 | dependencies = [ 181 | "anstream", 182 | "anstyle", 183 | "env_filter", 184 | "jiff", 185 | "log", 186 | ] 187 | 188 | [[package]] 189 | name = "equivalent" 190 | version = "1.0.2" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 193 | 194 | [[package]] 195 | name = "getrandom" 196 | version = "0.2.16" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 199 | dependencies = [ 200 | "cfg-if", 201 | "libc", 202 | "wasi", 203 | ] 204 | 205 | [[package]] 206 | name = "hashbrown" 207 | version = "0.15.3" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 210 | 211 | [[package]] 212 | name = "heck" 213 | version = "0.5.0" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 216 | 217 | [[package]] 218 | name = "hermit-abi" 219 | version = "0.3.9" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 222 | 223 | [[package]] 224 | name = "indexmap" 225 | version = "2.9.0" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 228 | dependencies = [ 229 | "equivalent", 230 | "hashbrown", 231 | ] 232 | 233 | [[package]] 234 | name = "is_terminal_polyfill" 235 | version = "1.70.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 238 | 239 | [[package]] 240 | name = "jiff" 241 | version = "0.2.13" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" 244 | dependencies = [ 245 | "jiff-static", 246 | "jiff-tzdb-platform", 247 | "log", 248 | "portable-atomic", 249 | "portable-atomic-util", 250 | "serde", 251 | "windows-sys", 252 | ] 253 | 254 | [[package]] 255 | name = "jiff-static" 256 | version = "0.2.13" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" 259 | dependencies = [ 260 | "proc-macro2", 261 | "quote", 262 | "syn", 263 | ] 264 | 265 | [[package]] 266 | name = "jiff-tzdb" 267 | version = "0.1.4" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" 270 | 271 | [[package]] 272 | name = "jiff-tzdb-platform" 273 | version = "0.1.3" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" 276 | dependencies = [ 277 | "jiff-tzdb", 278 | ] 279 | 280 | [[package]] 281 | name = "libc" 282 | version = "0.2.172" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 285 | 286 | [[package]] 287 | name = "libredox" 288 | version = "0.1.3" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 291 | dependencies = [ 292 | "bitflags", 293 | "libc", 294 | ] 295 | 296 | [[package]] 297 | name = "log" 298 | version = "0.4.27" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 301 | 302 | [[package]] 303 | name = "memchr" 304 | version = "2.7.4" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 307 | 308 | [[package]] 309 | name = "nix" 310 | version = "0.30.1" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 313 | dependencies = [ 314 | "bitflags", 315 | "cfg-if", 316 | "cfg_aliases", 317 | "libc", 318 | ] 319 | 320 | [[package]] 321 | name = "num_cpus" 322 | version = "1.16.0" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 325 | dependencies = [ 326 | "hermit-abi", 327 | "libc", 328 | ] 329 | 330 | [[package]] 331 | name = "once_cell" 332 | version = "1.21.3" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 335 | 336 | [[package]] 337 | name = "option-ext" 338 | version = "0.2.0" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 341 | 342 | [[package]] 343 | name = "portable-atomic" 344 | version = "1.11.0" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 347 | 348 | [[package]] 349 | name = "portable-atomic-util" 350 | version = "0.2.4" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 353 | dependencies = [ 354 | "portable-atomic", 355 | ] 356 | 357 | [[package]] 358 | name = "proc-macro2" 359 | version = "1.0.95" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 362 | dependencies = [ 363 | "unicode-ident", 364 | ] 365 | 366 | [[package]] 367 | name = "quote" 368 | version = "1.0.40" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 371 | dependencies = [ 372 | "proc-macro2", 373 | ] 374 | 375 | [[package]] 376 | name = "redox_users" 377 | version = "0.5.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 380 | dependencies = [ 381 | "getrandom", 382 | "libredox", 383 | "thiserror", 384 | ] 385 | 386 | [[package]] 387 | name = "regex" 388 | version = "1.11.1" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 391 | dependencies = [ 392 | "aho-corasick", 393 | "memchr", 394 | "regex-automata", 395 | "regex-syntax", 396 | ] 397 | 398 | [[package]] 399 | name = "regex-automata" 400 | version = "0.4.9" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 403 | dependencies = [ 404 | "aho-corasick", 405 | "memchr", 406 | "regex-syntax", 407 | ] 408 | 409 | [[package]] 410 | name = "regex-syntax" 411 | version = "0.8.5" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 414 | 415 | [[package]] 416 | name = "serde" 417 | version = "1.0.219" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 420 | dependencies = [ 421 | "serde_derive", 422 | ] 423 | 424 | [[package]] 425 | name = "serde_derive" 426 | version = "1.0.219" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 429 | dependencies = [ 430 | "proc-macro2", 431 | "quote", 432 | "syn", 433 | ] 434 | 435 | [[package]] 436 | name = "serde_spanned" 437 | version = "0.6.8" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 440 | dependencies = [ 441 | "serde", 442 | ] 443 | 444 | [[package]] 445 | name = "strsim" 446 | version = "0.11.1" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 449 | 450 | [[package]] 451 | name = "superfreq" 452 | version = "0.3.2" 453 | dependencies = [ 454 | "anyhow", 455 | "clap", 456 | "ctrlc", 457 | "dirs", 458 | "env_logger", 459 | "jiff", 460 | "log", 461 | "num_cpus", 462 | "serde", 463 | "thiserror", 464 | "toml", 465 | ] 466 | 467 | [[package]] 468 | name = "syn" 469 | version = "2.0.101" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 472 | dependencies = [ 473 | "proc-macro2", 474 | "quote", 475 | "unicode-ident", 476 | ] 477 | 478 | [[package]] 479 | name = "thiserror" 480 | version = "2.0.12" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 483 | dependencies = [ 484 | "thiserror-impl", 485 | ] 486 | 487 | [[package]] 488 | name = "thiserror-impl" 489 | version = "2.0.12" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 492 | dependencies = [ 493 | "proc-macro2", 494 | "quote", 495 | "syn", 496 | ] 497 | 498 | [[package]] 499 | name = "toml" 500 | version = "0.8.22" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" 503 | dependencies = [ 504 | "serde", 505 | "serde_spanned", 506 | "toml_datetime", 507 | "toml_edit", 508 | ] 509 | 510 | [[package]] 511 | name = "toml_datetime" 512 | version = "0.6.9" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" 515 | dependencies = [ 516 | "serde", 517 | ] 518 | 519 | [[package]] 520 | name = "toml_edit" 521 | version = "0.22.26" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" 524 | dependencies = [ 525 | "indexmap", 526 | "serde", 527 | "serde_spanned", 528 | "toml_datetime", 529 | "toml_write", 530 | "winnow", 531 | ] 532 | 533 | [[package]] 534 | name = "toml_write" 535 | version = "0.1.1" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" 538 | 539 | [[package]] 540 | name = "unicode-ident" 541 | version = "1.0.18" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 544 | 545 | [[package]] 546 | name = "utf8parse" 547 | version = "0.2.2" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 550 | 551 | [[package]] 552 | name = "wasi" 553 | version = "0.11.0+wasi-snapshot-preview1" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 556 | 557 | [[package]] 558 | name = "windows-sys" 559 | version = "0.59.0" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 562 | dependencies = [ 563 | "windows-targets", 564 | ] 565 | 566 | [[package]] 567 | name = "windows-targets" 568 | version = "0.52.6" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 571 | dependencies = [ 572 | "windows_aarch64_gnullvm", 573 | "windows_aarch64_msvc", 574 | "windows_i686_gnu", 575 | "windows_i686_gnullvm", 576 | "windows_i686_msvc", 577 | "windows_x86_64_gnu", 578 | "windows_x86_64_gnullvm", 579 | "windows_x86_64_msvc", 580 | ] 581 | 582 | [[package]] 583 | name = "windows_aarch64_gnullvm" 584 | version = "0.52.6" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 587 | 588 | [[package]] 589 | name = "windows_aarch64_msvc" 590 | version = "0.52.6" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 593 | 594 | [[package]] 595 | name = "windows_i686_gnu" 596 | version = "0.52.6" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 599 | 600 | [[package]] 601 | name = "windows_i686_gnullvm" 602 | version = "0.52.6" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 605 | 606 | [[package]] 607 | name = "windows_i686_msvc" 608 | version = "0.52.6" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 611 | 612 | [[package]] 613 | name = "windows_x86_64_gnu" 614 | version = "0.52.6" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 617 | 618 | [[package]] 619 | name = "windows_x86_64_gnullvm" 620 | version = "0.52.6" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 623 | 624 | [[package]] 625 | name = "windows_x86_64_msvc" 626 | version = "0.52.6" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 629 | 630 | [[package]] 631 | name = "winnow" 632 | version = "0.7.10" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" 635 | dependencies = [ 636 | "memchr", 637 | ] 638 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "superfreq" 3 | description = "Modern CPU frequency and power management utility for Linux" 4 | version = "0.3.2" 5 | edition = "2024" 6 | authors = ["NotAShelf "] 7 | rust-version = "1.85" 8 | 9 | [dependencies] 10 | serde = { version = "1.0", features = ["derive"] } 11 | toml = "0.8" 12 | dirs = "6.0" 13 | clap = { version = "4.0", features = ["derive"] } 14 | num_cpus = "1.16" 15 | ctrlc = "3.4" 16 | log = "0.4" 17 | env_logger = "0.11" 18 | thiserror = "2.0" 19 | anyhow = "1.0" 20 | jiff = "0.2.13" 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. “Contributor” 6 | means each individual or legal entity that creates, contributes to the 7 | creation of, or owns Covered Software. 8 | 9 | 1.2. “Contributor Version” 10 | means the combination of the Contributions of others (if any) used by a 11 | Contributor and that particular Contributor’s Contribution. 12 | 13 | 1.3. “Contribution” 14 | means Covered Software of a particular Contributor. 15 | 16 | 1.4. “Covered Software” 17 | means Source Code Form to which the initial Contributor has attached the 18 | notice in Exhibit A, the Executable Form of such Source Code Form, 19 | and Modifications of such Source Code Form, in each case 20 | including portions thereof. 21 | 22 | 1.5. “Incompatible With Secondary Licenses” 23 | means 24 | 25 | a. that the initial Contributor has attached the notice described 26 | in Exhibit B to the Covered Software; or 27 | 28 | b. that the Covered Software was made available under the terms of 29 | version 1.1 or earlier of the License, but not also under the terms 30 | of a Secondary License. 31 | 32 | 1.6. “Executable Form” 33 | means any form of the work other than Source Code Form. 34 | 35 | 1.7. “Larger Work” 36 | means a work that combines Covered Software with other material, 37 | in a separate file or files, that is not Covered Software. 38 | 39 | 1.8. “License” 40 | means this document. 41 | 42 | 1.9. “Licensable” 43 | means having the right to grant, to the maximum extent possible, 44 | whether at the time of the initial grant or subsequently, 45 | any and all of the rights conveyed by this License. 46 | 47 | 1.10. “Modifications” 48 | means any of the following: 49 | 50 | a. any file in Source Code Form that results from an addition to, 51 | deletion from, or modification of the contents of Covered Software; or 52 | 53 | b. any new file in Source Code Form that contains any Covered Software. 54 | 55 | 1.11. “Patent Claims” of a Contributor 56 | means any patent claim(s), including without limitation, method, process, 57 | and apparatus claims, in any patent Licensable by such Contributor that 58 | would be infringed, but for the grant of the License, by the making, 59 | using, selling, offering for sale, having made, import, or transfer of 60 | either its Contributions or its Contributor Version. 61 | 62 | 1.12. “Secondary License” 63 | means either the GNU General Public License, Version 2.0, the 64 | GNU Lesser General Public License, Version 2.1, the GNU Affero General 65 | Public License, Version 3.0, or any later versions of those licenses. 66 | 67 | 1.13. “Source Code Form” 68 | means the form of the work preferred for making modifications. 69 | 70 | 1.14. “You” (or “Your”) 71 | means an individual or a legal entity exercising rights under this License. 72 | For legal entities, “You” includes any entity that controls, 73 | is controlled by, or is under common control with You. For purposes of 74 | this definition, “control” means (a) the power, direct or indirect, 75 | to cause the direction or management of such entity, whether by contract 76 | or otherwise, or (b) ownership of more than fifty percent (50%) of the 77 | outstanding shares or beneficial ownership of such entity. 78 | 79 | 2. License Grants and Conditions 80 | 81 | 2.1. Grants 82 | Each Contributor hereby grants You a world-wide, royalty-free, 83 | non-exclusive license: 84 | 85 | a. under intellectual property rights (other than patent or trademark) 86 | Licensable by such Contributor to use, reproduce, make available, 87 | modify, display, perform, distribute, and otherwise exploit its 88 | Contributions, either on an unmodified basis, with Modifications, 89 | or as part of a Larger Work; and 90 | 91 | b. under Patent Claims of such Contributor to make, use, sell, 92 | offer for sale, have made, import, and otherwise transfer either 93 | its Contributions or its Contributor Version. 94 | 95 | 2.2. Effective Date 96 | The licenses granted in Section 2.1 with respect to any Contribution 97 | become effective for each Contribution on the date the Contributor 98 | first distributes such Contribution. 99 | 100 | 2.3. Limitations on Grant Scope 101 | The licenses granted in this Section 2 are the only rights granted 102 | under this License. No additional rights or licenses will be implied 103 | from the distribution or licensing of Covered Software under this License. 104 | Notwithstanding Section 2.1(b) above, no patent license is granted 105 | by a Contributor: 106 | 107 | a. for any code that a Contributor has removed from 108 | Covered Software; or 109 | 110 | b. for infringements caused by: (i) Your and any other third party’s 111 | modifications of Covered Software, or (ii) the combination of its 112 | Contributions with other software (except as part of its 113 | Contributor Version); or 114 | 115 | c. under Patent Claims infringed by Covered Software in the 116 | absence of its Contributions. 117 | 118 | This License does not grant any rights in the trademarks, service marks, 119 | or logos of any Contributor (except as may be necessary to comply with 120 | the notice requirements in Section 3.4). 121 | 122 | 2.4. Subsequent Licenses 123 | No Contributor makes additional grants as a result of Your choice to 124 | distribute the Covered Software under a subsequent version of this 125 | License (see Section 10.2) or under the terms of a Secondary License 126 | (if permitted under the terms of Section 3.3). 127 | 128 | 2.5. Representation 129 | Each Contributor represents that the Contributor believes its 130 | Contributions are its original creation(s) or it has sufficient rights 131 | to grant the rights to its Contributions conveyed by this License. 132 | 133 | 2.6. Fair Use 134 | This License is not intended to limit any rights You have under 135 | applicable copyright doctrines of fair use, fair dealing, 136 | or other equivalents. 137 | 138 | 2.7. Conditions 139 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the 140 | licenses granted in Section 2.1. 141 | 142 | 3. Responsibilities 143 | 144 | 3.1. Distribution of Source Form 145 | All distribution of Covered Software in Source Code Form, including 146 | any Modifications that You create or to which You contribute, must be 147 | under the terms of this License. You must inform recipients that the 148 | Source Code Form of the Covered Software is governed by the terms 149 | of this License, and how they can obtain a copy of this License. 150 | You may not attempt to alter or restrict the recipients’ rights 151 | in the Source Code Form. 152 | 153 | 3.2. Distribution of Executable Form 154 | If You distribute Covered Software in Executable Form then: 155 | 156 | a. such Covered Software must also be made available in Source Code 157 | Form, as described in Section 3.1, and You must inform recipients of 158 | the Executable Form how they can obtain a copy of such Source Code 159 | Form by reasonable means in a timely manner, at a charge no more than 160 | the cost of distribution to the recipient; and 161 | 162 | b. You may distribute such Executable Form under the terms of this 163 | License, or sublicense it under different terms, provided that the 164 | license for the Executable Form does not attempt to limit or alter 165 | the recipients’ rights in the Source Code Form under this License. 166 | 167 | 3.3. Distribution of a Larger Work 168 | You may create and distribute a Larger Work under terms of Your choice, 169 | provided that You also comply with the requirements of this License for 170 | the Covered Software. If the Larger Work is a combination of 171 | Covered Software with a work governed by one or more Secondary Licenses, 172 | and the Covered Software is not Incompatible With Secondary Licenses, 173 | this License permits You to additionally distribute such Covered Software 174 | under the terms of such Secondary License(s), so that the recipient of 175 | the Larger Work may, at their option, further distribute the 176 | Covered Software under the terms of either this License or such 177 | Secondary License(s). 178 | 179 | 3.4. Notices 180 | You may not remove or alter the substance of any license notices 181 | (including copyright notices, patent notices, disclaimers of warranty, 182 | or limitations of liability) contained within the Source Code Form of 183 | the Covered Software, except that You may alter any license notices to 184 | the extent required to remedy known factual inaccuracies. 185 | 186 | 3.5. Application of Additional Terms 187 | You may choose to offer, and to charge a fee for, warranty, support, 188 | indemnity or liability obligations to one or more recipients of 189 | Covered Software. However, You may do so only on Your own behalf, 190 | and not on behalf of any Contributor. You must make it absolutely clear 191 | that any such warranty, support, indemnity, or liability obligation is 192 | offered by You alone, and You hereby agree to indemnify every Contributor 193 | for any liability incurred by such Contributor as a result of warranty, 194 | support, indemnity or liability terms You offer. You may include 195 | additional disclaimers of warranty and limitations of liability 196 | specific to any jurisdiction. 197 | 198 | 4. Inability to Comply Due to Statute or Regulation 199 | 200 | If it is impossible for You to comply with any of the terms of this License 201 | with respect to some or all of the Covered Software due to statute, 202 | judicial order, or regulation then You must: (a) comply with the terms of 203 | this License to the maximum extent possible; and (b) describe the limitations 204 | and the code they affect. Such description must be placed in a text file 205 | included with all distributions of the Covered Software under this License. 206 | Except to the extent prohibited by statute or regulation, such description 207 | must be sufficiently detailed for a recipient of ordinary skill 208 | to be able to understand it. 209 | 210 | 5. Termination 211 | 212 | 5.1. The rights granted under this License will terminate automatically 213 | if You fail to comply with any of its terms. However, if You become 214 | compliant, then the rights granted under this License from a particular 215 | Contributor are reinstated (a) provisionally, unless and until such 216 | Contributor explicitly and finally terminates Your grants, and (b) on an 217 | ongoing basis, if such Contributor fails to notify You of the 218 | non-compliance by some reasonable means prior to 60 days after You have 219 | come back into compliance. Moreover, Your grants from a particular 220 | Contributor are reinstated on an ongoing basis if such Contributor 221 | notifies You of the non-compliance by some reasonable means, 222 | this is the first time You have received notice of non-compliance with 223 | this License from such Contributor, and You become compliant prior to 224 | 30 days after Your receipt of the notice. 225 | 226 | 5.2. If You initiate litigation against any entity by asserting a patent 227 | infringement claim (excluding declaratory judgment actions, 228 | counter-claims, and cross-claims) alleging that a Contributor Version 229 | directly or indirectly infringes any patent, then the rights granted 230 | to You by any and all Contributors for the Covered Software under 231 | Section 2.1 of this License shall terminate. 232 | 233 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 234 | end user license agreements (excluding distributors and resellers) which 235 | have been validly granted by You or Your distributors under this License 236 | prior to termination shall survive termination. 237 | 238 | 6. Disclaimer of Warranty 239 | 240 | Covered Software is provided under this License on an “as is” basis, without 241 | warranty of any kind, either expressed, implied, or statutory, including, 242 | without limitation, warranties that the Covered Software is free of defects, 243 | merchantable, fit for a particular purpose or non-infringing. The entire risk 244 | as to the quality and performance of the Covered Software is with You. 245 | Should any Covered Software prove defective in any respect, You 246 | (not any Contributor) assume the cost of any necessary servicing, repair, 247 | or correction. This disclaimer of warranty constitutes an essential part of 248 | this License. No use of any Covered Software is authorized under this 249 | License except under this disclaimer. 250 | 251 | 7. Limitation of Liability 252 | 253 | Under no circumstances and under no legal theory, whether tort 254 | (including negligence), contract, or otherwise, shall any Contributor, or 255 | anyone who distributes Covered Software as permitted above, be liable to 256 | You for any direct, indirect, special, incidental, or consequential damages 257 | of any character including, without limitation, damages for lost profits, 258 | loss of goodwill, work stoppage, computer failure or malfunction, or any and 259 | all other commercial damages or losses, even if such party shall have been 260 | informed of the possibility of such damages. This limitation of liability 261 | shall not apply to liability for death or personal injury resulting from 262 | such party’s negligence to the extent applicable law prohibits such 263 | limitation. Some jurisdictions do not allow the exclusion or limitation of 264 | incidental or consequential damages, so this exclusion and limitation may 265 | not apply to You. 266 | 267 | 8. Litigation 268 | 269 | Any litigation relating to this License may be brought only in the courts of 270 | a jurisdiction where the defendant maintains its principal place of business 271 | and such litigation shall be governed by laws of that jurisdiction, without 272 | reference to its conflict-of-law provisions. Nothing in this Section shall 273 | prevent a party’s ability to bring cross-claims or counter-claims. 274 | 275 | 9. Miscellaneous 276 | 277 | This License represents the complete agreement concerning the subject matter 278 | hereof. If any provision of this License is held to be unenforceable, 279 | such provision shall be reformed only to the extent necessary to make it 280 | enforceable. Any law or regulation which provides that the language of a 281 | contract shall be construed against the drafter shall not be used to construe 282 | this License against a Contributor. 283 | 284 | 10. Versions of the License 285 | 286 | 10.1. New Versions 287 | Mozilla Foundation is the license steward. Except as provided in 288 | Section 10.3, no one other than the license steward has the right to 289 | modify or publish new versions of this License. Each version will be 290 | given a distinguishing version number. 291 | 292 | 10.2. Effect of New Versions 293 | You may distribute the Covered Software under the terms of the version 294 | of the License under which You originally received the Covered Software, 295 | or under the terms of any subsequent version published 296 | by the license steward. 297 | 298 | 10.3. Modified Versions 299 | If you create software not governed by this License, and you want to 300 | create a new license for such software, you may create and use a modified 301 | version of this License if you rename the license and remove any 302 | references to the name of the license steward (except to note that such 303 | modified license differs from this License). 304 | 305 | 10.4. Distributing Source Code Form that is 306 | Incompatible With Secondary Licenses 307 | If You choose to distribute Source Code Form that is 308 | Incompatible With Secondary Licenses under the terms of this version of 309 | the License, the notice described in Exhibit B of this 310 | License must be attached. 311 | 312 | Exhibit A - Source Code Form License Notice 313 | 314 | This Source Code Form is subject to the terms of the 315 | Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed 316 | with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 317 | 318 | If it is not possible or desirable to put the notice in a particular file, 319 | then You may include the notice in a location (such as a LICENSE file in a 320 | relevant directory) where a recipient would be likely to 321 | look for such a notice. 322 | 323 | You may add additional accurate notices of copyright ownership. 324 | 325 | Exhibit B - “Incompatible With Secondary Licenses” Notice 326 | 327 | This Source Code Form is “Incompatible With Secondary Licenses”, 328 | as defined by the Mozilla Public License, v. 2.0. 329 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Superfreq 3 |

4 | 5 |
6 | Modern, transparent and intelligent utility for CPU management on Linux. 7 |
8 | 9 |
10 |
11 | Synopsis
12 | Features | Usage
13 | Contributing 14 |
15 |
16 | 17 | ## What is Superfreq 18 | 19 | Superfreq is a modern CPU frequency and power management utility for Linux 20 | systems. It provides intelligent control of CPU governors, frequencies, and 21 | power-saving features, helping optimize both performance and battery life. 22 | 23 | It is greatly inspired by auto-cpufreq, but rewritten from ground up to provide 24 | a smoother experience with a more efficient and more correct codebase. Some 25 | features are omitted, and it is _not_ a drop-in replacement for auto-cpufreq, 26 | but most common usecases are already implemented. 27 | 28 | ## Features 29 | 30 | - **Real-time CPU Management**: Monitor and control CPU governors, frequencies, 31 | and turbo boost 32 | - **Intelligent Power Management**: Different profiles for AC and battery 33 | operation 34 | - **Dynamic Turbo Boost Control**: Automatically enables/disables turbo based on 35 | CPU load and temperature 36 | - **Fine-tuned Controls**: Adjust energy performance preferences, biases, and 37 | frequency limits 38 | - **Per-core Control**: Apply settings globally or to specific CPU cores 39 | - **Battery Management**: Monitor battery status and power consumption 40 | - **System Load Tracking**: Track system load and make intelligent decisions 41 | - **Daemon Mode**: Run in background with adaptive polling to minimize overhead 42 | - **Conflict Detection**: Identifies and warns about conflicts with other power 43 | management tools 44 | 45 | ## Usage 46 | 47 | ### Basic Commands 48 | 49 | ```bash 50 | # Show current system information 51 | superfreq info 52 | 53 | # Run as a daemon in the background 54 | sudo superfreq daemon 55 | 56 | # Run with verbose logging 57 | sudo superfreq daemon --verbose 58 | 59 | # Display comprehensive debug information 60 | superfreq debug 61 | ``` 62 | 63 | ### CPU Governor Control 64 | 65 | ```bash 66 | # Set CPU governor for all cores 67 | sudo superfreq set-governor performance 68 | 69 | # Set CPU governor for a specific core 70 | sudo superfreq set-governor powersave --core-id 0 71 | 72 | # Force a specific governor mode persistently 73 | sudo superfreq force-governor performance 74 | ``` 75 | 76 | ### Turbo Boost Management 77 | 78 | ```bash 79 | # Always enable turbo boost 80 | sudo superfreq set-turbo always 81 | 82 | # Disable turbo boost 83 | sudo superfreq set-turbo never 84 | 85 | # Let Superfreq manage turbo boost based on conditions 86 | sudo superfreq set-turbo auto 87 | ``` 88 | 89 | ### Power and Performance Settings 90 | 91 | ```bash 92 | # Set Energy Performance Preference (EPP) 93 | sudo superfreq set-epp performance 94 | 95 | # Set Energy Performance Bias (EPB) 96 | sudo superfreq set-epb 4 97 | 98 | # Set ACPI platform profile 99 | sudo superfreq set-platform-profile balanced 100 | ``` 101 | 102 | ### Frequency Control 103 | 104 | ```bash 105 | # Set minimum CPU frequency (in MHz) 106 | sudo superfreq set-min-freq 800 107 | 108 | # Set maximum CPU frequency (in MHz) 109 | sudo superfreq set-max-freq 3000 110 | 111 | # Set per-core frequency limits 112 | sudo superfreq set-min-freq 1200 --core-id 0 113 | sudo superfreq set-max-freq 2800 --core-id 1 114 | ``` 115 | 116 | ### Battery Management 117 | 118 | ```bash 119 | # Set battery charging thresholds to extend battery lifespan 120 | sudo superfreq set-battery-thresholds 40 80 # Start charging at 40%, stop at 80% 121 | ``` 122 | 123 | Battery charging thresholds help extend battery longevity by preventing constant 124 | charging to 100%. Different laptop vendors implement this feature differently, 125 | but Superfreq attempts to support multiple vendor implementations including: 126 | 127 | - Lenovo ThinkPad/IdeaPad (Standard implementation) 128 | - ASUS laptops 129 | - Huawei laptops 130 | - Other devices using the standard Linux power_supply API 131 | 132 | Note that battery management is sensitive, and that your mileage may vary. 133 | Please open an issue if your vendor is not supported, but patches would help 134 | more than issue reports, as supporting hardware _needs_ hardware. 135 | 136 | ## Configuration 137 | 138 | Superfreq uses TOML configuration files. Default locations: 139 | 140 | - `/etc/xdg/superfreq/config.toml` 141 | - `/etc/superfreq.toml` 142 | 143 | You can also specify a custom path by setting the `SUPERFREQ_CONFIG` environment 144 | variable. 145 | 146 | ### Sample Configuration 147 | 148 | ```toml 149 | # Settings for when connected to a power source 150 | [charger] 151 | # CPU governor to use 152 | governor = "performance" 153 | # Turbo boost setting: "always", "auto", or "never" 154 | turbo = "auto" 155 | # Enable or disable automatic turbo management (when turbo = "auto") 156 | enable_auto_turbo = true 157 | # Custom thresholds for auto turbo management 158 | turbo_auto_settings = { 159 | load_threshold_high = 70.0, 160 | load_threshold_low = 30.0, 161 | temp_threshold_high = 75.0, 162 | initial_turbo_state = false, # whether turbo should be initially enabled (false = disabled) 163 | } 164 | # Energy Performance Preference 165 | epp = "performance" 166 | # Energy Performance Bias (0-15 scale or named value) 167 | epb = "balance_performance" 168 | # Platform profile (if supported) 169 | platform_profile = "performance" 170 | # Min/max frequency in MHz (optional) 171 | min_freq_mhz = 800 172 | max_freq_mhz = 3500 173 | # Optional: Profile-specific battery charge thresholds (overrides global setting) 174 | # battery_charge_thresholds = [40, 80] # Start at 40%, stop at 80% 175 | 176 | # Settings for when on battery power 177 | [battery] 178 | governor = "powersave" 179 | turbo = "auto" 180 | # More conservative auto turbo settings on battery 181 | enable_auto_turbo = true 182 | turbo_auto_settings = { 183 | load_threshold_high = 80.0, 184 | load_threshold_low = 40.0, 185 | temp_threshold_high = 70.0, 186 | initial_turbo_state = false, # start with turbo disabled on battery for power savings 187 | } 188 | epp = "power" 189 | epb = "balance_power" 190 | platform_profile = "low-power" 191 | min_freq_mhz = 800 192 | max_freq_mhz = 2500 193 | # Optional: Profile-specific battery charge thresholds (overrides global setting) 194 | # battery_charge_thresholds = [60, 80] # Start at 60%, stop at 80% (more conservative) 195 | 196 | # Global battery charging thresholds (applied to both profiles unless overridden) 197 | # Start charging at 40%, stop at 80% - extends battery lifespan 198 | # NOTE: Profile-specific thresholds (in [charger] or [battery] sections) 199 | # take precedence over this global setting 200 | battery_charge_thresholds = [40, 80] 201 | 202 | # Daemon configuration 203 | [daemon] 204 | # Base polling interval in seconds 205 | poll_interval_sec = 5 206 | # Enable adaptive polling that changes with system state 207 | adaptive_interval = true 208 | # Minimum polling interval for adaptive polling (seconds) 209 | min_poll_interval_sec = 1 210 | # Maximum polling interval for adaptive polling (seconds) 211 | max_poll_interval_sec = 30 212 | # Double the polling interval when on battery to save power 213 | throttle_on_battery = true 214 | # Logging level: Error, Warning, Info, Debug 215 | log_level = "Info" 216 | # Optional stats file path 217 | stats_file_path = "/var/run/superfreq-stats" 218 | 219 | # Optional: List of power supplies to ignore 220 | [power_supply_ignore_list] 221 | mouse_battery = "hid-12:34:56:78:90:ab-battery" 222 | # Add other devices to ignore here 223 | ``` 224 | 225 | ## Advanced Features 226 | 227 | Those are the more advanced features of Superfreq that some users might be more 228 | inclined to use than others. If you have a use-case that is not covered, please 229 | create an issue. 230 | 231 | ### Dynamic Turbo Boost Management 232 | 233 | When using `turbo = "auto"` with `enable_auto_turbo = true`, Superfreq 234 | dynamically controls CPU turbo boost based on: 235 | 236 | - **CPU Load Thresholds**: Enables turbo when load exceeds `load_threshold_high` 237 | (default 70%), disables when below `load_threshold_low` (default 30%) 238 | - **Temperature Protection**: Automatically disables turbo when CPU temperature 239 | exceeds `temp_threshold_high` (default 75°C) 240 | - **Hysteresis Control**: Prevents rapid toggling by maintaining previous state 241 | when load is between thresholds 242 | - **Configurable Initial State**: Sets the initial turbo state via 243 | `initial_turbo_state` (default: disabled) before system load data is available 244 | - **Profile-Specific Settings**: Configure different thresholds for battery vs. 245 | AC power 246 | 247 | This feature optimizes performance and power consumption by providing maximum 248 | performance for demanding tasks while conserving energy during light workloads. 249 | 250 | > [!TIP] 251 | > You can disable this logic with `enable_auto_turbo = false` to let the system 252 | > handle turbo boost natively when `turbo = "auto"`. 253 | 254 | #### Turbo Boost Behavior Table 255 | 256 | The table below explains how different combinations of `turbo` and 257 | `enable_auto_turbo` settings affect CPU turbo behavior: 258 | 259 | | Setting | `enable_auto_turbo = true` | `enable_auto_turbo = false` | 260 | | ------------------ | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | 261 | | `turbo = "always"` | **Always enabled**
Turbo is always active regardless of CPU load or temperature | **Always enabled**
Turbo is always active regardless of CPU load or temperature | 262 | | `turbo = "never"` | **Always disabled**
Turbo is always disabled regardless of CPU load or temperature | **Always disabled**
Turbo is always disabled regardless of CPU load or temperature | 263 | | `turbo = "auto"` | **Dynamically managed**
Superfreq enables/disables turbo based on CPU load and temperature thresholds | **System default**
Turbo is reset to system's default enabled state and is managed by the hardware/kernel | 264 | 265 | > [!NOTE] 266 | > When `turbo = "auto"` and `enable_auto_turbo = false`, Superfreq ensures that 267 | > any previous turbo state restrictions are removed, allowing the 268 | > hardware/kernel to manage turbo behavior according to its default algorithms. 269 | 270 | ### Adaptive Polling 271 | 272 | Superfreq includes a "sophisticated" (euphemism for complicated) adaptive 273 | polling system to try and maximize power efficiency 274 | 275 | - **Battery Discharge Analysis** - Automatically adjusts polling frequency based 276 | on the battery discharge rate, reducing system activity when battery is 277 | draining quickly 278 | - **System Activity Pattern Recognition** - Monitors CPU usage and temperature 279 | patterns to identify system stability 280 | - **Dynamic Interval Calculation** - Uses multiple factors to determine optimal 281 | polling intervals - up to 3x longer on battery with minimal user impact 282 | - **Idle Detection** - Significantly reduces polling frequency during extended 283 | idle periods to minimize power consumption 284 | - **Gradual Transition** - Smooth transitions between polling rates to avoid 285 | performance spikes 286 | - **Progressive Back-off** - Implements logarithmic back-off during idle periods 287 | (1min -> 1.5x, 2min -> 2x, 4min -> 3x, 8min -> 4x, 16min -> 5x) 288 | - **Battery Discharge Protection** - Includes safeguards against measurement 289 | noise to prevent erratic polling behavior 290 | 291 | When enabled, this intelligent polling system provides substantial power savings 292 | over conventional fixed-interval approaches, especially during low-activity or 293 | idle periods, while maintaining responsiveness when needed. 294 | 295 | ### Power Supply Filtering 296 | 297 | Configure Superfreq to ignore certain power supplies (like peripheral batteries) 298 | that might interfere with power state detection. 299 | 300 | ## Troubleshooting 301 | 302 | ### Permission Issues 303 | 304 | Most CPU management commands require root privileges. If you see permission 305 | errors, try running with `sudo`. 306 | 307 | ### Feature Compatibility 308 | 309 | Not all features are available on all hardware: 310 | 311 | - Turbo boost control requires CPU support for Intel/AMD boost features 312 | - EPP/EPB settings require CPU driver support 313 | - Platform profiles require ACPI platform profile support in your hardware 314 | 315 | ### Common Problems 316 | 317 | 1. **Settings not applying**: Check for conflicts with other power management 318 | tools 319 | 2. **CPU frequencies fluctuating**: May be due to thermal throttling 320 | 3. **Missing CPU information**: Verify kernel module support for your CPU 321 | 322 | While reporting issues, please attach the results from `superfreq debug`. 323 | 324 | ## Contributing 325 | 326 | Contributions to Superfreq are always welcome! Whether it's bug reports, feature 327 | requests, or code contributions, please feel free to contribute. 328 | 329 | > [!NOTE] 330 | > If you are looking to reimplement features from auto-cpufreq, please consider 331 | > opening an issue first and let us know what you have in mind. Certain features 332 | > (such as the system tray) are deliberately ignored, and might not be desired 333 | > in the codebase as they stand. Please discuss those features with us first :) 334 | 335 | ### Setup 336 | 337 | You will need Cargo and Rust installed on your system. Rust 1.85 or later is 338 | required. 339 | 340 | A `.envrc` is provided, and it's usage is encouraged for Nix users. 341 | Alternatively, you may use Nix for a reproducible developer environment 342 | 343 | ```bash 344 | nix develop 345 | ``` 346 | 347 | Non-Nix users may get the appropriate Cargo and Rust versions from their package 348 | manager, or using something like Rustup. 349 | 350 | ### Formatting & Lints 351 | 352 | Please make sure to run _at least_ `cargo fmt` inside the repository to make 353 | sure all of your code is properly formatted. For Nix code, please use Alejandra. 354 | 355 | Clippy lints are not _required_ as of now, but a good rule of thumb to run them 356 | before committing to catch possible code smell early. 357 | 358 | ## License 359 | 360 | Superfreq is available under [Mozilla Public License v2.0](LICENSE) for your 361 | convenience, and at our expense. Please see the license file for more details. 362 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1746904237, 6 | "narHash": "sha256-3e+AVBczosP5dCLQmMoMEogM57gmZ2qrVSrmq9aResQ=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "d89fc19e405cb2d55ce7cc114356846a0ee5e956", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable"; 3 | 4 | outputs = { 5 | self, 6 | nixpkgs, 7 | ... 8 | } @ inputs: let 9 | forAllSystems = nixpkgs.lib.genAttrs ["x86_64-linux" "aarch64-linux"]; 10 | pkgsForEach = forAllSystems (system: 11 | import nixpkgs { 12 | localSystem.system = system; 13 | overlays = [self.overlays.default]; 14 | }); 15 | in { 16 | overlays = { 17 | superfreq = final: _: { 18 | superfreq = final.callPackage ./nix/package.nix {}; 19 | }; 20 | default = self.overlays.superfreq; 21 | }; 22 | 23 | packages = 24 | nixpkgs.lib.mapAttrs (system: pkgs: { 25 | inherit (pkgs) superfreq; 26 | default = self.packages.${system}.superfreq; 27 | }) 28 | pkgsForEach; 29 | 30 | devShells = 31 | nixpkgs.lib.mapAttrs (system: pkgs: { 32 | default = pkgs.callPackage ./nix/shell.nix {}; 33 | }) 34 | pkgsForEach; 35 | 36 | nixosModules = { 37 | superfreq = import ./nix/module.nix inputs; 38 | default = self.nixosModules.superfreq; 39 | }; 40 | 41 | formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.alejandra); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /nix/module.nix: -------------------------------------------------------------------------------- 1 | inputs: { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib.modules) mkIf; 8 | inherit (lib.options) mkOption mkEnableOption mkPackageOption; 9 | inherit (lib.types) submodule; 10 | inherit (lib.lists) optional; 11 | inherit (lib.meta) getExe; 12 | 13 | cfg = config.services.superfreq; 14 | 15 | format = pkgs.formats.toml {}; 16 | cfgFile = format.generate "superfreq-config.toml" cfg.settings; 17 | in { 18 | options.services.superfreq = { 19 | enable = mkEnableOption "Automatic CPU speed & power optimizer for Linux"; 20 | package = mkPackageOption inputs.self.packages.${pkgs.stdenv.system} "superfreq" { 21 | pkgsText = "self.packages.\${pkgs.stdenv.system}"; 22 | }; 23 | 24 | settings = mkOption { 25 | default = {}; 26 | type = submodule {freeformType = format.type;}; 27 | description = "Configuration for Superfreq."; 28 | }; 29 | }; 30 | 31 | config = mkIf cfg.enable { 32 | environment.systemPackages = [cfg.package]; 33 | 34 | # This is necessary for the Superfreq CLI. The environment variable 35 | # passed to the systemd service will take priority in read order. 36 | environment.etc."superfreq.toml".source = cfgFile; 37 | 38 | systemd.services.superfreq = { 39 | wantedBy = ["multi-user.target"]; 40 | conflicts = [ 41 | "auto-cpufreq.service" 42 | "power-profiles-daemon.service" 43 | "tlp.service" 44 | "cpupower-gui.service" 45 | "thermald.service" 46 | ]; 47 | serviceConfig = { 48 | Environment = optional (cfg.settings != {}) ["SUPERFREQ_CONFIG=${cfgFile}"]; 49 | WorkingDirectory = ""; 50 | ExecStart = "${getExe cfg.package} daemon --verbose"; 51 | Restart = "on-failure"; 52 | 53 | RuntimeDirectory = "superfreq"; 54 | RuntimeDirectoryMode = "0755"; 55 | }; 56 | }; 57 | 58 | assertions = [ 59 | { 60 | assertion = !config.services.power-profiles-daemon.enable; 61 | message = '' 62 | You have set services.power-profiles-daemon.enable = true; 63 | which conflicts with Superfreq. 64 | ''; 65 | } 66 | { 67 | assertion = !config.services.auto-cpufreq.enable; 68 | message = '' 69 | You have set services.auto-cpufreq.enable = true; 70 | which conflicts with Superfreq. 71 | ''; 72 | } 73 | ]; 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /nix/package.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | rustPlatform, 4 | }: let 5 | fs = lib.fileset; 6 | in 7 | rustPlatform.buildRustPackage (finalAttrs: { 8 | pname = "superfreq"; 9 | version = "0.1.0"; 10 | 11 | src = fs.toSource { 12 | root = ../.; 13 | fileset = fs.unions [ 14 | (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) ../src) 15 | ../Cargo.lock 16 | ../Cargo.toml 17 | ]; 18 | }; 19 | 20 | cargoLock.lockFile = "${finalAttrs.src}/Cargo.lock"; 21 | useFetchCargoVendor = true; 22 | enableParallelBuilding = true; 23 | 24 | meta = { 25 | description = "Automatic CPU speed & power optimizer for Linux"; 26 | longDescription = '' 27 | Superfreq is a CPU speed & power optimizer for Linux. It uses 28 | the CPU frequency scaling driver to set the CPU frequency 29 | governor and the CPU power management driver to set the CPU 30 | power management mode. 31 | 32 | ''; 33 | homepage = "https://github.com/NotAShelf/superfreq"; 34 | mainProgram = "superfreq"; 35 | license = lib.licenses.mpl20; 36 | platforms = lib.platforms.linux; 37 | }; 38 | }) 39 | -------------------------------------------------------------------------------- /nix/shell.nix: -------------------------------------------------------------------------------- 1 | { 2 | mkShell, 3 | rust-analyzer-unwrapped, 4 | rustfmt, 5 | clippy, 6 | cargo, 7 | rustc, 8 | rustPlatform, 9 | }: 10 | mkShell { 11 | packages = [ 12 | cargo 13 | clippy 14 | rustc 15 | rustfmt 16 | rust-analyzer-unwrapped 17 | ]; 18 | 19 | env.RUST_SRC_PATH = "${rustPlatform.rustLibSrc}"; 20 | } 21 | -------------------------------------------------------------------------------- /src/battery.rs: -------------------------------------------------------------------------------- 1 | use crate::{config::types::BatteryChargeThresholds, util::error::ControlError, util::sysfs}; 2 | use log::{debug, warn}; 3 | use std::{ 4 | fs, io, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | pub type Result = std::result::Result; 9 | 10 | /// Represents a pattern of path suffixes used to control battery charge thresholds 11 | /// for different device vendors. 12 | #[derive(Clone)] 13 | pub struct ThresholdPathPattern { 14 | pub description: &'static str, 15 | pub start_path: &'static str, 16 | pub stop_path: &'static str, 17 | } 18 | 19 | // Threshold patterns 20 | const THRESHOLD_PATTERNS: &[ThresholdPathPattern] = &[ 21 | ThresholdPathPattern { 22 | description: "Standard", 23 | start_path: "charge_control_start_threshold", 24 | stop_path: "charge_control_end_threshold", 25 | }, 26 | ThresholdPathPattern { 27 | description: "ASUS", 28 | start_path: "charge_control_start_percentage", 29 | stop_path: "charge_control_end_percentage", 30 | }, 31 | // Combine Huawei and ThinkPad since they use identical paths 32 | ThresholdPathPattern { 33 | description: "ThinkPad/Huawei", 34 | start_path: "charge_start_threshold", 35 | stop_path: "charge_stop_threshold", 36 | }, 37 | // Framework laptop support 38 | ThresholdPathPattern { 39 | description: "Framework", 40 | start_path: "charge_behaviour_start_threshold", 41 | stop_path: "charge_behaviour_end_threshold", 42 | }, 43 | ]; 44 | 45 | /// Represents a battery that supports charge threshold control 46 | pub struct SupportedBattery<'a> { 47 | pub name: String, 48 | pub pattern: &'a ThresholdPathPattern, 49 | pub path: PathBuf, 50 | } 51 | 52 | /// Set battery charge thresholds to protect battery health 53 | /// 54 | /// This sets the start and stop charging thresholds for batteries that support this feature. 55 | /// Different laptop vendors implement battery thresholds in different ways, so this function 56 | /// attempts to handle multiple implementations (Lenovo, ASUS, etc.). 57 | /// 58 | /// The thresholds determine at what percentage the battery starts charging (when below `start_threshold`) 59 | /// and at what percentage it stops (when it reaches `stop_threshold`). 60 | /// 61 | /// # Arguments 62 | /// 63 | /// * `start_threshold` - The battery percentage at which charging should start (typically 0-99) 64 | /// * `stop_threshold` - The battery percentage at which charging should stop (typically 1-100) 65 | /// 66 | /// # Errors 67 | /// 68 | /// Returns an error if: 69 | /// - The thresholds are invalid (start >= stop or stop > 100) 70 | /// - No power supply path is found 71 | /// - No batteries with threshold support are found 72 | /// - Failed to set thresholds on any battery 73 | pub fn set_battery_charge_thresholds(start_threshold: u8, stop_threshold: u8) -> Result<()> { 74 | // Validate thresholds using `BatteryChargeThresholds` 75 | let thresholds = 76 | BatteryChargeThresholds::new(start_threshold, stop_threshold).map_err(|e| match e { 77 | crate::config::types::ConfigError::Validation(msg) => { 78 | ControlError::InvalidValueError(msg) 79 | } 80 | _ => ControlError::InvalidValueError(format!("Invalid battery threshold values: {e}")), 81 | })?; 82 | 83 | let power_supply_path = Path::new("/sys/class/power_supply"); 84 | if !power_supply_path.exists() { 85 | return Err(ControlError::NotSupported( 86 | "Power supply path not found, battery threshold control not supported".to_string(), 87 | )); 88 | } 89 | 90 | // XXX: Skip checking directory writability since /sys is a virtual filesystem 91 | // Individual file writability will be checked by find_battery_with_threshold_support 92 | 93 | let supported_batteries = find_supported_batteries(power_supply_path)?; 94 | if supported_batteries.is_empty() { 95 | return Err(ControlError::NotSupported( 96 | "No batteries with charge threshold control support found".to_string(), 97 | )); 98 | } 99 | 100 | apply_thresholds_to_batteries(&supported_batteries, thresholds.start, thresholds.stop) 101 | } 102 | 103 | /// Finds all batteries in the system that support threshold control 104 | fn find_supported_batteries(power_supply_path: &Path) -> Result>> { 105 | let entries = fs::read_dir(power_supply_path).map_err(|e| { 106 | if e.kind() == io::ErrorKind::PermissionDenied { 107 | ControlError::PermissionDenied(format!( 108 | "Permission denied accessing power supply directory: {}", 109 | power_supply_path.display() 110 | )) 111 | } else { 112 | ControlError::Io(e) 113 | } 114 | })?; 115 | 116 | let mut supported_batteries = Vec::new(); 117 | for entry in entries { 118 | let entry = match entry { 119 | Ok(e) => e, 120 | Err(e) => { 121 | warn!("Failed to read power-supply entry: {e}"); 122 | continue; 123 | } 124 | }; 125 | let ps_path = entry.path(); 126 | if is_battery(&ps_path)? { 127 | if let Some(battery) = find_battery_with_threshold_support(&ps_path) { 128 | supported_batteries.push(battery); 129 | } 130 | } 131 | } 132 | 133 | if supported_batteries.is_empty() { 134 | warn!("No batteries with charge threshold support found"); 135 | } else { 136 | debug!( 137 | "Found {} batteries with threshold support", 138 | supported_batteries.len() 139 | ); 140 | for battery in &supported_batteries { 141 | debug!( 142 | "Battery '{}' supports {} threshold control", 143 | battery.name, battery.pattern.description 144 | ); 145 | } 146 | } 147 | 148 | Ok(supported_batteries) 149 | } 150 | 151 | /// Applies the threshold settings to all supported batteries 152 | fn apply_thresholds_to_batteries( 153 | batteries: &[SupportedBattery<'_>], 154 | start_threshold: u8, 155 | stop_threshold: u8, 156 | ) -> Result<()> { 157 | let mut errors = Vec::new(); 158 | let mut success_count = 0; 159 | 160 | for battery in batteries { 161 | let start_path = battery.path.join(battery.pattern.start_path); 162 | let stop_path = battery.path.join(battery.pattern.stop_path); 163 | 164 | // Read current thresholds in case we need to restore them 165 | let current_stop = sysfs::read_sysfs_value(&stop_path).ok(); 166 | 167 | // Write stop threshold first (must be >= start threshold) 168 | let stop_result = sysfs::write_sysfs_value(&stop_path, &stop_threshold.to_string()); 169 | 170 | // Only proceed to set start threshold if stop threshold was set successfully 171 | if matches!(stop_result, Ok(())) { 172 | let start_result = sysfs::write_sysfs_value(&start_path, &start_threshold.to_string()); 173 | 174 | match start_result { 175 | Ok(()) => { 176 | debug!( 177 | "Set {}-{}% charge thresholds for {} battery '{}'", 178 | start_threshold, stop_threshold, battery.pattern.description, battery.name 179 | ); 180 | success_count += 1; 181 | } 182 | Err(e) => { 183 | // Start threshold failed, try to restore the previous stop threshold 184 | if let Some(prev_stop) = ¤t_stop { 185 | let restore_result = sysfs::write_sysfs_value(&stop_path, prev_stop); 186 | if let Err(re) = restore_result { 187 | warn!( 188 | "Failed to restore previous stop threshold for battery '{}': {}. Battery may be in an inconsistent state.", 189 | battery.name, re 190 | ); 191 | } else { 192 | debug!( 193 | "Restored previous stop threshold ({}) for battery '{}'", 194 | prev_stop, battery.name 195 | ); 196 | } 197 | } 198 | 199 | errors.push(format!( 200 | "Failed to set start threshold for {} battery '{}': {}", 201 | battery.pattern.description, battery.name, e 202 | )); 203 | } 204 | } 205 | } else if let Err(e) = stop_result { 206 | errors.push(format!( 207 | "Failed to set stop threshold for {} battery '{}': {}", 208 | battery.pattern.description, battery.name, e 209 | )); 210 | } 211 | } 212 | 213 | if success_count > 0 { 214 | if !errors.is_empty() { 215 | warn!( 216 | "Partial success setting battery thresholds: {}", 217 | errors.join("; ") 218 | ); 219 | } 220 | Ok(()) 221 | } else { 222 | Err(ControlError::WriteError(format!( 223 | "Failed to set charge thresholds on any battery: {}", 224 | errors.join("; ") 225 | ))) 226 | } 227 | } 228 | 229 | /// Determines if a power supply entry is a battery 230 | fn is_battery(path: &Path) -> Result { 231 | let type_path = path.join("type"); 232 | 233 | if !type_path.exists() { 234 | return Ok(false); 235 | } 236 | 237 | let ps_type = sysfs::read_sysfs_value(&type_path).map_err(|e| { 238 | ControlError::ReadError(format!("Failed to read {}: {}", type_path.display(), e)) 239 | })?; 240 | 241 | Ok(ps_type == "Battery") 242 | } 243 | 244 | /// Identifies if a battery supports threshold control and which pattern it uses 245 | fn find_battery_with_threshold_support(ps_path: &Path) -> Option> { 246 | for pattern in THRESHOLD_PATTERNS { 247 | let start_threshold_path = ps_path.join(pattern.start_path); 248 | let stop_threshold_path = ps_path.join(pattern.stop_path); 249 | 250 | // Ensure both paths exist and are writable before considering this battery supported 251 | if sysfs::path_exists_and_writable(&start_threshold_path) 252 | && sysfs::path_exists_and_writable(&stop_threshold_path) 253 | { 254 | return Some(SupportedBattery { 255 | name: ps_path.file_name()?.to_string_lossy().to_string(), 256 | pattern, 257 | path: ps_path.to_path_buf(), 258 | }); 259 | } 260 | } 261 | None 262 | } 263 | -------------------------------------------------------------------------------- /src/cli/debug.rs: -------------------------------------------------------------------------------- 1 | use crate::config::AppConfig; 2 | use crate::cpu; 3 | use crate::monitor; 4 | use crate::util::error::AppError; 5 | use std::fs; 6 | use std::process::{Command, Stdio}; 7 | use std::time::Duration; 8 | 9 | /// Prints comprehensive debug information about the system 10 | pub fn run_debug(config: &AppConfig) -> Result<(), AppError> { 11 | println!("=== SUPERFREQ DEBUG INFORMATION ==="); 12 | println!("Version: {}", env!("CARGO_PKG_VERSION")); 13 | 14 | // Current date and time 15 | println!("Timestamp: {}", jiff::Timestamp::now()); 16 | 17 | // Kernel information 18 | if let Ok(kernel_info) = get_kernel_info() { 19 | println!("Kernel Version: {kernel_info}"); 20 | } else { 21 | println!("Kernel Version: Unable to determine"); 22 | } 23 | 24 | // System uptime 25 | if let Ok(uptime) = get_system_uptime() { 26 | println!( 27 | "System Uptime: {} hours, {} minutes", 28 | uptime.as_secs() / 3600, 29 | (uptime.as_secs() % 3600) / 60 30 | ); 31 | } else { 32 | println!("System Uptime: Unable to determine"); 33 | } 34 | 35 | // Get system information 36 | match monitor::collect_system_report(config) { 37 | Ok(report) => { 38 | println!("\n--- SYSTEM INFORMATION ---"); 39 | println!("CPU Model: {}", report.system_info.cpu_model); 40 | println!("Architecture: {}", report.system_info.architecture); 41 | println!( 42 | "Linux Distribution: {}", 43 | report.system_info.linux_distribution 44 | ); 45 | 46 | println!("\n--- CONFIGURATION ---"); 47 | println!("Current Configuration: {config:#?}"); 48 | 49 | // Print important sysfs paths and whether they exist 50 | println!("\n--- SYSFS PATHS ---"); 51 | check_and_print_sysfs_path( 52 | "/sys/devices/system/cpu/intel_pstate/no_turbo", 53 | "Intel P-State Turbo Control", 54 | ); 55 | check_and_print_sysfs_path( 56 | "/sys/devices/system/cpu/cpufreq/boost", 57 | "Generic CPU Boost Control", 58 | ); 59 | check_and_print_sysfs_path( 60 | "/sys/devices/system/cpu/amd_pstate/cpufreq/boost", 61 | "AMD P-State Boost Control", 62 | ); 63 | check_and_print_sysfs_path( 64 | "/sys/firmware/acpi/platform_profile", 65 | "ACPI Platform Profile Control", 66 | ); 67 | check_and_print_sysfs_path("/sys/class/power_supply", "Power Supply Information"); 68 | 69 | println!("\n--- CPU INFORMATION ---"); 70 | println!("Current Governor: {:?}", report.cpu_global.current_governor); 71 | println!( 72 | "Available Governors: {}", 73 | report.cpu_global.available_governors.join(", ") 74 | ); 75 | println!("Turbo Status: {:?}", report.cpu_global.turbo_status); 76 | println!( 77 | "Energy Performance Preference (EPP): {:?}", 78 | report.cpu_global.epp 79 | ); 80 | println!("Energy Performance Bias (EPB): {:?}", report.cpu_global.epb); 81 | 82 | // Add governor override information 83 | if let Some(override_governor) = cpu::get_governor_override() { 84 | println!("Governor Override: {}", override_governor.trim()); 85 | } else { 86 | println!("Governor Override: None"); 87 | } 88 | 89 | println!("\n--- PLATFORM PROFILE ---"); 90 | println!( 91 | "Current Platform Profile: {:?}", 92 | report.cpu_global.platform_profile 93 | ); 94 | match cpu::get_platform_profiles() { 95 | Ok(profiles) => println!("Available Platform Profiles: {}", profiles.join(", ")), 96 | Err(_) => println!("Available Platform Profiles: Not supported on this system"), 97 | } 98 | 99 | println!("\n--- CPU CORES DETAIL ---"); 100 | println!("Total CPU Cores: {}", report.cpu_cores.len()); 101 | for core in &report.cpu_cores { 102 | println!("Core {}:", core.core_id); 103 | println!( 104 | " Current Frequency: {} MHz", 105 | core.current_frequency_mhz 106 | .map_or_else(|| "N/A".to_string(), |f| f.to_string()) 107 | ); 108 | println!( 109 | " Min Frequency: {} MHz", 110 | core.min_frequency_mhz 111 | .map_or_else(|| "N/A".to_string(), |f| f.to_string()) 112 | ); 113 | println!( 114 | " Max Frequency: {} MHz", 115 | core.max_frequency_mhz 116 | .map_or_else(|| "N/A".to_string(), |f| f.to_string()) 117 | ); 118 | println!( 119 | " Usage: {}%", 120 | core.usage_percent 121 | .map_or_else(|| "N/A".to_string(), |u| format!("{u:.1}")) 122 | ); 123 | println!( 124 | " Temperature: {}°C", 125 | core.temperature_celsius 126 | .map_or_else(|| "N/A".to_string(), |t| format!("{t:.1}")) 127 | ); 128 | } 129 | 130 | println!("\n--- TEMPERATURE INFORMATION ---"); 131 | println!( 132 | "Average CPU Temperature: {}", 133 | report.cpu_global.average_temperature_celsius.map_or_else( 134 | || "N/A (CPU temperature sensor not detected)".to_string(), 135 | |t| format!("{t:.1}°C") 136 | ) 137 | ); 138 | 139 | println!("\n--- BATTERY INFORMATION ---"); 140 | if report.batteries.is_empty() { 141 | println!("No batteries found or all are ignored."); 142 | } else { 143 | for battery in &report.batteries { 144 | println!("Battery: {}", battery.name); 145 | println!(" AC Connected: {}", battery.ac_connected); 146 | println!( 147 | " Charging State: {}", 148 | battery.charging_state.as_deref().unwrap_or("N/A") 149 | ); 150 | println!( 151 | " Capacity: {}%", 152 | battery 153 | .capacity_percent 154 | .map_or_else(|| "N/A".to_string(), |c| c.to_string()) 155 | ); 156 | println!( 157 | " Power Rate: {} W", 158 | battery 159 | .power_rate_watts 160 | .map_or_else(|| "N/A".to_string(), |p| format!("{p:.2}")) 161 | ); 162 | println!( 163 | " Charge Start Threshold: {}", 164 | battery 165 | .charge_start_threshold 166 | .map_or_else(|| "N/A".to_string(), |t| t.to_string()) 167 | ); 168 | println!( 169 | " Charge Stop Threshold: {}", 170 | battery 171 | .charge_stop_threshold 172 | .map_or_else(|| "N/A".to_string(), |t| t.to_string()) 173 | ); 174 | } 175 | } 176 | 177 | println!("\n--- SYSTEM LOAD ---"); 178 | println!( 179 | "Load Average (1 min): {:.2}", 180 | report.system_load.load_avg_1min 181 | ); 182 | println!( 183 | "Load Average (5 min): {:.2}", 184 | report.system_load.load_avg_5min 185 | ); 186 | println!( 187 | "Load Average (15 min): {:.2}", 188 | report.system_load.load_avg_15min 189 | ); 190 | 191 | println!("\n--- DAEMON STATUS ---"); 192 | // Simple check for daemon status - can be expanded later 193 | let daemon_status = fs::metadata("/var/run/superfreq.pid").is_ok(); 194 | println!("Daemon Running: {daemon_status}"); 195 | 196 | // Check for systemd service status 197 | if let Ok(systemd_status) = is_systemd_service_active("superfreq") { 198 | println!("Systemd Service Active: {systemd_status}"); 199 | } 200 | 201 | Ok(()) 202 | } 203 | Err(e) => Err(AppError::Monitor(e)), 204 | } 205 | } 206 | 207 | /// Get kernel version information 208 | fn get_kernel_info() -> Result { 209 | let output = Command::new("uname") 210 | .arg("-r") 211 | .output() 212 | .map_err(AppError::Io)?; 213 | 214 | let kernel_version = String::from_utf8(output.stdout) 215 | .map_err(|e| AppError::Generic(format!("Failed to parse kernel version: {e}")))?; 216 | Ok(kernel_version.trim().to_string()) 217 | } 218 | 219 | /// Get system uptime 220 | fn get_system_uptime() -> Result { 221 | let uptime_str = fs::read_to_string("/proc/uptime").map_err(AppError::Io)?; 222 | let uptime_secs = uptime_str 223 | .split_whitespace() 224 | .next() 225 | .ok_or_else(|| AppError::Generic("Invalid format in /proc/uptime file".to_string()))? 226 | .parse::() 227 | .map_err(|e| AppError::Generic(format!("Failed to parse uptime from /proc/uptime: {e}")))?; 228 | 229 | Ok(Duration::from_secs_f64(uptime_secs)) 230 | } 231 | 232 | /// Check if a sysfs path exists and print its status 233 | fn check_and_print_sysfs_path(path: &str, description: &str) { 234 | let exists = std::path::Path::new(path).exists(); 235 | println!( 236 | "{}: {} ({})", 237 | description, 238 | path, 239 | if exists { "Exists" } else { "Not Found" } 240 | ); 241 | } 242 | 243 | /// Check if a systemd service is active 244 | fn is_systemd_service_active(service_name: &str) -> Result { 245 | let output = Command::new("systemctl") 246 | .arg("is-active") 247 | .arg(format!("{service_name}.service")) 248 | .stdout(Stdio::piped()) // capture stdout instead of letting it print 249 | .stderr(Stdio::null()) // redirect stderr to null 250 | .output() 251 | .map_err(AppError::Io)?; 252 | 253 | // Check if the command executed successfully 254 | if !output.status.success() { 255 | // Command failed - service is either not found or not active 256 | return Ok(false); 257 | } 258 | 259 | // Command executed successfully, now check the output content 260 | let status = String::from_utf8(output.stdout) 261 | .map_err(|e| AppError::Generic(format!("Failed to parse systemctl output: {e}")))?; 262 | 263 | // Explicitly verify the output is "active" 264 | Ok(status.trim() == "active") 265 | } 266 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod debug; 2 | -------------------------------------------------------------------------------- /src/config/load.rs: -------------------------------------------------------------------------------- 1 | // Configuration loading functionality 2 | use std::fs; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use crate::config::types::{AppConfig, AppConfigToml, ConfigError, DaemonConfig, ProfileConfig}; 6 | 7 | /// The primary function to load application configuration from a specific path or from default locations. 8 | /// 9 | /// # Arguments 10 | /// 11 | /// * `specific_path` - If provided, only attempts to load from this path and errors if not found 12 | /// 13 | /// # Returns 14 | /// 15 | /// * `Ok(AppConfig)` - Successfully loaded configuration 16 | /// * `Err(ConfigError)` - Error loading or parsing configuration 17 | pub fn load_config() -> Result { 18 | load_config_from_path(None) 19 | } 20 | 21 | /// Load configuration from a specific path or try default paths 22 | pub fn load_config_from_path(specific_path: Option<&str>) -> Result { 23 | // If a specific path is provided, only try that one 24 | if let Some(path_str) = specific_path { 25 | let path = Path::new(path_str); 26 | if path.exists() { 27 | return load_and_parse_config(path); 28 | } 29 | return Err(ConfigError::Io(std::io::Error::new( 30 | std::io::ErrorKind::NotFound, 31 | format!("Specified config file not found: {}", path.display()), 32 | ))); 33 | } 34 | 35 | // Check for SUPERFREQ_CONFIG environment variable 36 | if let Ok(env_path) = std::env::var("SUPERFREQ_CONFIG") { 37 | let env_path = Path::new(&env_path); 38 | if env_path.exists() { 39 | println!( 40 | "Loading config from SUPERFREQ_CONFIG: {}", 41 | env_path.display() 42 | ); 43 | return load_and_parse_config(env_path); 44 | } 45 | eprintln!( 46 | "Warning: Config file specified by SUPERFREQ_CONFIG not found: {}", 47 | env_path.display() 48 | ); 49 | } 50 | 51 | // System-wide paths 52 | let config_paths = vec![ 53 | PathBuf::from("/etc/xdg/superfreq/config.toml"), 54 | PathBuf::from("/etc/superfreq.toml"), 55 | ]; 56 | 57 | for path in config_paths { 58 | if path.exists() { 59 | println!("Loading config from: {}", path.display()); 60 | match load_and_parse_config(&path) { 61 | Ok(config) => return Ok(config), 62 | Err(e) => { 63 | eprintln!("Error with config file {}: {}", path.display(), e); 64 | // Continue trying other files 65 | } 66 | } 67 | } 68 | } 69 | 70 | println!("No configuration file found or all failed to parse. Using default configuration."); 71 | // Construct default AppConfig by converting default AppConfigToml 72 | let default_toml_config = AppConfigToml::default(); 73 | Ok(AppConfig { 74 | charger: ProfileConfig::from(default_toml_config.charger), 75 | battery: ProfileConfig::from(default_toml_config.battery), 76 | ignored_power_supplies: default_toml_config.ignored_power_supplies, 77 | daemon: DaemonConfig::default(), 78 | }) 79 | } 80 | 81 | /// Load and parse a configuration file 82 | fn load_and_parse_config(path: &Path) -> Result { 83 | let contents = fs::read_to_string(path).map_err(ConfigError::Io)?; 84 | 85 | let toml_app_config = toml::from_str::(&contents).map_err(ConfigError::Toml)?; 86 | 87 | // Handle inheritance of values from global to profile configs 88 | let mut charger_profile = toml_app_config.charger.clone(); 89 | let mut battery_profile = toml_app_config.battery.clone(); 90 | 91 | // Clone global battery_charge_thresholds once if it exists 92 | if let Some(global_thresholds) = toml_app_config.battery_charge_thresholds { 93 | // Apply to charger profile if not already set 94 | if charger_profile.battery_charge_thresholds.is_none() { 95 | charger_profile.battery_charge_thresholds = Some(global_thresholds.clone()); 96 | } 97 | 98 | // Apply to battery profile if not already set 99 | if battery_profile.battery_charge_thresholds.is_none() { 100 | battery_profile.battery_charge_thresholds = Some(global_thresholds); 101 | } 102 | } 103 | 104 | // Convert AppConfigToml to AppConfig 105 | Ok(AppConfig { 106 | charger: ProfileConfig::from(charger_profile), 107 | battery: ProfileConfig::from(battery_profile), 108 | ignored_power_supplies: toml_app_config.ignored_power_supplies, 109 | daemon: DaemonConfig { 110 | poll_interval_sec: toml_app_config.daemon.poll_interval_sec, 111 | adaptive_interval: toml_app_config.daemon.adaptive_interval, 112 | min_poll_interval_sec: toml_app_config.daemon.min_poll_interval_sec, 113 | max_poll_interval_sec: toml_app_config.daemon.max_poll_interval_sec, 114 | throttle_on_battery: toml_app_config.daemon.throttle_on_battery, 115 | log_level: toml_app_config.daemon.log_level, 116 | stats_file_path: toml_app_config.daemon.stats_file_path, 117 | }, 118 | }) 119 | } 120 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod load; 2 | pub mod types; 3 | 4 | pub use load::*; 5 | pub use types::*; 6 | -------------------------------------------------------------------------------- /src/config/types.rs: -------------------------------------------------------------------------------- 1 | // Configuration types and structures for superfreq 2 | use crate::core::TurboSetting; 3 | use serde::{Deserialize, Serialize}; 4 | use std::convert::TryFrom; 5 | 6 | /// Defines constant-returning functions used for default values. 7 | /// This hopefully reduces repetition since we have way too many default functions 8 | /// that just return constants. 9 | macro_rules! default_const { 10 | ($name:ident, $type:ty, $value:expr) => { 11 | const fn $name() -> $type { 12 | $value 13 | } 14 | }; 15 | } 16 | 17 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] 18 | pub struct BatteryChargeThresholds { 19 | pub start: u8, 20 | pub stop: u8, 21 | } 22 | 23 | impl BatteryChargeThresholds { 24 | pub fn new(start: u8, stop: u8) -> Result { 25 | if stop == 0 { 26 | return Err(ConfigError::Validation( 27 | "Stop threshold must be greater than 0%".to_string(), 28 | )); 29 | } 30 | if start >= stop { 31 | return Err(ConfigError::Validation(format!( 32 | "Start threshold ({start}) must be less than stop threshold ({stop})" 33 | ))); 34 | } 35 | if stop > 100 { 36 | return Err(ConfigError::Validation(format!( 37 | "Stop threshold ({stop}) cannot exceed 100%" 38 | ))); 39 | } 40 | 41 | Ok(Self { start, stop }) 42 | } 43 | } 44 | 45 | impl TryFrom<(u8, u8)> for BatteryChargeThresholds { 46 | type Error = ConfigError; 47 | 48 | fn try_from(values: (u8, u8)) -> Result { 49 | let (start, stop) = values; 50 | Self::new(start, stop) 51 | } 52 | } 53 | 54 | // Structs for configuration using serde::Deserialize 55 | #[derive(Deserialize, Serialize, Debug, Clone)] 56 | pub struct ProfileConfig { 57 | pub governor: Option, 58 | pub turbo: Option, 59 | pub epp: Option, // Energy Performance Preference (EPP) 60 | pub epb: Option, // Energy Performance Bias (EPB) - usually an integer, but string for flexibility from sysfs 61 | pub min_freq_mhz: Option, 62 | pub max_freq_mhz: Option, 63 | pub platform_profile: Option, 64 | #[serde(default)] 65 | pub turbo_auto_settings: TurboAutoSettings, 66 | #[serde(default)] 67 | pub enable_auto_turbo: bool, 68 | #[serde(skip_serializing_if = "Option::is_none")] 69 | pub battery_charge_thresholds: Option, 70 | } 71 | 72 | impl Default for ProfileConfig { 73 | fn default() -> Self { 74 | Self { 75 | governor: Some("schedutil".to_string()), // common sensible default (?) 76 | turbo: Some(TurboSetting::Auto), 77 | epp: None, // defaults depend on governor and system 78 | epb: None, // defaults depend on governor and system 79 | min_freq_mhz: None, // no override 80 | max_freq_mhz: None, // no override 81 | platform_profile: None, // no override 82 | turbo_auto_settings: TurboAutoSettings::default(), 83 | enable_auto_turbo: default_enable_auto_turbo(), 84 | battery_charge_thresholds: None, 85 | } 86 | } 87 | } 88 | 89 | #[derive(Deserialize, Serialize, Debug, Default, Clone)] 90 | pub struct AppConfig { 91 | #[serde(default)] 92 | pub charger: ProfileConfig, 93 | #[serde(default)] 94 | pub battery: ProfileConfig, 95 | pub ignored_power_supplies: Option>, 96 | #[serde(default)] 97 | pub daemon: DaemonConfig, 98 | } 99 | 100 | // Error type for config loading 101 | #[derive(Debug, thiserror::Error)] 102 | pub enum ConfigError { 103 | #[error("I/O error: {0}")] 104 | Io(#[from] std::io::Error), 105 | 106 | #[error("TOML parsing error: {0}")] 107 | Toml(#[from] toml::de::Error), 108 | 109 | #[error("Configuration validation error: {0}")] 110 | Validation(String), 111 | } 112 | 113 | // Intermediate structs for TOML parsing 114 | #[derive(Deserialize, Serialize, Debug, Clone)] 115 | pub struct ProfileConfigToml { 116 | pub governor: Option, 117 | pub turbo: Option, // "always", "auto", "never" 118 | pub epp: Option, 119 | pub epb: Option, 120 | pub min_freq_mhz: Option, 121 | pub max_freq_mhz: Option, 122 | pub platform_profile: Option, 123 | pub turbo_auto_settings: Option, 124 | #[serde(default = "default_enable_auto_turbo")] 125 | pub enable_auto_turbo: bool, 126 | #[serde(skip_serializing_if = "Option::is_none")] 127 | pub battery_charge_thresholds: Option, 128 | } 129 | 130 | #[derive(Deserialize, Serialize, Debug, Clone, Default)] 131 | pub struct AppConfigToml { 132 | #[serde(default)] 133 | pub charger: ProfileConfigToml, 134 | #[serde(default)] 135 | pub battery: ProfileConfigToml, 136 | #[serde(skip_serializing_if = "Option::is_none")] 137 | pub battery_charge_thresholds: Option, 138 | pub ignored_power_supplies: Option>, 139 | #[serde(default)] 140 | pub daemon: DaemonConfigToml, 141 | } 142 | 143 | impl Default for ProfileConfigToml { 144 | fn default() -> Self { 145 | Self { 146 | governor: Some("schedutil".to_string()), 147 | turbo: Some("auto".to_string()), 148 | epp: None, 149 | epb: None, 150 | min_freq_mhz: None, 151 | max_freq_mhz: None, 152 | platform_profile: None, 153 | turbo_auto_settings: None, 154 | enable_auto_turbo: default_enable_auto_turbo(), 155 | battery_charge_thresholds: None, 156 | } 157 | } 158 | } 159 | 160 | #[derive(Deserialize, Serialize, Debug, Clone)] 161 | pub struct TurboAutoSettings { 162 | #[serde(default = "default_load_threshold_high")] 163 | pub load_threshold_high: f32, 164 | #[serde(default = "default_load_threshold_low")] 165 | pub load_threshold_low: f32, 166 | #[serde(default = "default_temp_threshold_high")] 167 | pub temp_threshold_high: f32, 168 | /// Initial turbo boost state when no previous state exists. 169 | /// Set to `true` to start with turbo enabled, `false` to start with turbo disabled. 170 | /// This is only used at first launch or after a reset. 171 | #[serde(default = "default_initial_turbo_state")] 172 | pub initial_turbo_state: bool, 173 | } 174 | 175 | // Default thresholds for Auto turbo mode 176 | pub const DEFAULT_LOAD_THRESHOLD_HIGH: f32 = 70.0; // enable turbo if load is above this 177 | pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // disable turbo if load is below this 178 | pub const DEFAULT_TEMP_THRESHOLD_HIGH: f32 = 75.0; // disable turbo if temperature is above this 179 | pub const DEFAULT_INITIAL_TURBO_STATE: bool = false; // by default, start with turbo disabled 180 | 181 | default_const!( 182 | default_load_threshold_high, 183 | f32, 184 | DEFAULT_LOAD_THRESHOLD_HIGH 185 | ); 186 | default_const!(default_load_threshold_low, f32, DEFAULT_LOAD_THRESHOLD_LOW); 187 | default_const!( 188 | default_temp_threshold_high, 189 | f32, 190 | DEFAULT_TEMP_THRESHOLD_HIGH 191 | ); 192 | default_const!( 193 | default_initial_turbo_state, 194 | bool, 195 | DEFAULT_INITIAL_TURBO_STATE 196 | ); 197 | 198 | impl Default for TurboAutoSettings { 199 | fn default() -> Self { 200 | Self { 201 | load_threshold_high: DEFAULT_LOAD_THRESHOLD_HIGH, 202 | load_threshold_low: DEFAULT_LOAD_THRESHOLD_LOW, 203 | temp_threshold_high: DEFAULT_TEMP_THRESHOLD_HIGH, 204 | initial_turbo_state: DEFAULT_INITIAL_TURBO_STATE, 205 | } 206 | } 207 | } 208 | 209 | impl From for ProfileConfig { 210 | fn from(toml_config: ProfileConfigToml) -> Self { 211 | Self { 212 | governor: toml_config.governor, 213 | turbo: toml_config 214 | .turbo 215 | .and_then(|s| match s.to_lowercase().as_str() { 216 | "always" => Some(TurboSetting::Always), 217 | "auto" => Some(TurboSetting::Auto), 218 | "never" => Some(TurboSetting::Never), 219 | _ => None, 220 | }), 221 | epp: toml_config.epp, 222 | epb: toml_config.epb, 223 | min_freq_mhz: toml_config.min_freq_mhz, 224 | max_freq_mhz: toml_config.max_freq_mhz, 225 | platform_profile: toml_config.platform_profile, 226 | turbo_auto_settings: toml_config.turbo_auto_settings.unwrap_or_default(), 227 | enable_auto_turbo: toml_config.enable_auto_turbo, 228 | battery_charge_thresholds: toml_config.battery_charge_thresholds, 229 | } 230 | } 231 | } 232 | 233 | #[derive(Deserialize, Serialize, Debug, Clone)] 234 | pub struct DaemonConfig { 235 | #[serde(default = "default_poll_interval_sec")] 236 | pub poll_interval_sec: u64, 237 | #[serde(default = "default_adaptive_interval")] 238 | pub adaptive_interval: bool, 239 | #[serde(default = "default_min_poll_interval_sec")] 240 | pub min_poll_interval_sec: u64, 241 | #[serde(default = "default_max_poll_interval_sec")] 242 | pub max_poll_interval_sec: u64, 243 | #[serde(default = "default_throttle_on_battery")] 244 | pub throttle_on_battery: bool, 245 | #[serde(default = "default_log_level")] 246 | pub log_level: LogLevel, 247 | #[serde(default = "default_stats_file_path")] 248 | pub stats_file_path: Option, 249 | } 250 | 251 | #[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)] 252 | pub enum LogLevel { 253 | Error, 254 | Warning, 255 | Info, 256 | Debug, 257 | } 258 | 259 | impl Default for DaemonConfig { 260 | fn default() -> Self { 261 | Self { 262 | poll_interval_sec: default_poll_interval_sec(), 263 | adaptive_interval: default_adaptive_interval(), 264 | min_poll_interval_sec: default_min_poll_interval_sec(), 265 | max_poll_interval_sec: default_max_poll_interval_sec(), 266 | throttle_on_battery: default_throttle_on_battery(), 267 | log_level: default_log_level(), 268 | stats_file_path: default_stats_file_path(), 269 | } 270 | } 271 | } 272 | 273 | default_const!(default_poll_interval_sec, u64, 5); 274 | default_const!(default_adaptive_interval, bool, false); 275 | default_const!(default_min_poll_interval_sec, u64, 1); 276 | default_const!(default_max_poll_interval_sec, u64, 30); 277 | default_const!(default_throttle_on_battery, bool, true); 278 | default_const!(default_log_level, LogLevel, LogLevel::Info); 279 | default_const!(default_stats_file_path, Option, None); 280 | default_const!(default_enable_auto_turbo, bool, true); 281 | 282 | #[derive(Deserialize, Serialize, Debug, Clone)] 283 | pub struct DaemonConfigToml { 284 | #[serde(default = "default_poll_interval_sec")] 285 | pub poll_interval_sec: u64, 286 | #[serde(default = "default_adaptive_interval")] 287 | pub adaptive_interval: bool, 288 | #[serde(default = "default_min_poll_interval_sec")] 289 | pub min_poll_interval_sec: u64, 290 | #[serde(default = "default_max_poll_interval_sec")] 291 | pub max_poll_interval_sec: u64, 292 | #[serde(default = "default_throttle_on_battery")] 293 | pub throttle_on_battery: bool, 294 | #[serde(default = "default_log_level")] 295 | pub log_level: LogLevel, 296 | #[serde(default = "default_stats_file_path")] 297 | pub stats_file_path: Option, 298 | } 299 | 300 | impl Default for DaemonConfigToml { 301 | fn default() -> Self { 302 | Self { 303 | poll_interval_sec: default_poll_interval_sec(), 304 | adaptive_interval: default_adaptive_interval(), 305 | min_poll_interval_sec: default_min_poll_interval_sec(), 306 | max_poll_interval_sec: default_max_poll_interval_sec(), 307 | throttle_on_battery: default_throttle_on_battery(), 308 | log_level: default_log_level(), 309 | stats_file_path: default_stats_file_path(), 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ValueEnum)] 6 | pub enum TurboSetting { 7 | Always, // turbo is forced on (if possible) 8 | Auto, // system or driver controls turbo 9 | Never, // turbo is forced off 10 | } 11 | 12 | #[derive(Debug, Clone, Copy, ValueEnum)] 13 | pub enum GovernorOverrideMode { 14 | Performance, 15 | Powersave, 16 | Reset, 17 | } 18 | 19 | impl fmt::Display for GovernorOverrideMode { 20 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | match self { 22 | Self::Performance => write!(f, "performance"), 23 | Self::Powersave => write!(f, "powersave"), 24 | Self::Reset => write!(f, "reset"), 25 | } 26 | } 27 | } 28 | 29 | pub struct SystemInfo { 30 | // Overall system details 31 | pub cpu_model: String, 32 | pub architecture: String, 33 | pub linux_distribution: String, 34 | } 35 | 36 | pub struct CpuCoreInfo { 37 | // Per-core data 38 | pub core_id: u32, 39 | pub current_frequency_mhz: Option, 40 | pub min_frequency_mhz: Option, 41 | pub max_frequency_mhz: Option, 42 | pub usage_percent: Option, 43 | pub temperature_celsius: Option, 44 | } 45 | 46 | pub struct CpuGlobalInfo { 47 | // System-wide CPU settings 48 | pub current_governor: Option, 49 | pub available_governors: Vec, 50 | pub turbo_status: Option, // true for enabled, false for disabled 51 | pub epp: Option, // Energy Performance Preference 52 | pub epb: Option, // Energy Performance Bias 53 | pub platform_profile: Option, 54 | pub average_temperature_celsius: Option, // Average temperature across all cores 55 | } 56 | 57 | pub struct BatteryInfo { 58 | // Battery status (AC connected, charging state, capacity, power rate, charge start/stop thresholds if available). 59 | pub name: String, 60 | pub ac_connected: bool, 61 | pub charging_state: Option, // e.g., "Charging", "Discharging", "Full" 62 | pub capacity_percent: Option, 63 | pub power_rate_watts: Option, // positive for charging, negative for discharging 64 | pub charge_start_threshold: Option, 65 | pub charge_stop_threshold: Option, 66 | } 67 | 68 | pub struct SystemLoad { 69 | // System load averages. 70 | pub load_avg_1min: f32, 71 | pub load_avg_5min: f32, 72 | pub load_avg_15min: f32, 73 | } 74 | 75 | pub struct SystemReport { 76 | // Now combine all the above for a snapshot of the system state. 77 | pub system_info: SystemInfo, 78 | pub cpu_cores: Vec, 79 | pub cpu_global: CpuGlobalInfo, 80 | pub batteries: Vec, 81 | pub system_load: SystemLoad, 82 | pub timestamp: std::time::SystemTime, // so we know when the report was generated 83 | } 84 | 85 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 86 | pub enum OperationalMode { 87 | Powersave, 88 | Performance, 89 | } 90 | -------------------------------------------------------------------------------- /src/cpu.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{GovernorOverrideMode, TurboSetting}; 2 | use crate::util::error::ControlError; 3 | use core::str; 4 | use log::debug; 5 | use std::{fs, io, path::Path, string::ToString}; 6 | 7 | pub type Result = std::result::Result; 8 | 9 | // Valid EPB string values 10 | const VALID_EPB_STRINGS: &[&str] = &[ 11 | "performance", 12 | "balance-performance", 13 | "balance_performance", // alternative form 14 | "balance-power", 15 | "balance_power", // alternative form 16 | "power", 17 | ]; 18 | 19 | // EPP (Energy Performance Preference) string values 20 | const EPP_FALLBACK_VALUES: &[&str] = &[ 21 | "default", 22 | "performance", 23 | "balance-performance", 24 | "balance_performance", // alternative form with underscore 25 | "balance-power", 26 | "balance_power", // alternative form with underscore 27 | "power", 28 | ]; 29 | 30 | // Write a value to a sysfs file 31 | fn write_sysfs_value(path: impl AsRef, value: &str) -> Result<()> { 32 | let p = path.as_ref(); 33 | 34 | fs::write(p, value).map_err(|e| { 35 | let error_msg = format!("Path: {:?}, Value: '{}', Error: {}", p.display(), value, e); 36 | match e.kind() { 37 | io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(error_msg), 38 | io::ErrorKind::NotFound => { 39 | ControlError::PathMissing(format!("Path '{}' does not exist", p.display())) 40 | } 41 | _ => ControlError::WriteError(error_msg), 42 | } 43 | }) 44 | } 45 | 46 | pub fn get_logical_core_count() -> Result { 47 | // Using num_cpus::get() for a reliable count of logical cores accessible. 48 | // The monitor module's get_logical_core_count might be more specific to cpufreq-capable cores, 49 | // but for applying settings, we might want to iterate over all reported by OS. 50 | // However, settings usually apply to cores with cpufreq. 51 | // Let's use a similar discovery to monitor's get_logical_core_count 52 | let mut num_cores: u32 = 0; 53 | let path = Path::new("/sys/devices/system/cpu"); 54 | if !path.exists() { 55 | return Err(ControlError::NotSupported(format!( 56 | "No logical cores found at {}.", 57 | path.display() 58 | ))); 59 | } 60 | 61 | let entries = fs::read_dir(path) 62 | .map_err(|_| { 63 | ControlError::PermissionDenied(format!("Cannot read contents of {}.", path.display())) 64 | })? 65 | .flatten(); 66 | 67 | for entry in entries { 68 | let entry_file_name = entry.file_name(); 69 | let Some(name) = entry_file_name.to_str() else { 70 | continue; 71 | }; 72 | 73 | // Skip non-CPU directories (e.g., cpuidle, cpufreq) 74 | if !name.starts_with("cpu") || name.len() <= 3 || !name[3..].chars().all(char::is_numeric) { 75 | continue; 76 | } 77 | 78 | if !entry.path().join("cpufreq").exists() { 79 | continue; 80 | } 81 | 82 | if name[3..].parse::().is_ok() { 83 | num_cores += 1; 84 | } 85 | } 86 | if num_cores == 0 { 87 | // Fallback if sysfs iteration above fails to find any cpufreq cores 88 | num_cores = num_cpus::get() as u32; 89 | } 90 | 91 | Ok(num_cores) 92 | } 93 | 94 | fn for_each_cpu_core(mut action: F) -> Result<()> 95 | where 96 | F: FnMut(u32) -> Result<()>, 97 | { 98 | let num_cores: u32 = get_logical_core_count()?; 99 | 100 | for core_id in 0u32..num_cores { 101 | action(core_id)?; 102 | } 103 | Ok(()) 104 | } 105 | 106 | pub fn set_governor(governor: &str, core_id: Option) -> Result<()> { 107 | // Validate the governor is available on this system 108 | // This returns both the validation result and the list of available governors 109 | let (is_valid, available_governors) = is_governor_valid(governor)?; 110 | 111 | if !is_valid { 112 | return Err(ControlError::InvalidGovernor(format!( 113 | "Governor '{}' is not available on this system. Valid governors: {}", 114 | governor, 115 | available_governors.join(", ") 116 | ))); 117 | } 118 | 119 | let action = |id: u32| { 120 | let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_governor"); 121 | if Path::new(&path).exists() { 122 | write_sysfs_value(&path, governor) 123 | } else { 124 | // Silently ignore if the path doesn't exist for a specific core, 125 | // as not all cores might have cpufreq (e.g. offline cores) 126 | Ok(()) 127 | } 128 | }; 129 | 130 | core_id.map_or_else(|| for_each_cpu_core(action), action) 131 | } 132 | 133 | /// Check if the provided governor is available in the system 134 | /// Returns a tuple of (`is_valid`, `available_governors`) to avoid redundant file reads 135 | fn is_governor_valid(governor: &str) -> Result<(bool, Vec)> { 136 | let governors = get_available_governors()?; 137 | 138 | // Convert input governor to lowercase for case-insensitive comparison 139 | let governor_lower = governor.to_lowercase(); 140 | 141 | // Convert all available governors to lowercase for comparison 142 | let governors_lower: Vec = governors.iter().map(|g| g.to_lowercase()).collect(); 143 | 144 | // Check if the lowercase governor is in the lowercase list 145 | Ok((governors_lower.contains(&governor_lower), governors)) 146 | } 147 | 148 | /// Get available CPU governors from the system 149 | fn get_available_governors() -> Result> { 150 | let cpu_base_path = Path::new("/sys/devices/system/cpu"); 151 | 152 | // First try the traditional path with cpu0. This is the most common case 153 | // and will usually catch early, but we should try to keep the code to handle 154 | // "edge" cases lightweight, for the (albeit smaller) number of users that 155 | // run Superfreq on unusual systems. 156 | let cpu0_path = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors"; 157 | if Path::new(cpu0_path).exists() { 158 | let content = fs::read_to_string(cpu0_path).map_err(|e| { 159 | ControlError::ReadError(format!("Failed to read available governors from cpu0: {e}")) 160 | })?; 161 | 162 | let governors: Vec = content 163 | .split_whitespace() 164 | .map(ToString::to_string) 165 | .collect(); 166 | 167 | if !governors.is_empty() { 168 | return Ok(governors); 169 | } 170 | } 171 | 172 | // If cpu0 doesn't have the file or it's empty, scan all CPUs 173 | // This handles heterogeneous systems where cpu0 might not have cpufreq 174 | if let Ok(entries) = fs::read_dir(cpu_base_path) { 175 | for entry in entries.flatten() { 176 | let path = entry.path(); 177 | let file_name = entry.file_name(); 178 | let name = match file_name.to_str() { 179 | Some(name) => name, 180 | None => continue, 181 | }; 182 | 183 | // Skip non-CPU directories 184 | if !name.starts_with("cpu") 185 | || name.len() <= 3 186 | || !name[3..].chars().all(char::is_numeric) 187 | { 188 | continue; 189 | } 190 | 191 | let governor_path = path.join("cpufreq/scaling_available_governors"); 192 | if governor_path.exists() { 193 | match fs::read_to_string(&governor_path) { 194 | Ok(content) => { 195 | let governors: Vec = content 196 | .split_whitespace() 197 | .map(ToString::to_string) 198 | .collect(); 199 | 200 | if !governors.is_empty() { 201 | return Ok(governors); 202 | } 203 | } 204 | Err(_) => continue, // try next CPU if this one fails 205 | } 206 | } 207 | } 208 | } 209 | 210 | // If we get here, we couldn't find any valid governors list 211 | Err(ControlError::NotSupported( 212 | "Could not determine available governors on any CPU".to_string(), 213 | )) 214 | } 215 | 216 | pub fn set_turbo(setting: TurboSetting) -> Result<()> { 217 | let value_pstate = match setting { 218 | TurboSetting::Always => "0", // no_turbo = 0 means turbo is enabled 219 | TurboSetting::Never => "1", // no_turbo = 1 means turbo is disabled 220 | // Auto mode is handled at the engine level, not directly at the sysfs level 221 | TurboSetting::Auto => { 222 | debug!("Turbo Auto mode is managed by engine logic based on system conditions"); 223 | return Ok(()); 224 | } 225 | }; 226 | let value_boost = match setting { 227 | TurboSetting::Always => "1", // boost = 1 means turbo is enabled 228 | TurboSetting::Never => "0", // boost = 0 means turbo is disabled 229 | TurboSetting::Auto => { 230 | debug!("Turbo Auto mode is managed by engine logic based on system conditions"); 231 | return Ok(()); 232 | } 233 | }; 234 | 235 | // AMD specific paths 236 | let amd_pstate_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost"; 237 | let msr_boost_path = "/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost"; 238 | 239 | // Path priority (from most to least specific) 240 | let pstate_path = "/sys/devices/system/cpu/intel_pstate/no_turbo"; 241 | let boost_path = "/sys/devices/system/cpu/cpufreq/boost"; 242 | 243 | // Try each boost control path in order of specificity 244 | if Path::new(pstate_path).exists() { 245 | write_sysfs_value(pstate_path, value_pstate) 246 | } else if Path::new(amd_pstate_path).exists() { 247 | write_sysfs_value(amd_pstate_path, value_boost) 248 | } else if Path::new(msr_boost_path).exists() { 249 | write_sysfs_value(msr_boost_path, value_boost) 250 | } else if Path::new(boost_path).exists() { 251 | write_sysfs_value(boost_path, value_boost) 252 | } else { 253 | // Also try per-core cpufreq boost for some AMD systems 254 | let result = try_set_per_core_boost(value_boost)?; 255 | if result { 256 | Ok(()) 257 | } else { 258 | Err(ControlError::NotSupported( 259 | "No supported CPU boost control mechanism found.".to_string(), 260 | )) 261 | } 262 | } 263 | } 264 | 265 | /// Try to set boost on a per-core basis for systems that support it 266 | fn try_set_per_core_boost(value: &str) -> Result { 267 | let mut success = false; 268 | let num_cores = get_logical_core_count()?; 269 | 270 | for core_id in 0..num_cores { 271 | let boost_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/boost"); 272 | 273 | if Path::new(&boost_path).exists() { 274 | write_sysfs_value(&boost_path, value)?; 275 | success = true; 276 | } 277 | } 278 | 279 | Ok(success) 280 | } 281 | 282 | pub fn set_epp(epp: &str, core_id: Option) -> Result<()> { 283 | // Validate the EPP value against available options 284 | let available_epp = get_available_epp_values()?; 285 | if !available_epp.iter().any(|v| v.eq_ignore_ascii_case(epp)) { 286 | return Err(ControlError::InvalidValueError(format!( 287 | "Invalid EPP value: '{}'. Available values: {}", 288 | epp, 289 | available_epp.join(", ") 290 | ))); 291 | } 292 | 293 | let action = |id: u32| { 294 | let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_preference"); 295 | if Path::new(&path).exists() { 296 | write_sysfs_value(&path, epp) 297 | } else { 298 | Ok(()) 299 | } 300 | }; 301 | core_id.map_or_else(|| for_each_cpu_core(action), action) 302 | } 303 | 304 | /// Get available EPP values from the system 305 | fn get_available_epp_values() -> Result> { 306 | let path = "/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences"; 307 | 308 | if !Path::new(path).exists() { 309 | // If the file doesn't exist, fall back to a default set of common values 310 | // This is safer than failing outright, as some systems may allow these values │ 311 | // even without explicitly listing them 312 | return Ok(EPP_FALLBACK_VALUES.iter().map(|&s| s.to_string()).collect()); 313 | } 314 | 315 | let content = fs::read_to_string(path).map_err(|e| { 316 | ControlError::ReadError(format!("Failed to read available EPP values: {e}")) 317 | })?; 318 | 319 | Ok(content 320 | .split_whitespace() 321 | .map(ToString::to_string) 322 | .collect()) 323 | } 324 | 325 | pub fn set_epb(epb: &str, core_id: Option) -> Result<()> { 326 | // Validate EPB value - should be a number 0-15 or a recognized string value 327 | validate_epb_value(epb)?; 328 | 329 | let action = |id: u32| { 330 | let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_bias"); 331 | if Path::new(&path).exists() { 332 | write_sysfs_value(&path, epb) 333 | } else { 334 | Ok(()) 335 | } 336 | }; 337 | core_id.map_or_else(|| for_each_cpu_core(action), action) 338 | } 339 | 340 | fn validate_epb_value(epb: &str) -> Result<()> { 341 | // EPB can be a number from 0-15 or a recognized string 342 | // Try parsing as a number first 343 | if let Ok(value) = epb.parse::() { 344 | if value <= 15 { 345 | return Ok(()); 346 | } 347 | return Err(ControlError::InvalidValueError(format!( 348 | "EPB numeric value must be between 0 and 15, got {value}" 349 | ))); 350 | } 351 | 352 | // If not a number, check if it's a recognized string value. 353 | // This is using case-insensitive comparison 354 | if VALID_EPB_STRINGS 355 | .iter() 356 | .any(|valid| valid.eq_ignore_ascii_case(epb)) 357 | { 358 | Ok(()) 359 | } else { 360 | Err(ControlError::InvalidValueError(format!( 361 | "Invalid EPB value: '{}'. Must be a number 0-15 or one of: {}", 362 | epb, 363 | VALID_EPB_STRINGS.join(", ") 364 | ))) 365 | } 366 | } 367 | 368 | pub fn set_min_frequency(freq_mhz: u32, core_id: Option) -> Result<()> { 369 | // Check if the new minimum frequency would be greater than current maximum 370 | if let Some(id) = core_id { 371 | validate_min_frequency(id, freq_mhz)?; 372 | } else { 373 | // Check for all cores 374 | let num_cores = get_logical_core_count()?; 375 | for id in 0..num_cores { 376 | validate_min_frequency(id, freq_mhz)?; 377 | } 378 | } 379 | 380 | // XXX: We use u64 for the intermediate calculation to prevent overflow 381 | let freq_khz = u64::from(freq_mhz) * 1000; 382 | let freq_khz_str = freq_khz.to_string(); 383 | 384 | let action = |id: u32| { 385 | let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_min_freq"); 386 | if Path::new(&path).exists() { 387 | write_sysfs_value(&path, &freq_khz_str) 388 | } else { 389 | Ok(()) 390 | } 391 | }; 392 | core_id.map_or_else(|| for_each_cpu_core(action), action) 393 | } 394 | 395 | pub fn set_max_frequency(freq_mhz: u32, core_id: Option) -> Result<()> { 396 | // Check if the new maximum frequency would be less than current minimum 397 | if let Some(id) = core_id { 398 | validate_max_frequency(id, freq_mhz)?; 399 | } else { 400 | // Check for all cores 401 | let num_cores = get_logical_core_count()?; 402 | for id in 0..num_cores { 403 | validate_max_frequency(id, freq_mhz)?; 404 | } 405 | } 406 | 407 | // XXX: Use a u64 here as well. 408 | let freq_khz = u64::from(freq_mhz) * 1000; 409 | let freq_khz_str = freq_khz.to_string(); 410 | 411 | let action = |id: u32| { 412 | let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_max_freq"); 413 | if Path::new(&path).exists() { 414 | write_sysfs_value(&path, &freq_khz_str) 415 | } else { 416 | Ok(()) 417 | } 418 | }; 419 | core_id.map_or_else(|| for_each_cpu_core(action), action) 420 | } 421 | 422 | fn read_sysfs_value_as_u32(path: &str) -> Result { 423 | if !Path::new(path).exists() { 424 | return Err(ControlError::NotSupported(format!( 425 | "File does not exist: {path}" 426 | ))); 427 | } 428 | 429 | let content = fs::read_to_string(path) 430 | .map_err(|e| ControlError::ReadError(format!("Failed to read {path}: {e}")))?; 431 | 432 | content 433 | .trim() 434 | .parse::() 435 | .map_err(|e| ControlError::ParseError(format!("Failed to parse value from {path}: {e}"))) 436 | } 437 | 438 | fn validate_min_frequency(core_id: u32, new_min_freq_mhz: u32) -> Result<()> { 439 | let max_freq_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/scaling_max_freq"); 440 | 441 | if !Path::new(&max_freq_path).exists() { 442 | return Ok(()); 443 | } 444 | 445 | let max_freq_khz = read_sysfs_value_as_u32(&max_freq_path)?; 446 | let new_min_freq_khz = new_min_freq_mhz * 1000; 447 | 448 | if new_min_freq_khz > max_freq_khz { 449 | return Err(ControlError::InvalidValueError(format!( 450 | "Minimum frequency ({} MHz) cannot be higher than maximum frequency ({} MHz) for core {}", 451 | new_min_freq_mhz, 452 | max_freq_khz / 1000, 453 | core_id 454 | ))); 455 | } 456 | 457 | Ok(()) 458 | } 459 | 460 | fn validate_max_frequency(core_id: u32, new_max_freq_mhz: u32) -> Result<()> { 461 | let min_freq_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/scaling_min_freq"); 462 | 463 | if !Path::new(&min_freq_path).exists() { 464 | return Ok(()); 465 | } 466 | 467 | let min_freq_khz = read_sysfs_value_as_u32(&min_freq_path)?; 468 | let new_max_freq_khz = new_max_freq_mhz * 1000; 469 | 470 | if new_max_freq_khz < min_freq_khz { 471 | return Err(ControlError::InvalidValueError(format!( 472 | "Maximum frequency ({} MHz) cannot be lower than minimum frequency ({} MHz) for core {}", 473 | new_max_freq_mhz, 474 | min_freq_khz / 1000, 475 | core_id 476 | ))); 477 | } 478 | 479 | Ok(()) 480 | } 481 | 482 | /// Sets the platform profile. 483 | /// This changes the system performance, temperature, fan, and other hardware replated characteristics. 484 | /// 485 | /// Also see [`The Kernel docs`] for this. 486 | /// 487 | /// [`The Kernel docs`]: 488 | /// 489 | /// # Examples 490 | /// 491 | /// ``` 492 | /// set_platform_profile("balanced"); 493 | /// ``` 494 | /// 495 | pub fn set_platform_profile(profile: &str) -> Result<()> { 496 | let path = "/sys/firmware/acpi/platform_profile"; 497 | if !Path::new(path).exists() { 498 | return Err(ControlError::NotSupported(format!( 499 | "Platform profile control not found at {path}.", 500 | ))); 501 | } 502 | 503 | let available_profiles = get_platform_profiles()?; 504 | 505 | if !available_profiles.contains(&profile.to_string()) { 506 | return Err(ControlError::InvalidProfile(format!( 507 | "Invalid platform control profile provided.\n\ 508 | Provided profile: {} \n\ 509 | Available profiles:\n\ 510 | {}", 511 | profile, 512 | available_profiles.join(", ") 513 | ))); 514 | } 515 | write_sysfs_value(path, profile) 516 | } 517 | 518 | /// Returns the list of available platform profiles. 519 | /// 520 | /// # Errors 521 | /// 522 | /// # Returns 523 | /// 524 | /// - [`ControlError::NotSupported`] if: 525 | /// - The file `/sys/firmware/acpi/platform_profile_choices` does not exist. 526 | /// - The file `/sys/firmware/acpi/platform_profile_choices` is empty. 527 | /// 528 | /// - [`ControlError::PermissionDenied`] if the file `/sys/firmware/acpi/platform_profile_choices` cannot be read. 529 | /// 530 | pub fn get_platform_profiles() -> Result> { 531 | let path = "/sys/firmware/acpi/platform_profile_choices"; 532 | 533 | if !Path::new(path).exists() { 534 | return Err(ControlError::NotSupported(format!( 535 | "Platform profile choices not found at {path}." 536 | ))); 537 | } 538 | 539 | let content = fs::read_to_string(path) 540 | .map_err(|_| ControlError::PermissionDenied(format!("Cannot read contents of {path}.")))?; 541 | 542 | Ok(content 543 | .split_whitespace() 544 | .map(ToString::to_string) 545 | .collect()) 546 | } 547 | 548 | /// Path for storing the governor override state 549 | const GOVERNOR_OVERRIDE_PATH: &str = "/etc/xdg/superfreq/governor_override"; 550 | 551 | /// Force a specific CPU governor or reset to automatic mode 552 | pub fn force_governor(mode: GovernorOverrideMode) -> Result<()> { 553 | // Create directory if it doesn't exist 554 | let dir_path = Path::new("/etc/xdg/superfreq"); 555 | if !dir_path.exists() { 556 | fs::create_dir_all(dir_path).map_err(|e| { 557 | if e.kind() == io::ErrorKind::PermissionDenied { 558 | ControlError::PermissionDenied(format!( 559 | "Permission denied creating directory: {}. Try running with sudo.", 560 | dir_path.display() 561 | )) 562 | } else { 563 | ControlError::Io(e) 564 | } 565 | })?; 566 | } 567 | 568 | match mode { 569 | GovernorOverrideMode::Reset => { 570 | // Remove the override file if it exists 571 | if Path::new(GOVERNOR_OVERRIDE_PATH).exists() { 572 | fs::remove_file(GOVERNOR_OVERRIDE_PATH).map_err(|e| { 573 | if e.kind() == io::ErrorKind::PermissionDenied { 574 | ControlError::PermissionDenied(format!( 575 | "Permission denied removing override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo." 576 | )) 577 | } else { 578 | ControlError::Io(e) 579 | } 580 | })?; 581 | println!( 582 | "Governor override has been reset. Normal profile-based settings will be used." 583 | ); 584 | } else { 585 | println!("No governor override was set."); 586 | } 587 | Ok(()) 588 | } 589 | GovernorOverrideMode::Performance | GovernorOverrideMode::Powersave => { 590 | // Create the override file with the selected governor 591 | let governor = mode.to_string().to_lowercase(); 592 | fs::write(GOVERNOR_OVERRIDE_PATH, &governor).map_err(|e| { 593 | if e.kind() == io::ErrorKind::PermissionDenied { 594 | ControlError::PermissionDenied(format!( 595 | "Permission denied writing to override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo." 596 | )) 597 | } else { 598 | ControlError::Io(e) 599 | } 600 | })?; 601 | 602 | // Also apply the governor immediately 603 | set_governor(&governor, None)?; 604 | 605 | println!( 606 | "Governor override set to '{governor}'. This setting will persist across reboots." 607 | ); 608 | println!("To reset, use: superfreq force-governor reset"); 609 | Ok(()) 610 | } 611 | } 612 | } 613 | 614 | /// Get the current governor override if set 615 | pub fn get_governor_override() -> Option { 616 | if Path::new(GOVERNOR_OVERRIDE_PATH).exists() { 617 | fs::read_to_string(GOVERNOR_OVERRIDE_PATH).ok() 618 | } else { 619 | None 620 | } 621 | } 622 | -------------------------------------------------------------------------------- /src/daemon.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{AppConfig, LogLevel}; 2 | use crate::core::SystemReport; 3 | use crate::engine; 4 | use crate::monitor; 5 | use crate::util::error::{AppError, ControlError}; 6 | use log::{LevelFilter, debug, error, info, warn}; 7 | use std::collections::VecDeque; 8 | use std::fs::File; 9 | use std::io::Write; 10 | use std::sync::Arc; 11 | use std::sync::atomic::{AtomicBool, Ordering}; 12 | use std::time::{Duration, Instant}; 13 | 14 | /// Parameters for computing optimal polling interval 15 | struct IntervalParams { 16 | /// Base polling interval in seconds 17 | base_interval: u64, 18 | /// Minimum allowed polling interval in seconds 19 | min_interval: u64, 20 | /// Maximum allowed polling interval in seconds 21 | max_interval: u64, 22 | /// How rapidly CPU usage is changing 23 | cpu_volatility: f32, 24 | /// How rapidly temperature is changing 25 | temp_volatility: f32, 26 | /// Battery discharge rate in %/hour if available 27 | battery_discharge_rate: Option, 28 | /// Time since last detected user activity 29 | last_user_activity: Duration, 30 | /// Whether the system appears to be idle 31 | is_system_idle: bool, 32 | /// Whether the system is running on battery power 33 | on_battery: bool, 34 | } 35 | 36 | /// Calculate the idle time multiplier based on system idle duration 37 | /// 38 | /// Returns a multiplier between 1.0 and 5.0 (capped): 39 | /// - For idle times < 2 minutes: Linear interpolation from 1.0 to 2.0 40 | /// - For idle times >= 2 minutes: Logarithmic scaling (1.0 + log2(minutes)) 41 | fn idle_multiplier(idle_secs: u64) -> f32 { 42 | if idle_secs == 0 { 43 | return 1.0; // No idle time, no multiplier effect 44 | } 45 | 46 | let idle_factor = if idle_secs < 120 { 47 | // Less than 2 minutes (0 to 119 seconds) 48 | // Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s) 49 | 1.0 + (idle_secs as f32) / 120.0 50 | } else { 51 | // 2 minutes (120 seconds) or more 52 | let idle_time_minutes = idle_secs / 60; 53 | // Logarithmic scaling: 1.0 + log2(minutes) 54 | 1.0 + (idle_time_minutes as f32).log2().max(0.5) 55 | }; 56 | 57 | // Cap the multiplier to avoid excessive intervals 58 | idle_factor.min(5.0) // max factor of 5x 59 | } 60 | 61 | /// Calculate optimal polling interval based on system conditions and history 62 | /// 63 | /// Returns Ok with the calculated interval, or Err if the configuration is invalid 64 | fn compute_new( 65 | params: &IntervalParams, 66 | system_history: &SystemHistory, 67 | ) -> Result { 68 | // Use the centralized validation function 69 | validate_poll_intervals(params.min_interval, params.max_interval)?; 70 | 71 | // Start with base interval 72 | let mut adjusted_interval = params.base_interval; 73 | 74 | // If we're on battery, we want to be more aggressive about saving power 75 | if params.on_battery { 76 | // Apply a multiplier based on battery discharge rate 77 | if let Some(discharge_rate) = params.battery_discharge_rate { 78 | if discharge_rate > 20.0 { 79 | // High discharge rate - increase polling interval significantly (3x) 80 | adjusted_interval = adjusted_interval.saturating_mul(3); 81 | } else if discharge_rate > 10.0 { 82 | // Moderate discharge - double polling interval (2x) 83 | adjusted_interval = adjusted_interval.saturating_mul(2); 84 | } else { 85 | // Low discharge rate - increase by 50% (multiply by 3/2) 86 | adjusted_interval = adjusted_interval.saturating_mul(3).saturating_div(2); 87 | } 88 | } else { 89 | // If we don't know discharge rate, use a conservative multiplier (2x) 90 | adjusted_interval = adjusted_interval.saturating_mul(2); 91 | } 92 | } 93 | 94 | // Adjust for system idleness 95 | if params.is_system_idle { 96 | let idle_time_seconds = params.last_user_activity.as_secs(); 97 | 98 | // Apply adjustment only if the system has been idle for a non-zero duration 99 | if idle_time_seconds > 0 { 100 | let idle_factor = idle_multiplier(idle_time_seconds); 101 | 102 | debug!( 103 | "System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x", 104 | idle_time_seconds, 105 | (idle_time_seconds as f32 / 60.0).round(), 106 | idle_factor 107 | ); 108 | 109 | // Convert f32 multiplier to integer-safe math 110 | // Multiply by a large number first, then divide to maintain precision 111 | // Use 1000 as the scaling factor to preserve up to 3 decimal places 112 | let scaling_factor = 1000; 113 | let scaled_factor = (idle_factor * scaling_factor as f32) as u64; 114 | adjusted_interval = adjusted_interval 115 | .saturating_mul(scaled_factor) 116 | .saturating_div(scaling_factor); 117 | } 118 | // If idle_time_seconds is 0, no factor is applied by this block 119 | } 120 | 121 | // Adjust for CPU/temperature volatility 122 | if params.cpu_volatility > 10.0 || params.temp_volatility > 2.0 { 123 | // For division by 2 (halving the interval), we can safely use integer division 124 | adjusted_interval = (adjusted_interval / 2).max(1); 125 | } 126 | 127 | // Enforce a minimum of 1 second to prevent busy loops, regardless of params.min_interval 128 | let min_safe_interval = params.min_interval.max(1); 129 | let new_interval = adjusted_interval.clamp(min_safe_interval, params.max_interval); 130 | 131 | // Blend the new interval with the cached value if available 132 | let blended_interval = if let Some(cached) = system_history.last_computed_interval { 133 | // Use a weighted average: 70% previous value, 30% new value 134 | // This smooths out drastic changes in polling frequency 135 | const PREVIOUS_VALUE_WEIGHT: u128 = 7; // 70% 136 | const NEW_VALUE_WEIGHT: u128 = 3; // 30% 137 | const TOTAL_WEIGHT: u128 = PREVIOUS_VALUE_WEIGHT + NEW_VALUE_WEIGHT; // 10 138 | 139 | // XXX: Use u128 arithmetic to avoid overflow with large interval values 140 | let result = (u128::from(cached) * PREVIOUS_VALUE_WEIGHT 141 | + u128::from(new_interval) * NEW_VALUE_WEIGHT) 142 | / TOTAL_WEIGHT; 143 | 144 | result as u64 145 | } else { 146 | new_interval 147 | }; 148 | 149 | // Blended result still needs to respect the configured bounds 150 | // Again enforce minimum of 1 second regardless of params.min_interval 151 | Ok(blended_interval.clamp(min_safe_interval, params.max_interval)) 152 | } 153 | 154 | /// Tracks historical system data for "advanced" adaptive polling 155 | #[derive(Debug)] 156 | struct SystemHistory { 157 | /// Last several CPU usage measurements 158 | cpu_usage_history: VecDeque, 159 | /// Last several temperature readings 160 | temperature_history: VecDeque, 161 | /// Time of last detected user activity 162 | last_user_activity: Instant, 163 | /// Previous battery percentage (to calculate discharge rate) 164 | last_battery_percentage: Option, 165 | /// Timestamp of last battery reading 166 | last_battery_timestamp: Option, 167 | /// Battery discharge rate (%/hour) 168 | battery_discharge_rate: Option, 169 | /// Time spent in each system state 170 | state_durations: std::collections::HashMap, 171 | /// Last time a state transition happened 172 | last_state_change: Instant, 173 | /// Current system state 174 | current_state: SystemState, 175 | /// Last computed optimal polling interval 176 | last_computed_interval: Option, 177 | } 178 | 179 | impl Default for SystemHistory { 180 | fn default() -> Self { 181 | Self { 182 | cpu_usage_history: VecDeque::new(), 183 | temperature_history: VecDeque::new(), 184 | last_user_activity: Instant::now(), 185 | last_battery_percentage: None, 186 | last_battery_timestamp: None, 187 | battery_discharge_rate: None, 188 | state_durations: std::collections::HashMap::new(), 189 | last_state_change: Instant::now(), 190 | current_state: SystemState::default(), 191 | last_computed_interval: None, 192 | } 193 | } 194 | } 195 | 196 | impl SystemHistory { 197 | /// Update system history with new report data 198 | fn update(&mut self, report: &SystemReport) { 199 | // Update CPU usage history 200 | if !report.cpu_cores.is_empty() { 201 | let mut total_usage: f32 = 0.0; 202 | let mut core_count: usize = 0; 203 | 204 | for core in &report.cpu_cores { 205 | if let Some(usage) = core.usage_percent { 206 | total_usage += usage; 207 | core_count += 1; 208 | } 209 | } 210 | 211 | if core_count > 0 { 212 | let avg_usage = total_usage / core_count as f32; 213 | 214 | // Keep only the last 5 measurements 215 | if self.cpu_usage_history.len() >= 5 { 216 | self.cpu_usage_history.pop_front(); 217 | } 218 | self.cpu_usage_history.push_back(avg_usage); 219 | 220 | // Update last_user_activity if CPU usage indicates activity 221 | // Consider significant CPU usage or sudden change as user activity 222 | if avg_usage > 20.0 223 | || (self.cpu_usage_history.len() > 1 224 | && (avg_usage - self.cpu_usage_history[self.cpu_usage_history.len() - 2]) 225 | .abs() 226 | > 15.0) 227 | { 228 | self.last_user_activity = Instant::now(); 229 | debug!("User activity detected based on CPU usage"); 230 | } 231 | } 232 | } 233 | 234 | // Update temperature history 235 | if let Some(temp) = report.cpu_global.average_temperature_celsius { 236 | if self.temperature_history.len() >= 5 { 237 | self.temperature_history.pop_front(); 238 | } 239 | self.temperature_history.push_back(temp); 240 | 241 | // Significant temperature increase can indicate user activity 242 | if self.temperature_history.len() > 1 { 243 | let temp_change = 244 | temp - self.temperature_history[self.temperature_history.len() - 2]; 245 | if temp_change > 5.0 { 246 | // 5°C rise in temperature 247 | self.last_user_activity = Instant::now(); 248 | debug!("User activity detected based on temperature change"); 249 | } 250 | } 251 | } 252 | 253 | // Update battery discharge rate 254 | if let Some(battery) = report.batteries.first() { 255 | // Reset when we are charging or have just connected AC 256 | if battery.ac_connected { 257 | // Reset discharge tracking but continue updating the rest of 258 | // the history so we still detect activity/load changes on AC. 259 | self.battery_discharge_rate = None; 260 | self.last_battery_percentage = None; 261 | self.last_battery_timestamp = None; 262 | } 263 | 264 | if let Some(current_percentage) = battery.capacity_percent { 265 | let current_percent = f32::from(current_percentage); 266 | 267 | if let (Some(last_percentage), Some(last_timestamp)) = 268 | (self.last_battery_percentage, self.last_battery_timestamp) 269 | { 270 | let elapsed_hours = last_timestamp.elapsed().as_secs_f32() / 3600.0; 271 | // Only calculate discharge rate if at least 30 seconds have passed 272 | // and we're not on AC power 273 | if elapsed_hours > 0.0083 && !battery.ac_connected { 274 | // 0.0083 hours = 30 seconds 275 | // Calculate discharge rate in percent per hour 276 | let percent_change = last_percentage - current_percent; 277 | if percent_change > 0.0 { 278 | // Only if battery is discharging 279 | let hourly_rate = percent_change / elapsed_hours; 280 | // Clamp the discharge rate to a reasonable maximum value (100%/hour) 281 | let clamped_rate = hourly_rate.min(100.0); 282 | self.battery_discharge_rate = Some(clamped_rate); 283 | } 284 | } 285 | } 286 | 287 | self.last_battery_percentage = Some(current_percent); 288 | self.last_battery_timestamp = Some(Instant::now()); 289 | } 290 | } 291 | 292 | // Update system state tracking 293 | let new_state = determine_system_state(report, self); 294 | if new_state != self.current_state { 295 | // Record time spent in previous state 296 | let time_in_state = self.last_state_change.elapsed(); 297 | *self 298 | .state_durations 299 | .entry(self.current_state.clone()) 300 | .or_insert(Duration::ZERO) += time_in_state; 301 | 302 | // State changes (except to Idle) likely indicate user activity 303 | if new_state != SystemState::Idle && new_state != SystemState::LowLoad { 304 | self.last_user_activity = Instant::now(); 305 | debug!("User activity detected based on system state change to {new_state:?}"); 306 | } 307 | 308 | // Update state 309 | self.current_state = new_state; 310 | self.last_state_change = Instant::now(); 311 | } 312 | 313 | // Check for significant load changes 314 | if report.system_load.load_avg_1min > 1.0 { 315 | self.last_user_activity = Instant::now(); 316 | debug!("User activity detected based on system load"); 317 | } 318 | } 319 | 320 | /// Calculate CPU usage volatility (how much it's changing) 321 | fn get_cpu_volatility(&self) -> f32 { 322 | if self.cpu_usage_history.len() < 2 { 323 | return 0.0; 324 | } 325 | 326 | let mut sum_of_changes = 0.0; 327 | for i in 1..self.cpu_usage_history.len() { 328 | sum_of_changes += (self.cpu_usage_history[i] - self.cpu_usage_history[i - 1]).abs(); 329 | } 330 | 331 | sum_of_changes / (self.cpu_usage_history.len() - 1) as f32 332 | } 333 | 334 | /// Calculate temperature volatility 335 | fn get_temperature_volatility(&self) -> f32 { 336 | if self.temperature_history.len() < 2 { 337 | return 0.0; 338 | } 339 | 340 | let mut sum_of_changes = 0.0; 341 | for i in 1..self.temperature_history.len() { 342 | sum_of_changes += (self.temperature_history[i] - self.temperature_history[i - 1]).abs(); 343 | } 344 | 345 | sum_of_changes / (self.temperature_history.len() - 1) as f32 346 | } 347 | 348 | /// Determine if the system appears to be idle 349 | fn is_system_idle(&self) -> bool { 350 | if self.cpu_usage_history.is_empty() { 351 | return false; 352 | } 353 | 354 | // System considered idle if the average CPU usage of last readings is below 10% 355 | let recent_avg = 356 | self.cpu_usage_history.iter().sum::() / self.cpu_usage_history.len() as f32; 357 | recent_avg < 10.0 && self.get_cpu_volatility() < 5.0 358 | } 359 | 360 | /// Calculate optimal polling interval based on system conditions 361 | fn calculate_optimal_interval( 362 | &self, 363 | config: &AppConfig, 364 | on_battery: bool, 365 | ) -> Result { 366 | let params = IntervalParams { 367 | base_interval: config.daemon.poll_interval_sec, 368 | min_interval: config.daemon.min_poll_interval_sec, 369 | max_interval: config.daemon.max_poll_interval_sec, 370 | cpu_volatility: self.get_cpu_volatility(), 371 | temp_volatility: self.get_temperature_volatility(), 372 | battery_discharge_rate: self.battery_discharge_rate, 373 | last_user_activity: self.last_user_activity.elapsed(), 374 | is_system_idle: self.is_system_idle(), 375 | on_battery, 376 | }; 377 | 378 | compute_new(¶ms, self) 379 | } 380 | } 381 | 382 | /// Validates that poll interval configuration is consistent 383 | /// Returns Ok if configuration is valid, Err with a descriptive message if invalid 384 | fn validate_poll_intervals(min_interval: u64, max_interval: u64) -> Result<(), ControlError> { 385 | if min_interval < 1 { 386 | return Err(ControlError::InvalidValueError( 387 | "min_interval must be ≥ 1".to_string(), 388 | )); 389 | } 390 | if max_interval < 1 { 391 | return Err(ControlError::InvalidValueError( 392 | "max_interval must be ≥ 1".to_string(), 393 | )); 394 | } 395 | if max_interval >= min_interval { 396 | Ok(()) 397 | } else { 398 | Err(ControlError::InvalidValueError(format!( 399 | "Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})" 400 | ))) 401 | } 402 | } 403 | 404 | /// Run the daemon 405 | pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> { 406 | // Set effective log level based on config and verbose flag 407 | let effective_log_level = if verbose { 408 | LogLevel::Debug 409 | } else { 410 | config.daemon.log_level 411 | }; 412 | 413 | // Get the appropriate level filter 414 | let level_filter = match effective_log_level { 415 | LogLevel::Error => LevelFilter::Error, 416 | LogLevel::Warning => LevelFilter::Warn, 417 | LogLevel::Info => LevelFilter::Info, 418 | LogLevel::Debug => LevelFilter::Debug, 419 | }; 420 | 421 | // Update the log level filter if needed, without re-initializing the logger 422 | log::set_max_level(level_filter); 423 | 424 | info!("Starting superfreq daemon..."); 425 | 426 | // Validate critical configuration values before proceeding 427 | if let Err(err) = validate_poll_intervals( 428 | config.daemon.min_poll_interval_sec, 429 | config.daemon.max_poll_interval_sec, 430 | ) { 431 | return Err(AppError::Control(err)); 432 | } 433 | 434 | // Create a flag that will be set to true when a signal is received 435 | let running = Arc::new(AtomicBool::new(true)); 436 | let r = running.clone(); 437 | 438 | // Set up signal handlers 439 | ctrlc::set_handler(move || { 440 | info!("Received shutdown signal, exiting..."); 441 | r.store(false, Ordering::SeqCst); 442 | }) 443 | .map_err(|e| AppError::Generic(format!("Error setting Ctrl-C handler: {e}")))?; 444 | 445 | info!( 446 | "Daemon initialized with poll interval: {}s", 447 | config.daemon.poll_interval_sec 448 | ); 449 | 450 | // Set up stats file if configured 451 | if let Some(stats_path) = &config.daemon.stats_file_path { 452 | info!("Stats will be written to: {stats_path}"); 453 | } 454 | 455 | // Variables for adaptive polling 456 | // Make sure that the poll interval is *never* zero to prevent a busy loop 457 | let mut current_poll_interval = config.daemon.poll_interval_sec.max(1); 458 | if config.daemon.poll_interval_sec == 0 { 459 | warn!("Poll interval is set to zero in config, using 1s minimum to prevent a busy loop"); 460 | } 461 | let mut system_history = SystemHistory::default(); 462 | 463 | // Main loop 464 | while running.load(Ordering::SeqCst) { 465 | let start_time = Instant::now(); 466 | 467 | match monitor::collect_system_report(&config) { 468 | Ok(report) => { 469 | debug!("Collected system report, applying settings..."); 470 | 471 | // Store the current state before updating history 472 | let previous_state = system_history.current_state.clone(); 473 | 474 | // Update system history with new data 475 | system_history.update(&report); 476 | 477 | // Update the stats file if configured 478 | if let Some(stats_path) = &config.daemon.stats_file_path { 479 | if let Err(e) = write_stats_file(stats_path, &report) { 480 | error!("Failed to write stats file: {e}"); 481 | } 482 | } 483 | 484 | match engine::determine_and_apply_settings(&report, &config, None) { 485 | Ok(()) => { 486 | debug!("Successfully applied system settings"); 487 | 488 | // If system state changed, log the new state 489 | if system_history.current_state != previous_state { 490 | info!( 491 | "System state changed to: {:?}", 492 | system_history.current_state 493 | ); 494 | } 495 | } 496 | Err(e) => { 497 | error!("Error applying system settings: {e}"); 498 | } 499 | } 500 | 501 | // Check if we're on battery 502 | let on_battery = !report.batteries.is_empty() 503 | && report.batteries.first().is_some_and(|b| !b.ac_connected); 504 | 505 | // Calculate optimal polling interval if adaptive polling is enabled 506 | if config.daemon.adaptive_interval { 507 | match system_history.calculate_optimal_interval(&config, on_battery) { 508 | Ok(optimal_interval) => { 509 | // Store the new interval 510 | system_history.last_computed_interval = Some(optimal_interval); 511 | 512 | debug!("Recalculated optimal interval: {optimal_interval}s"); 513 | 514 | // Don't change the interval too dramatically at once 515 | match optimal_interval.cmp(¤t_poll_interval) { 516 | std::cmp::Ordering::Greater => { 517 | current_poll_interval = 518 | (current_poll_interval + optimal_interval) / 2; 519 | } 520 | std::cmp::Ordering::Less => { 521 | current_poll_interval = current_poll_interval 522 | - ((current_poll_interval - optimal_interval) / 2).max(1); 523 | } 524 | std::cmp::Ordering::Equal => { 525 | // No change needed when they're equal 526 | } 527 | } 528 | } 529 | Err(e) => { 530 | // Log the error and stop the daemon when an invalid configuration is detected 531 | error!("Critical configuration error: {e}"); 532 | running.store(false, Ordering::SeqCst); 533 | break; 534 | } 535 | } 536 | 537 | // Make sure that we respect the (user) configured min and max limits 538 | current_poll_interval = current_poll_interval.clamp( 539 | config.daemon.min_poll_interval_sec, 540 | config.daemon.max_poll_interval_sec, 541 | ); 542 | 543 | debug!("Adaptive polling: set interval to {current_poll_interval}s"); 544 | } else { 545 | // If adaptive polling is disabled, still apply battery-saving adjustment 546 | if config.daemon.throttle_on_battery && on_battery { 547 | let battery_multiplier = 2; // poll half as often on battery 548 | 549 | // We need to make sure `poll_interval_sec` is *at least* 1 550 | // before multiplying. 551 | let safe_interval = config.daemon.poll_interval_sec.max(1); 552 | current_poll_interval = (safe_interval * battery_multiplier) 553 | .min(config.daemon.max_poll_interval_sec); 554 | 555 | debug!( 556 | "On battery power, increased poll interval to {current_poll_interval}s" 557 | ); 558 | } else { 559 | // Use the configured poll interval 560 | current_poll_interval = config.daemon.poll_interval_sec.max(1); 561 | if config.daemon.poll_interval_sec == 0 { 562 | debug!("Using minimum poll interval of 1s instead of configured 0s"); 563 | } 564 | } 565 | } 566 | } 567 | Err(e) => { 568 | error!("Error collecting system report: {e}"); 569 | } 570 | } 571 | 572 | // Sleep for the remaining time in the poll interval 573 | let elapsed = start_time.elapsed(); 574 | let poll_duration = Duration::from_secs(current_poll_interval); 575 | if elapsed < poll_duration { 576 | let sleep_time = poll_duration - elapsed; 577 | debug!("Sleeping for {}s until next cycle", sleep_time.as_secs()); 578 | std::thread::sleep(sleep_time); 579 | } 580 | } 581 | 582 | info!("Daemon stopped"); 583 | Ok(()) 584 | } 585 | 586 | /// Write current system stats to a file for --stats to read 587 | fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> { 588 | let mut file = File::create(path)?; 589 | 590 | writeln!(file, "timestamp={:?}", report.timestamp)?; 591 | 592 | // CPU info 593 | writeln!(file, "governor={:?}", report.cpu_global.current_governor)?; 594 | writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?; 595 | if let Some(temp) = report.cpu_global.average_temperature_celsius { 596 | writeln!(file, "cpu_temp={temp:.1}")?; 597 | } 598 | 599 | // Battery info 600 | if !report.batteries.is_empty() { 601 | let battery = &report.batteries[0]; 602 | writeln!(file, "ac_power={}", battery.ac_connected)?; 603 | if let Some(cap) = battery.capacity_percent { 604 | writeln!(file, "battery_percent={cap}")?; 605 | } 606 | } 607 | 608 | // System load 609 | writeln!(file, "load_1m={:.2}", report.system_load.load_avg_1min)?; 610 | writeln!(file, "load_5m={:.2}", report.system_load.load_avg_5min)?; 611 | writeln!(file, "load_15m={:.2}", report.system_load.load_avg_15min)?; 612 | 613 | Ok(()) 614 | } 615 | 616 | /// Simplified system state used for determining when to adjust polling interval 617 | #[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] 618 | enum SystemState { 619 | #[default] 620 | Unknown, 621 | OnAC, 622 | OnBattery, 623 | HighLoad, 624 | LowLoad, 625 | HighTemp, 626 | Idle, 627 | } 628 | 629 | /// Determine the current system state for adaptive polling 630 | fn determine_system_state(report: &SystemReport, history: &SystemHistory) -> SystemState { 631 | // Check power state first 632 | if !report.batteries.is_empty() { 633 | if let Some(battery) = report.batteries.first() { 634 | if battery.ac_connected { 635 | return SystemState::OnAC; 636 | } 637 | return SystemState::OnBattery; 638 | } 639 | } 640 | 641 | // No batteries means desktop, so always AC 642 | if report.batteries.is_empty() { 643 | return SystemState::OnAC; 644 | } 645 | 646 | // Check temperature 647 | if let Some(temp) = report.cpu_global.average_temperature_celsius { 648 | if temp > 80.0 { 649 | return SystemState::HighTemp; 650 | } 651 | } 652 | 653 | // Check load first, as high load should take precedence over idle state 654 | let avg_load = report.system_load.load_avg_1min; 655 | if avg_load > 3.0 { 656 | return SystemState::HighLoad; 657 | } 658 | 659 | // Check idle state only if we don't have high load 660 | if history.is_system_idle() { 661 | return SystemState::Idle; 662 | } 663 | 664 | // Check for low load 665 | if avg_load < 0.5 { 666 | return SystemState::LowLoad; 667 | } 668 | 669 | // Default case 670 | SystemState::Unknown 671 | } 672 | -------------------------------------------------------------------------------- /src/engine.rs: -------------------------------------------------------------------------------- 1 | use crate::battery; 2 | use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings}; 3 | use crate::core::{OperationalMode, SystemReport, TurboSetting}; 4 | use crate::cpu::{self}; 5 | use crate::util::error::{ControlError, EngineError}; 6 | use log::{debug, info, warn}; 7 | use std::sync::OnceLock; 8 | use std::sync::atomic::{AtomicBool, Ordering}; 9 | 10 | /// Track turbo boost state for AC and battery power modes 11 | struct TurboHysteresisStates { 12 | /// State for when on AC power 13 | charger: TurboHysteresis, 14 | /// State for when on battery power 15 | battery: TurboHysteresis, 16 | } 17 | 18 | impl TurboHysteresisStates { 19 | const fn new() -> Self { 20 | Self { 21 | charger: TurboHysteresis::new(), 22 | battery: TurboHysteresis::new(), 23 | } 24 | } 25 | 26 | const fn get_for_power_state(&self, is_on_ac: bool) -> &TurboHysteresis { 27 | if is_on_ac { 28 | &self.charger 29 | } else { 30 | &self.battery 31 | } 32 | } 33 | } 34 | 35 | static TURBO_STATES: OnceLock = OnceLock::new(); 36 | 37 | /// Get or initialize the global turbo states 38 | fn get_turbo_states() -> &'static TurboHysteresisStates { 39 | TURBO_STATES.get_or_init(TurboHysteresisStates::new) 40 | } 41 | 42 | /// Manage turbo boost hysteresis state. 43 | /// Contains the state needed to implement hysteresis 44 | /// for the dynamic turbo management feature 45 | struct TurboHysteresis { 46 | /// Whether turbo was enabled in the previous cycle 47 | previous_state: AtomicBool, 48 | /// Whether the hysteresis state has been initialized 49 | initialized: AtomicBool, 50 | } 51 | 52 | impl TurboHysteresis { 53 | const fn new() -> Self { 54 | Self { 55 | previous_state: AtomicBool::new(false), 56 | initialized: AtomicBool::new(false), 57 | } 58 | } 59 | 60 | /// Get the previous turbo state, if initialized 61 | fn get_previous_state(&self) -> Option { 62 | if self.initialized.load(Ordering::Acquire) { 63 | Some(self.previous_state.load(Ordering::Acquire)) 64 | } else { 65 | None 66 | } 67 | } 68 | 69 | /// Initialize the state with a specific value if not already initialized 70 | /// Only one thread should be able to initialize the state 71 | fn initialize_with(&self, initial_state: bool) -> bool { 72 | // First, try to atomically change initialized from false to true 73 | // Only one thread can win the initialization race 74 | match self.initialized.compare_exchange( 75 | false, // expected: not initialized 76 | true, // desired: mark as initialized 77 | Ordering::Release, // success: release for memory visibility 78 | Ordering::Acquire, // failure: just need to acquire the current value 79 | ) { 80 | Ok(_) => { 81 | // We won the race to initialize 82 | // Now it's safe to set the initial state since we know we're the only 83 | // thread that has successfully marked this as initialized 84 | self.previous_state.store(initial_state, Ordering::Release); 85 | initial_state 86 | } 87 | Err(_) => { 88 | // Another thread already initialized it. 89 | // Just read the current state value that was set by the winning thread 90 | self.previous_state.load(Ordering::Acquire) 91 | } 92 | } 93 | } 94 | 95 | /// Update the turbo state for hysteresis 96 | fn update_state(&self, new_state: bool) { 97 | // First store the new state, then mark as initialized 98 | // With this, any thread seeing initialized=true will also see the correct state 99 | self.previous_state.store(new_state, Ordering::Release); 100 | 101 | // Already initialized, no need for compare_exchange 102 | if self.initialized.load(Ordering::Relaxed) { 103 | return; 104 | } 105 | 106 | // Otherwise, try to set initialized=true (but only if it was false) 107 | self.initialized 108 | .compare_exchange( 109 | false, // expected: not initialized 110 | true, // desired: mark as initialized 111 | Ordering::Release, // success: release for memory visibility 112 | Ordering::Relaxed, // failure: we don't care about the current value on failure 113 | ) 114 | .ok(); // Ignore the result. If it fails, it means another thread already initialized it 115 | } 116 | } 117 | 118 | /// Try applying a CPU feature and handle common error cases. Centralizes the where we 119 | /// previously did: 120 | /// 1. Try to apply a feature setting 121 | /// 2. If not supported, log a warning and continue 122 | /// 3. If other error, propagate the error 123 | fn try_apply_feature( 124 | feature_name: &str, 125 | value_description: &str, 126 | apply_fn: F, 127 | ) -> Result<(), EngineError> 128 | where 129 | F: FnOnce() -> Result, 130 | { 131 | info!("Setting {feature_name} to '{value_description}'"); 132 | 133 | match apply_fn() { 134 | Ok(_) => Ok(()), 135 | Err(e) => { 136 | if matches!(e, ControlError::NotSupported(_)) { 137 | warn!( 138 | "{feature_name} setting is not supported on this system. Skipping {feature_name} configuration." 139 | ); 140 | Ok(()) 141 | } else { 142 | // Propagate all other errors, including InvalidValueError 143 | Err(EngineError::ControlError(e)) 144 | } 145 | } 146 | } 147 | } 148 | 149 | /// Determines the appropriate CPU profile based on power status or forced mode, 150 | /// and applies the settings (via helpers defined in the `cpu` module) 151 | pub fn determine_and_apply_settings( 152 | report: &SystemReport, 153 | config: &AppConfig, 154 | force_mode: Option, 155 | ) -> Result<(), EngineError> { 156 | // First, check if there's a governor override set 157 | if let Some(override_governor) = cpu::get_governor_override() { 158 | info!( 159 | "Governor override is active: '{}'. Setting governor.", 160 | override_governor.trim() 161 | ); 162 | 163 | // Apply the override governor setting 164 | try_apply_feature("override governor", override_governor.trim(), || { 165 | cpu::set_governor(override_governor.trim(), None) 166 | })?; 167 | } 168 | 169 | // Determine AC/Battery status once, early in the function 170 | // For desktops (no batteries), we should always use the AC power profile 171 | // For laptops, we check if all batteries report connected to AC 172 | let on_ac_power = if report.batteries.is_empty() { 173 | // No batteries means desktop/server, always on AC 174 | true 175 | } else { 176 | // Check if all batteries report AC connected 177 | report.batteries.iter().all(|b| b.ac_connected) 178 | }; 179 | 180 | let selected_profile_config: &ProfileConfig; 181 | 182 | if let Some(mode) = force_mode { 183 | match mode { 184 | OperationalMode::Powersave => { 185 | info!("Forced Powersave mode selected. Applying 'battery' profile."); 186 | selected_profile_config = &config.battery; 187 | } 188 | OperationalMode::Performance => { 189 | info!("Forced Performance mode selected. Applying 'charger' profile."); 190 | selected_profile_config = &config.charger; 191 | } 192 | } 193 | } else { 194 | // Use the previously computed on_ac_power value 195 | if on_ac_power { 196 | info!("On AC power, selecting Charger profile."); 197 | selected_profile_config = &config.charger; 198 | } else { 199 | info!("On Battery power, selecting Battery profile."); 200 | selected_profile_config = &config.battery; 201 | } 202 | } 203 | 204 | // Apply settings from selected_profile_config 205 | if let Some(governor) = &selected_profile_config.governor { 206 | info!("Setting governor to '{governor}'"); 207 | // Let set_governor handle the validation 208 | if let Err(e) = cpu::set_governor(governor, None) { 209 | // If the governor is not available, log a warning 210 | if matches!(e, ControlError::InvalidGovernor(_)) 211 | || matches!(e, ControlError::NotSupported(_)) 212 | { 213 | warn!( 214 | "Configured governor '{governor}' is not available on this system. Skipping." 215 | ); 216 | } else { 217 | return Err(e.into()); 218 | } 219 | } 220 | } 221 | 222 | if let Some(turbo_setting) = selected_profile_config.turbo { 223 | info!("Setting turbo to '{turbo_setting:?}'"); 224 | match turbo_setting { 225 | TurboSetting::Auto => { 226 | if selected_profile_config.enable_auto_turbo { 227 | debug!("Managing turbo in auto mode based on system conditions"); 228 | manage_auto_turbo(report, selected_profile_config, on_ac_power)?; 229 | } else { 230 | debug!( 231 | "Superfreq's dynamic turbo management is disabled by configuration. Ensuring system uses its default behavior for automatic turbo control." 232 | ); 233 | // Make sure the system is set to its default automatic turbo mode. 234 | // This is important if turbo was previously forced off. 235 | try_apply_feature("Turbo boost", "system default (Auto)", || { 236 | cpu::set_turbo(TurboSetting::Auto) 237 | })?; 238 | } 239 | } 240 | _ => { 241 | try_apply_feature("Turbo boost", &format!("{turbo_setting:?}"), || { 242 | cpu::set_turbo(turbo_setting) 243 | })?; 244 | } 245 | } 246 | } 247 | 248 | if let Some(epp) = &selected_profile_config.epp { 249 | try_apply_feature("EPP", epp, || cpu::set_epp(epp, None))?; 250 | } 251 | 252 | if let Some(epb) = &selected_profile_config.epb { 253 | try_apply_feature("EPB", epb, || cpu::set_epb(epb, None))?; 254 | } 255 | 256 | if let Some(min_freq) = selected_profile_config.min_freq_mhz { 257 | try_apply_feature("min frequency", &format!("{min_freq} MHz"), || { 258 | cpu::set_min_frequency(min_freq, None) 259 | })?; 260 | } 261 | 262 | if let Some(max_freq) = selected_profile_config.max_freq_mhz { 263 | try_apply_feature("max frequency", &format!("{max_freq} MHz"), || { 264 | cpu::set_max_frequency(max_freq, None) 265 | })?; 266 | } 267 | 268 | if let Some(profile) = &selected_profile_config.platform_profile { 269 | try_apply_feature("platform profile", profile, || { 270 | cpu::set_platform_profile(profile) 271 | })?; 272 | } 273 | 274 | // Set battery charge thresholds if configured 275 | if let Some(thresholds) = &selected_profile_config.battery_charge_thresholds { 276 | let start_threshold = thresholds.start; 277 | let stop_threshold = thresholds.stop; 278 | 279 | if start_threshold < stop_threshold && stop_threshold <= 100 { 280 | info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%"); 281 | match battery::set_battery_charge_thresholds(start_threshold, stop_threshold) { 282 | Ok(()) => debug!("Battery charge thresholds set successfully"), 283 | Err(e) => warn!("Failed to set battery charge thresholds: {e}"), 284 | } 285 | } else { 286 | warn!( 287 | "Invalid battery threshold values: start={start_threshold}, stop={stop_threshold}" 288 | ); 289 | } 290 | } 291 | 292 | debug!("Profile settings applied successfully."); 293 | 294 | Ok(()) 295 | } 296 | 297 | fn manage_auto_turbo( 298 | report: &SystemReport, 299 | config: &ProfileConfig, 300 | on_ac_power: bool, 301 | ) -> Result<(), EngineError> { 302 | // Get the auto turbo settings from the config 303 | let turbo_settings = &config.turbo_auto_settings; 304 | 305 | // Validate the complete configuration to ensure it's usable 306 | validate_turbo_auto_settings(turbo_settings)?; 307 | 308 | // Get average CPU temperature and CPU load 309 | let cpu_temp = report.cpu_global.average_temperature_celsius; 310 | 311 | // Check if we have CPU usage data available 312 | let avg_cpu_usage = if report.cpu_cores.is_empty() { 313 | None 314 | } else { 315 | let sum: f32 = report 316 | .cpu_cores 317 | .iter() 318 | .filter_map(|core| core.usage_percent) 319 | .sum(); 320 | let count = report 321 | .cpu_cores 322 | .iter() 323 | .filter(|core| core.usage_percent.is_some()) 324 | .count(); 325 | 326 | if count > 0 { 327 | Some(sum / count as f32) 328 | } else { 329 | None 330 | } 331 | }; 332 | 333 | // Get the previous state or initialize with the configured initial state 334 | let previous_turbo_enabled = { 335 | let turbo_states = get_turbo_states(); 336 | let hysteresis = turbo_states.get_for_power_state(on_ac_power); 337 | if let Some(state) = hysteresis.get_previous_state() { 338 | state 339 | } else { 340 | // Initialize with the configured initial state and return it 341 | hysteresis.initialize_with(turbo_settings.initial_turbo_state) 342 | } 343 | }; 344 | 345 | // Decision logic for enabling/disabling turbo with hysteresis 346 | let enable_turbo = match (cpu_temp, avg_cpu_usage, previous_turbo_enabled) { 347 | // If temperature is too high, disable turbo regardless of load 348 | (Some(temp), _, _) if temp >= turbo_settings.temp_threshold_high => { 349 | info!( 350 | "Auto Turbo: Disabled due to high temperature ({:.1}°C >= {:.1}°C)", 351 | temp, turbo_settings.temp_threshold_high 352 | ); 353 | false 354 | } 355 | 356 | // If load is high enough, enable turbo (unless temp already caused it to disable) 357 | (_, Some(usage), _) if usage >= turbo_settings.load_threshold_high => { 358 | info!( 359 | "Auto Turbo: Enabled due to high CPU load ({:.1}% >= {:.1}%)", 360 | usage, turbo_settings.load_threshold_high 361 | ); 362 | true 363 | } 364 | 365 | // If load is low, disable turbo 366 | (_, Some(usage), _) if usage <= turbo_settings.load_threshold_low => { 367 | info!( 368 | "Auto Turbo: Disabled due to low CPU load ({:.1}% <= {:.1}%)", 369 | usage, turbo_settings.load_threshold_low 370 | ); 371 | false 372 | } 373 | 374 | // In intermediate load range, maintain previous state (hysteresis) 375 | (_, Some(usage), prev_state) 376 | if usage > turbo_settings.load_threshold_low 377 | && usage < turbo_settings.load_threshold_high => 378 | { 379 | info!( 380 | "Auto Turbo: Maintaining previous state ({}) due to intermediate load ({:.1}%)", 381 | if prev_state { "enabled" } else { "disabled" }, 382 | usage 383 | ); 384 | prev_state 385 | } 386 | 387 | // When CPU load data is present but temperature is missing, use the same hysteresis logic 388 | (None, Some(usage), prev_state) => { 389 | info!( 390 | "Auto Turbo: Maintaining previous state ({}) due to missing temperature data (load: {:.1}%)", 391 | if prev_state { "enabled" } else { "disabled" }, 392 | usage 393 | ); 394 | prev_state 395 | } 396 | 397 | // When all metrics are missing, maintain the previous state 398 | (None, None, prev_state) => { 399 | info!( 400 | "Auto Turbo: Maintaining previous state ({}) due to missing all CPU metrics", 401 | if prev_state { "enabled" } else { "disabled" } 402 | ); 403 | prev_state 404 | } 405 | 406 | // Any other cases with partial metrics, maintain previous state for stability 407 | (_, _, prev_state) => { 408 | info!( 409 | "Auto Turbo: Maintaining previous state ({}) due to incomplete CPU metrics", 410 | if prev_state { "enabled" } else { "disabled" } 411 | ); 412 | prev_state 413 | } 414 | }; 415 | 416 | // Save the current state for next time 417 | { 418 | let turbo_states = get_turbo_states(); 419 | let hysteresis = turbo_states.get_for_power_state(on_ac_power); 420 | hysteresis.update_state(enable_turbo); 421 | } 422 | 423 | // Only apply the setting if the state has changed 424 | let changed = previous_turbo_enabled != enable_turbo; 425 | if changed { 426 | let turbo_setting = if enable_turbo { 427 | TurboSetting::Always 428 | } else { 429 | TurboSetting::Never 430 | }; 431 | 432 | info!( 433 | "Auto Turbo: Applying turbo change from {} to {}", 434 | if previous_turbo_enabled { 435 | "enabled" 436 | } else { 437 | "disabled" 438 | }, 439 | if enable_turbo { "enabled" } else { "disabled" } 440 | ); 441 | 442 | match cpu::set_turbo(turbo_setting) { 443 | Ok(()) => { 444 | debug!( 445 | "Auto Turbo: Successfully set turbo to {}", 446 | if enable_turbo { "enabled" } else { "disabled" } 447 | ); 448 | Ok(()) 449 | } 450 | Err(e) => Err(EngineError::ControlError(e)), 451 | } 452 | } else { 453 | debug!( 454 | "Auto Turbo: Maintaining turbo state ({}) - no change needed", 455 | if enable_turbo { "enabled" } else { "disabled" } 456 | ); 457 | Ok(()) 458 | } 459 | } 460 | 461 | fn validate_turbo_auto_settings(settings: &TurboAutoSettings) -> Result<(), EngineError> { 462 | if settings.load_threshold_high <= settings.load_threshold_low 463 | || settings.load_threshold_high > 100.0 464 | || settings.load_threshold_high < 0.0 465 | || settings.load_threshold_low < 0.0 466 | || settings.load_threshold_low > 100.0 467 | { 468 | return Err(EngineError::ConfigurationError( 469 | "Invalid turbo auto settings: load thresholds must be between 0 % and 100 % with high > low" 470 | .to_string(), 471 | )); 472 | } 473 | 474 | // Validate temperature threshold (realistic range for CPU temps in Celsius) 475 | // TODO: different CPUs have different temperature thresholds. While 110 is a good example 476 | // "extreme" case, the upper barrier might be *lower* for some devices. We'll want to fix 477 | // this eventually, or make it configurable. 478 | if settings.temp_threshold_high <= 0.0 || settings.temp_threshold_high > 110.0 { 479 | return Err(EngineError::ConfigurationError( 480 | "Invalid turbo auto settings: temperature threshold must be between 0°C and 110°C" 481 | .to_string(), 482 | )); 483 | } 484 | 485 | Ok(()) 486 | } 487 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod battery; 2 | mod cli; 3 | mod config; 4 | mod core; 5 | mod cpu; 6 | mod daemon; 7 | mod engine; 8 | mod monitor; 9 | mod util; 10 | 11 | use crate::config::AppConfig; 12 | use crate::core::{GovernorOverrideMode, TurboSetting}; 13 | use crate::util::error::{AppError, ControlError}; 14 | use clap::{Parser, value_parser}; 15 | use env_logger::Builder; 16 | use log::{debug, error, info}; 17 | use std::error::Error; 18 | use std::sync::Once; 19 | 20 | #[derive(Parser, Debug)] 21 | #[clap(author, version, about, long_about = None)] 22 | struct Cli { 23 | #[clap(subcommand)] 24 | command: Option, 25 | } 26 | 27 | #[derive(Parser, Debug)] 28 | enum Commands { 29 | /// Display current system information 30 | Info, 31 | /// Run as a daemon in the background 32 | Daemon { 33 | #[clap(long)] 34 | verbose: bool, 35 | }, 36 | /// Set CPU governor 37 | SetGovernor { 38 | governor: String, 39 | #[clap(long)] 40 | core_id: Option, 41 | }, 42 | /// Force a specific governor mode persistently 43 | ForceGovernor { 44 | /// Mode to force: performance, powersave, or reset 45 | #[clap(value_enum)] 46 | mode: GovernorOverrideMode, 47 | }, 48 | /// Set turbo boost behavior 49 | SetTurbo { 50 | #[clap(value_enum)] 51 | setting: TurboSetting, 52 | }, 53 | /// Display comprehensive debug information 54 | Debug, 55 | /// Set Energy Performance Preference (EPP) 56 | SetEpp { 57 | epp: String, 58 | #[clap(long)] 59 | core_id: Option, 60 | }, 61 | /// Set Energy Performance Bias (EPB) 62 | SetEpb { 63 | epb: String, // Typically 0-15 64 | #[clap(long)] 65 | core_id: Option, 66 | }, 67 | /// Set minimum CPU frequency 68 | SetMinFreq { 69 | freq_mhz: u32, 70 | #[clap(long)] 71 | core_id: Option, 72 | }, 73 | /// Set maximum CPU frequency 74 | SetMaxFreq { 75 | freq_mhz: u32, 76 | #[clap(long)] 77 | core_id: Option, 78 | }, 79 | /// Set ACPI platform profile 80 | SetPlatformProfile { profile: String }, 81 | /// Set battery charge thresholds to extend battery lifespan 82 | SetBatteryThresholds { 83 | /// Percentage at which charging starts (when below this value) 84 | #[clap(value_parser = value_parser!(u8).range(0..=99))] 85 | start_threshold: u8, 86 | /// Percentage at which charging stops (when it reaches this value) 87 | #[clap(value_parser = value_parser!(u8).range(1..=100))] 88 | stop_threshold: u8, 89 | }, 90 | } 91 | 92 | fn main() -> Result<(), AppError> { 93 | // Initialize logger once for the entire application 94 | init_logger(); 95 | 96 | let cli = Cli::parse(); 97 | 98 | // Load configuration first, as it might be needed by the monitor module 99 | // E.g., for ignored power supplies 100 | let config = match config::load_config() { 101 | Ok(cfg) => cfg, 102 | Err(e) => { 103 | error!("Error loading configuration: {e}. Using default values."); 104 | // Proceed with default config if loading fails 105 | AppConfig::default() 106 | } 107 | }; 108 | 109 | let command_result: Result<(), AppError> = match cli.command { 110 | // TODO: This will be moved to a different module in the future. 111 | Some(Commands::Info) => match monitor::collect_system_report(&config) { 112 | Ok(report) => { 113 | // Format section headers with proper centering 114 | let format_section = |title: &str| { 115 | let title_len = title.len(); 116 | let total_width = title_len + 8; // 8 is for padding (4 on each side) 117 | let separator = "═".repeat(total_width); 118 | 119 | println!("\n╔{separator}╗"); 120 | 121 | // Calculate centering 122 | println!("║ {title} ║"); 123 | 124 | println!("╚{separator}╝"); 125 | }; 126 | 127 | format_section("System Information"); 128 | println!("CPU Model: {}", report.system_info.cpu_model); 129 | println!("Architecture: {}", report.system_info.architecture); 130 | println!( 131 | "Linux Distribution: {}", 132 | report.system_info.linux_distribution 133 | ); 134 | 135 | // Format timestamp in a readable way 136 | println!("Current Time: {}", jiff::Timestamp::now()); 137 | 138 | format_section("CPU Global Info"); 139 | println!( 140 | "Current Governor: {}", 141 | report 142 | .cpu_global 143 | .current_governor 144 | .as_deref() 145 | .unwrap_or("N/A") 146 | ); 147 | println!( 148 | "Available Governors: {}", // 21 length baseline 149 | report.cpu_global.available_governors.join(", ") 150 | ); 151 | println!( 152 | "Turbo Status: {}", 153 | match report.cpu_global.turbo_status { 154 | Some(true) => "Enabled", 155 | Some(false) => "Disabled", 156 | None => "Unknown", 157 | } 158 | ); 159 | 160 | println!( 161 | "EPP: {}", 162 | report.cpu_global.epp.as_deref().unwrap_or("N/A") 163 | ); 164 | println!( 165 | "EPB: {}", 166 | report.cpu_global.epb.as_deref().unwrap_or("N/A") 167 | ); 168 | println!( 169 | "Platform Profile: {}", 170 | report 171 | .cpu_global 172 | .platform_profile 173 | .as_deref() 174 | .unwrap_or("N/A") 175 | ); 176 | println!( 177 | "CPU Temperature: {}", 178 | report.cpu_global.average_temperature_celsius.map_or_else( 179 | || "N/A (No sensor detected)".to_string(), 180 | |t| format!("{t:.1}°C") 181 | ) 182 | ); 183 | 184 | format_section("CPU Core Info"); 185 | 186 | // Get max core ID length for padding 187 | let max_core_id_len = report 188 | .cpu_cores 189 | .last() 190 | .map_or(1, |core| core.core_id.to_string().len()); 191 | 192 | // Table headers 193 | println!( 194 | " {:>width$} │ {:^10} │ {:^10} │ {:^10} │ {:^7} │ {:^9}", 195 | "Core", 196 | "Current", 197 | "Min", 198 | "Max", 199 | "Usage", 200 | "Temp", 201 | width = max_core_id_len + 4 202 | ); 203 | println!( 204 | " {:─>width$}──┼─{:─^10}─┼─{:─^10}─┼─{:─^10}─┼─{:─^7}─┼─{:─^9}", 205 | "", 206 | "", 207 | "", 208 | "", 209 | "", 210 | "", 211 | width = max_core_id_len + 4 212 | ); 213 | 214 | for core_info in &report.cpu_cores { 215 | // Format frequencies: if current > max, show in a special way 216 | let current_freq = match core_info.current_frequency_mhz { 217 | Some(freq) => { 218 | let max_freq = core_info.max_frequency_mhz.unwrap_or(0); 219 | if freq > max_freq && max_freq > 0 { 220 | // Special format for boosted frequencies 221 | format!("{freq}*") 222 | } else { 223 | format!("{freq}") 224 | } 225 | } 226 | None => "N/A".to_string(), 227 | }; 228 | 229 | // CPU core display 230 | println!( 231 | " Core {:10} │ {:>10} │ {:>10} │ {:>7} │ {:>9}", 232 | core_info.core_id, 233 | format!("{} MHz", current_freq), 234 | format!( 235 | "{} MHz", 236 | core_info 237 | .min_frequency_mhz 238 | .map_or_else(|| "N/A".to_string(), |f| f.to_string()) 239 | ), 240 | format!( 241 | "{} MHz", 242 | core_info 243 | .max_frequency_mhz 244 | .map_or_else(|| "N/A".to_string(), |f| f.to_string()) 245 | ), 246 | format!( 247 | "{}%", 248 | core_info 249 | .usage_percent 250 | .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}")) 251 | ), 252 | format!( 253 | "{}°C", 254 | core_info 255 | .temperature_celsius 256 | .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}")) 257 | ), 258 | width = max_core_id_len 259 | ); 260 | } 261 | 262 | // Only display battery info for systems that have real batteries 263 | // Skip this section entirely on desktop systems 264 | if !report.batteries.is_empty() { 265 | let has_real_batteries = report.batteries.iter().any(|b| { 266 | // Check if any battery has actual battery data 267 | // (as opposed to peripherals like wireless mice) 268 | b.capacity_percent.is_some() || b.power_rate_watts.is_some() 269 | }); 270 | 271 | if has_real_batteries { 272 | format_section("Battery Info"); 273 | for battery_info in &report.batteries { 274 | // Check if this appears to be a real system battery 275 | if battery_info.capacity_percent.is_some() 276 | || battery_info.power_rate_watts.is_some() 277 | { 278 | let power_status = if battery_info.ac_connected { 279 | "Connected to AC" 280 | } else { 281 | "Running on Battery" 282 | }; 283 | 284 | println!("Battery {}:", battery_info.name); 285 | println!(" Power Status: {power_status}"); 286 | println!( 287 | " State: {}", 288 | battery_info.charging_state.as_deref().unwrap_or("Unknown") 289 | ); 290 | 291 | if let Some(capacity) = battery_info.capacity_percent { 292 | println!(" Capacity: {capacity}%"); 293 | } 294 | 295 | if let Some(power) = battery_info.power_rate_watts { 296 | let direction = if power >= 0.0 { 297 | "charging" 298 | } else { 299 | "discharging" 300 | }; 301 | println!( 302 | " Power Rate: {:.2} W ({})", 303 | power.abs(), 304 | direction 305 | ); 306 | } 307 | 308 | // Display charge thresholds if available 309 | if battery_info.charge_start_threshold.is_some() 310 | || battery_info.charge_stop_threshold.is_some() 311 | { 312 | println!( 313 | " Charge Thresholds: {}-{}", 314 | battery_info 315 | .charge_start_threshold 316 | .map_or_else(|| "N/A".to_string(), |t| t.to_string()), 317 | battery_info 318 | .charge_stop_threshold 319 | .map_or_else(|| "N/A".to_string(), |t| t.to_string()) 320 | ); 321 | } 322 | } 323 | } 324 | } 325 | } 326 | 327 | format_section("System Load"); 328 | println!( 329 | "Load Average (1m): {:.2}", 330 | report.system_load.load_avg_1min 331 | ); 332 | println!( 333 | "Load Average (5m): {:.2}", 334 | report.system_load.load_avg_5min 335 | ); 336 | println!( 337 | "Load Average (15m): {:.2}", 338 | report.system_load.load_avg_15min 339 | ); 340 | Ok(()) 341 | } 342 | Err(e) => Err(AppError::Monitor(e)), 343 | }, 344 | Some(Commands::SetGovernor { governor, core_id }) => { 345 | cpu::set_governor(&governor, core_id).map_err(AppError::Control) 346 | } 347 | Some(Commands::ForceGovernor { mode }) => { 348 | cpu::force_governor(mode).map_err(AppError::Control) 349 | } 350 | Some(Commands::SetTurbo { setting }) => cpu::set_turbo(setting).map_err(AppError::Control), 351 | Some(Commands::SetEpp { epp, core_id }) => { 352 | cpu::set_epp(&epp, core_id).map_err(AppError::Control) 353 | } 354 | Some(Commands::SetEpb { epb, core_id }) => { 355 | cpu::set_epb(&epb, core_id).map_err(AppError::Control) 356 | } 357 | Some(Commands::SetMinFreq { freq_mhz, core_id }) => { 358 | // Basic validation for reasonable CPU frequency values 359 | validate_freq(freq_mhz, "Minimum")?; 360 | cpu::set_min_frequency(freq_mhz, core_id).map_err(AppError::Control) 361 | } 362 | Some(Commands::SetMaxFreq { freq_mhz, core_id }) => { 363 | // Basic validation for reasonable CPU frequency values 364 | validate_freq(freq_mhz, "Maximum")?; 365 | cpu::set_max_frequency(freq_mhz, core_id).map_err(AppError::Control) 366 | } 367 | Some(Commands::SetPlatformProfile { profile }) => { 368 | // Get available platform profiles and validate early if possible 369 | match cpu::get_platform_profiles() { 370 | Ok(available_profiles) => { 371 | if available_profiles.contains(&profile) { 372 | info!("Setting platform profile to '{profile}'"); 373 | cpu::set_platform_profile(&profile).map_err(AppError::Control) 374 | } else { 375 | error!( 376 | "Invalid platform profile: '{}'. Available profiles: {}", 377 | profile, 378 | available_profiles.join(", ") 379 | ); 380 | Err(AppError::Generic(format!( 381 | "Invalid platform profile: '{}'. Available profiles: {}", 382 | profile, 383 | available_profiles.join(", ") 384 | ))) 385 | } 386 | } 387 | Err(_e) => { 388 | // If we can't get profiles (e.g., feature not supported), pass through to the function 389 | cpu::set_platform_profile(&profile).map_err(AppError::Control) 390 | } 391 | } 392 | } 393 | Some(Commands::SetBatteryThresholds { 394 | start_threshold, 395 | stop_threshold, 396 | }) => { 397 | // We only need to check if start < stop since the range validation is handled by Clap 398 | if start_threshold >= stop_threshold { 399 | error!( 400 | "Start threshold ({start_threshold}) must be less than stop threshold ({stop_threshold})" 401 | ); 402 | Err(AppError::Generic(format!( 403 | "Start threshold ({start_threshold}) must be less than stop threshold ({stop_threshold})" 404 | ))) 405 | } else { 406 | info!( 407 | "Setting battery thresholds: start at {start_threshold}%, stop at {stop_threshold}%" 408 | ); 409 | battery::set_battery_charge_thresholds(start_threshold, stop_threshold) 410 | .map_err(AppError::Control) 411 | } 412 | } 413 | Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose), 414 | Some(Commands::Debug) => cli::debug::run_debug(&config), 415 | None => { 416 | info!("Welcome to superfreq! Use --help for commands."); 417 | debug!("Current effective configuration: {config:?}"); 418 | Ok(()) 419 | } 420 | }; 421 | 422 | if let Err(e) = command_result { 423 | error!("Error executing command: {e}"); 424 | if let Some(source) = e.source() { 425 | error!("Caused by: {source}"); 426 | } 427 | 428 | // Check for permission denied errors 429 | if let AppError::Control(control_error) = &e { 430 | if matches!(control_error, ControlError::PermissionDenied(_)) { 431 | error!( 432 | "Hint: This operation may require administrator privileges (e.g., run with sudo)." 433 | ); 434 | } 435 | } 436 | 437 | std::process::exit(1); 438 | } 439 | 440 | Ok(()) 441 | } 442 | 443 | /// Initialize the logger for the entire application 444 | static LOGGER_INIT: Once = Once::new(); 445 | fn init_logger() { 446 | LOGGER_INIT.call_once(|| { 447 | // Set default log level based on environment or default to Info 448 | let env_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()); 449 | 450 | Builder::new() 451 | .parse_filters(&env_log) 452 | .format_timestamp(None) 453 | .format_module_path(false) 454 | .init(); 455 | 456 | debug!("Logger initialized with RUST_LOG={env_log}"); 457 | }); 458 | } 459 | 460 | /// Validate CPU frequency input values 461 | fn validate_freq(freq_mhz: u32, label: &str) -> Result<(), AppError> { 462 | if freq_mhz == 0 { 463 | error!("{label} frequency cannot be zero"); 464 | Err(AppError::Generic(format!( 465 | "{label} frequency cannot be zero" 466 | ))) 467 | } else if freq_mhz > 10000 { 468 | // Extremely high value unlikely to be valid 469 | error!("{label} frequency ({freq_mhz} MHz) is unreasonably high"); 470 | Err(AppError::Generic(format!( 471 | "{label} frequency ({freq_mhz} MHz) is unreasonably high" 472 | ))) 473 | } else { 474 | Ok(()) 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /src/util/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | #[derive(Debug, thiserror::Error)] 4 | pub enum ControlError { 5 | #[error("I/O error: {0}")] 6 | Io(#[from] io::Error), 7 | 8 | #[error("Failed to write to sysfs path: {0}")] 9 | WriteError(String), 10 | 11 | #[error("Failed to read sysfs path: {0}")] 12 | ReadError(String), 13 | 14 | #[error("Invalid value for setting: {0}")] 15 | InvalidValueError(String), 16 | 17 | #[error("Control action not supported: {0}")] 18 | NotSupported(String), 19 | 20 | #[error("Permission denied: {0}. Try running with sudo.")] 21 | PermissionDenied(String), 22 | 23 | #[error("Invalid platform control profile {0} supplied, please provide a valid one.")] 24 | InvalidProfile(String), 25 | 26 | #[error("Invalid governor: {0}")] 27 | InvalidGovernor(String), 28 | 29 | #[error("Failed to parse value: {0}")] 30 | ParseError(String), 31 | 32 | #[error("Path missing: {0}")] 33 | PathMissing(String), 34 | } 35 | 36 | #[derive(Debug, thiserror::Error)] 37 | pub enum SysMonitorError { 38 | #[error("I/O error: {0}")] 39 | Io(#[from] io::Error), 40 | 41 | #[error("Failed to read sysfs path: {0}")] 42 | ReadError(String), 43 | 44 | #[error("Failed to parse value: {0}")] 45 | ParseError(String), 46 | 47 | #[error("Failed to parse /proc/stat: {0}")] 48 | ProcStatParseError(String), 49 | } 50 | 51 | #[derive(Debug, thiserror::Error)] 52 | pub enum EngineError { 53 | #[error("CPU control error: {0}")] 54 | ControlError(#[from] ControlError), 55 | 56 | #[error("Configuration error: {0}")] 57 | ConfigurationError(String), 58 | } 59 | 60 | // A unified error type for the entire application 61 | #[derive(Debug, thiserror::Error)] 62 | pub enum AppError { 63 | #[error("{0}")] 64 | Control(#[from] ControlError), 65 | 66 | #[error("{0}")] 67 | Monitor(#[from] SysMonitorError), 68 | 69 | #[error("{0}")] 70 | Engine(#[from] EngineError), 71 | 72 | #[error("{0}")] 73 | Config(#[from] crate::config::ConfigError), 74 | 75 | #[error("{0}")] 76 | Generic(String), 77 | 78 | #[error("I/O error: {0}")] 79 | Io(#[from] io::Error), 80 | } 81 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod sysfs; 3 | -------------------------------------------------------------------------------- /src/util/sysfs.rs: -------------------------------------------------------------------------------- 1 | use crate::util::error::ControlError; 2 | use std::{fs, io, path::Path}; 3 | 4 | /// Write a value to a sysfs file with consistent error handling 5 | /// 6 | /// # Arguments 7 | /// 8 | /// * `path` - The file path to write to 9 | /// * `value` - The string value to write 10 | /// 11 | /// # Errors 12 | /// 13 | /// Returns a `ControlError` variant based on the specific error: 14 | /// - `ControlError::PermissionDenied` if permission is denied 15 | /// - `ControlError::PathMissing` if the path doesn't exist 16 | /// - `ControlError::WriteError` for other I/O errors 17 | pub fn write_sysfs_value(path: impl AsRef, value: &str) -> Result<(), ControlError> { 18 | let p = path.as_ref(); 19 | 20 | fs::write(p, value).map_err(|e| { 21 | let error_msg = format!("Path: {:?}, Value: '{}', Error: {}", p.display(), value, e); 22 | match e.kind() { 23 | io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(error_msg), 24 | io::ErrorKind::NotFound => { 25 | ControlError::PathMissing(format!("Path '{}' does not exist", p.display())) 26 | } 27 | _ => ControlError::WriteError(error_msg), 28 | } 29 | }) 30 | } 31 | 32 | /// Read a value from a sysfs file with consistent error handling 33 | /// 34 | /// # Arguments 35 | /// 36 | /// * `path` - The file path to read from 37 | /// 38 | /// # Returns 39 | /// 40 | /// Returns the trimmed contents of the file as a String 41 | /// 42 | /// # Errors 43 | /// 44 | /// Returns a `ControlError` variant based on the specific error: 45 | /// - `ControlError::PermissionDenied` if permission is denied 46 | /// - `ControlError::PathMissing` if the path doesn't exist 47 | /// - `ControlError::ReadError` for other I/O errors 48 | pub fn read_sysfs_value(path: impl AsRef) -> Result { 49 | let p = path.as_ref(); 50 | fs::read_to_string(p) 51 | .map_err(|e| { 52 | let error_msg = format!("Path: {:?}, Error: {}", p.display(), e); 53 | match e.kind() { 54 | io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(error_msg), 55 | io::ErrorKind::NotFound => { 56 | ControlError::PathMissing(format!("Path '{}' does not exist", p.display())) 57 | } 58 | _ => ControlError::ReadError(error_msg), 59 | } 60 | }) 61 | .map(|s| s.trim().to_string()) 62 | } 63 | 64 | /// Safely check if a path exists and is writable 65 | /// 66 | /// # Arguments 67 | /// 68 | /// * `path` - The file path to check 69 | /// 70 | /// # Returns 71 | /// 72 | /// Returns true if the path exists and is writable, false otherwise 73 | pub fn path_exists_and_writable(path: &Path) -> bool { 74 | if !path.exists() { 75 | return false; 76 | } 77 | 78 | // Try to open the file with write access to verify write permission 79 | fs::OpenOptions::new().write(true).open(path).is_ok() 80 | } 81 | --------------------------------------------------------------------------------