├── .github └── workflows │ └── ddc-hi.yml ├── .gitignore ├── .rustfmt.toml ├── COPYING ├── Cargo.lock ├── Cargo.toml ├── README.md ├── build.rs ├── ci.nix ├── default.nix ├── flake.lock ├── flake.nix ├── lock.nix ├── shell.nix └── src └── lib.rs /.github/workflows/ddc-hi.yml: -------------------------------------------------------------------------------- 1 | env: 2 | CI_ALLOW_ROOT: '1' 3 | CI_CONFIG: ./ci.nix 4 | CI_PLATFORM: gh-actions 5 | jobs: 6 | ci-check: 7 | name: ddc-hi check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - id: checkout 11 | name: git clone 12 | uses: actions/checkout@v3 13 | with: 14 | submodules: true 15 | - id: nix-install 16 | name: nix install 17 | uses: arcnmx/ci/actions/nix/install@v0.6 18 | - id: ci-action-build 19 | name: nix build ci.gh-actions.configFile 20 | uses: arcnmx/ci/actions/nix/build@v0.6 21 | with: 22 | attrs: ci.gh-actions.configFile 23 | out-link: .ci/workflow.yml 24 | - id: ci-action-compare 25 | name: gh-actions compare 26 | uses: arcnmx/ci/actions/nix/run@v0.6 27 | with: 28 | args: -u .github/workflows/ddc-hi.yml .ci/workflow.yml 29 | attrs: nixpkgs.diffutils 30 | command: diff 31 | macos: 32 | name: ddc-hi-macos 33 | runs-on: macos-latest 34 | steps: 35 | - id: checkout 36 | name: git clone 37 | uses: actions/checkout@v3 38 | with: 39 | submodules: true 40 | - id: nix-install 41 | name: nix install 42 | uses: arcnmx/ci/actions/nix/install@v0.6 43 | - id: ci-setup 44 | name: nix setup 45 | uses: arcnmx/ci/actions/nix/run@v0.6 46 | with: 47 | attrs: ci.job.macos.run.setup 48 | quiet: false 49 | - id: ci-dirty 50 | name: nix test dirty 51 | uses: arcnmx/ci/actions/nix/run@v0.6 52 | with: 53 | attrs: ci.job.macos.run.test 54 | command: ci-build-dirty 55 | quiet: false 56 | stdout: ${{ runner.temp }}/ci.build.dirty 57 | - id: ci-test 58 | name: nix test build 59 | uses: arcnmx/ci/actions/nix/run@v0.6 60 | with: 61 | attrs: ci.job.macos.run.test 62 | command: ci-build-realise 63 | ignore-exit-code: true 64 | quiet: false 65 | stdin: ${{ runner.temp }}/ci.build.dirty 66 | - env: 67 | CI_EXIT_CODE: ${{ steps.ci-test.outputs.exit-code }} 68 | id: ci-summary 69 | name: nix test results 70 | uses: arcnmx/ci/actions/nix/run@v0.6 71 | with: 72 | attrs: ci.job.macos.run.test 73 | command: ci-build-summarise 74 | quiet: false 75 | stdin: ${{ runner.temp }}/ci.build.dirty 76 | stdout: ${{ runner.temp }}/ci.build.cache 77 | - env: 78 | CACHIX_SIGNING_KEY: ${{ secrets.CACHIX_SIGNING_KEY }} 79 | id: ci-cache 80 | if: always() 81 | name: nix test cache 82 | uses: arcnmx/ci/actions/nix/run@v0.6 83 | with: 84 | attrs: ci.job.macos.run.test 85 | command: ci-build-cache 86 | quiet: false 87 | stdin: ${{ runner.temp }}/ci.build.cache 88 | nixos: 89 | name: ddc-hi-nixos 90 | runs-on: ubuntu-latest 91 | steps: 92 | - id: checkout 93 | name: git clone 94 | uses: actions/checkout@v3 95 | with: 96 | submodules: true 97 | - id: nix-install 98 | name: nix install 99 | uses: arcnmx/ci/actions/nix/install@v0.6 100 | - id: ci-setup 101 | name: nix setup 102 | uses: arcnmx/ci/actions/nix/run@v0.6 103 | with: 104 | attrs: ci.job.nixos.run.setup 105 | quiet: false 106 | - id: ci-dirty 107 | name: nix test dirty 108 | uses: arcnmx/ci/actions/nix/run@v0.6 109 | with: 110 | attrs: ci.job.nixos.run.test 111 | command: ci-build-dirty 112 | quiet: false 113 | stdout: ${{ runner.temp }}/ci.build.dirty 114 | - id: ci-test 115 | name: nix test build 116 | uses: arcnmx/ci/actions/nix/run@v0.6 117 | with: 118 | attrs: ci.job.nixos.run.test 119 | command: ci-build-realise 120 | ignore-exit-code: true 121 | quiet: false 122 | stdin: ${{ runner.temp }}/ci.build.dirty 123 | - env: 124 | CI_EXIT_CODE: ${{ steps.ci-test.outputs.exit-code }} 125 | id: ci-summary 126 | name: nix test results 127 | uses: arcnmx/ci/actions/nix/run@v0.6 128 | with: 129 | attrs: ci.job.nixos.run.test 130 | command: ci-build-summarise 131 | quiet: false 132 | stdin: ${{ runner.temp }}/ci.build.dirty 133 | stdout: ${{ runner.temp }}/ci.build.cache 134 | - env: 135 | CACHIX_SIGNING_KEY: ${{ secrets.CACHIX_SIGNING_KEY }} 136 | id: ci-cache 137 | if: always() 138 | name: nix test cache 139 | uses: arcnmx/ci/actions/nix/run@v0.6 140 | with: 141 | attrs: ci.job.nixos.run.test 142 | command: ci-build-cache 143 | quiet: false 144 | stdin: ${{ runner.temp }}/ci.build.cache 145 | name: ddc-hi 146 | 'on': 147 | - push 148 | - pull_request 149 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /result* 3 | /.cargo/ 4 | /.gitattributes 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | comment_width = 100 2 | condense_wildcard_suffixes = true 3 | hard_tabs = false 4 | imports_granularity = "One" 5 | group_imports = "One" 6 | match_arm_blocks = false 7 | match_block_trailing_comma = true 8 | force_multiline_blocks = false 9 | max_width = 120 10 | newline_style = "Unix" 11 | normalize_comments = false 12 | overflow_delimited_expr = true 13 | reorder_impl_items = true 14 | reorder_modules = true 15 | tab_spaces = 4 16 | trailing_semicolon = false 17 | unstable_features = true 18 | use_field_init_shorthand = true 19 | use_try_shorthand = true 20 | wrap_comments = true 21 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 arcnmx 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "1.3.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 10 | 11 | [[package]] 12 | name = "byteorder" 13 | version = "1.4.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "core-foundation" 25 | version = "0.9.3" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 28 | dependencies = [ 29 | "core-foundation-sys 0.8.3", 30 | "libc", 31 | ] 32 | 33 | [[package]] 34 | name = "core-foundation-sys" 35 | version = "0.6.2" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" 38 | 39 | [[package]] 40 | name = "core-foundation-sys" 41 | version = "0.8.3" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 44 | 45 | [[package]] 46 | name = "core-graphics" 47 | version = "0.22.3" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" 50 | dependencies = [ 51 | "bitflags", 52 | "core-foundation", 53 | "core-graphics-types", 54 | "foreign-types", 55 | "libc", 56 | ] 57 | 58 | [[package]] 59 | name = "core-graphics-types" 60 | version = "0.1.1" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" 63 | dependencies = [ 64 | "bitflags", 65 | "core-foundation", 66 | "foreign-types", 67 | "libc", 68 | ] 69 | 70 | [[package]] 71 | name = "ddc" 72 | version = "0.2.2" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "ba69f2c53e320fc4abad17cb02bbbf04d1a36f18e9907f347589ec5991b3c6c5" 75 | dependencies = [ 76 | "mccs", 77 | ] 78 | 79 | [[package]] 80 | name = "ddc-hi" 81 | version = "0.5.0" 82 | dependencies = [ 83 | "ddc", 84 | "ddc-i2c", 85 | "ddc-macos", 86 | "ddc-winapi", 87 | "edid", 88 | "log", 89 | "mccs", 90 | "mccs-caps", 91 | "mccs-db", 92 | "nvapi", 93 | "thiserror", 94 | ] 95 | 96 | [[package]] 97 | name = "ddc-i2c" 98 | version = "0.2.1" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "66503057bd41fc21b532b3ebe33b2ec57e5d4971fcfc3844306ebcb499b6c8c2" 101 | dependencies = [ 102 | "ddc", 103 | "i2c", 104 | "i2c-linux", 105 | "resize-slice", 106 | ] 107 | 108 | [[package]] 109 | name = "ddc-macos" 110 | version = "0.2.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "5cbaf316c113cfc30da8856c8104dfb4168b73fdd78562d1542e358fe8299dea" 113 | dependencies = [ 114 | "core-foundation", 115 | "core-foundation-sys 0.8.3", 116 | "core-graphics", 117 | "ddc", 118 | "io-kit-sys", 119 | "mach 0.3.2", 120 | "thiserror", 121 | ] 122 | 123 | [[package]] 124 | name = "ddc-winapi" 125 | version = "0.2.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "3238e71b65c870e236de529546a689202fca64a2eaeec43995d28f6920d7fc9e" 128 | dependencies = [ 129 | "ddc", 130 | "widestring", 131 | "winapi", 132 | ] 133 | 134 | [[package]] 135 | name = "dtoa" 136 | version = "0.4.8" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" 139 | 140 | [[package]] 141 | name = "edid" 142 | version = "0.3.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "24ce75530893d834dcfe3bb67ce0e7dec489484e7cb4423ca31618af4bab24fe" 145 | dependencies = [ 146 | "nom", 147 | ] 148 | 149 | [[package]] 150 | name = "foreign-types" 151 | version = "0.3.2" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 154 | dependencies = [ 155 | "foreign-types-shared", 156 | ] 157 | 158 | [[package]] 159 | name = "foreign-types-shared" 160 | version = "0.1.1" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 163 | 164 | [[package]] 165 | name = "i2c" 166 | version = "0.1.0" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "60c7b7bdd7b3a985fdcf94a0d7d98e7a47fde8b7f22fb55ce1a91cc104a2ce9a" 169 | dependencies = [ 170 | "bitflags", 171 | ] 172 | 173 | [[package]] 174 | name = "i2c-linux" 175 | version = "0.1.2" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "c0268a871aaa071221d6c2875ebedcf64710e59b0d87c68c8faf5e98b87dd2a4" 178 | dependencies = [ 179 | "bitflags", 180 | "i2c", 181 | "i2c-linux-sys", 182 | "resize-slice", 183 | "udev", 184 | ] 185 | 186 | [[package]] 187 | name = "i2c-linux-sys" 188 | version = "0.2.1" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "55cd060ed0016621d3da4ed3a23b0158084de90d1f3a8e59f3d391aacd3bbcf8" 191 | dependencies = [ 192 | "bitflags", 193 | "byteorder", 194 | "libc", 195 | ] 196 | 197 | [[package]] 198 | name = "io-kit-sys" 199 | version = "0.1.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "f21dcc74995dd4cd090b147e79789f8d65959cbfb5f0b118002db869ea3bd0a0" 202 | dependencies = [ 203 | "core-foundation-sys 0.6.2", 204 | "mach 0.2.3", 205 | ] 206 | 207 | [[package]] 208 | name = "libc" 209 | version = "0.2.138" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" 212 | 213 | [[package]] 214 | name = "libudev-sys" 215 | version = "0.1.4" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" 218 | dependencies = [ 219 | "libc", 220 | "pkg-config", 221 | ] 222 | 223 | [[package]] 224 | name = "linked-hash-map" 225 | version = "0.5.6" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 228 | 229 | [[package]] 230 | name = "log" 231 | version = "0.4.17" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 234 | dependencies = [ 235 | "cfg-if", 236 | ] 237 | 238 | [[package]] 239 | name = "mach" 240 | version = "0.2.3" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "86dd2487cdfea56def77b88438a2c915fb45113c5319bfe7e14306ca4cd0b0e1" 243 | dependencies = [ 244 | "libc", 245 | ] 246 | 247 | [[package]] 248 | name = "mach" 249 | version = "0.3.2" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" 252 | dependencies = [ 253 | "libc", 254 | ] 255 | 256 | [[package]] 257 | name = "mccs" 258 | version = "0.1.0" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "74366c6da4179141e0d4551a46799a7e667a68eda60561690d5048bd8e0f8422" 261 | dependencies = [ 262 | "void", 263 | ] 264 | 265 | [[package]] 266 | name = "mccs-caps" 267 | version = "0.1.0" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "d9766b1345aec53f3f1781797e31f7367a8c53871a0e30214d8372fe2ccbe3ce" 270 | dependencies = [ 271 | "mccs", 272 | "nom", 273 | ] 274 | 275 | [[package]] 276 | name = "mccs-db" 277 | version = "0.1.2" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "99726fbbe1e11e2908c461e8fab6c9106a5cb13338cc4feb68a01cced38026d0" 280 | dependencies = [ 281 | "mccs", 282 | "nom", 283 | "serde", 284 | "serde_derive", 285 | "serde_yaml", 286 | ] 287 | 288 | [[package]] 289 | name = "memchr" 290 | version = "1.0.2" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" 293 | dependencies = [ 294 | "libc", 295 | ] 296 | 297 | [[package]] 298 | name = "nom" 299 | version = "3.2.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b" 302 | dependencies = [ 303 | "memchr", 304 | ] 305 | 306 | [[package]] 307 | name = "nvapi" 308 | version = "0.1.4" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "7c63de8cd8362e2c38d1a48dea6ae68e6293a8d8d22a52180d0f8dcc779b3158" 311 | dependencies = [ 312 | "i2c", 313 | "log", 314 | "nvapi-sys", 315 | "void", 316 | ] 317 | 318 | [[package]] 319 | name = "nvapi-sys" 320 | version = "0.1.3" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "b29e9a9393c69ee856bfcf5f76ed1ef32d2c0dd6f58558fd43334278fc1e7ea7" 323 | dependencies = [ 324 | "bitflags", 325 | "winapi", 326 | ] 327 | 328 | [[package]] 329 | name = "pkg-config" 330 | version = "0.3.26" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 333 | 334 | [[package]] 335 | name = "proc-macro2" 336 | version = "1.0.47" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" 339 | dependencies = [ 340 | "unicode-ident", 341 | ] 342 | 343 | [[package]] 344 | name = "quote" 345 | version = "1.0.21" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 348 | dependencies = [ 349 | "proc-macro2", 350 | ] 351 | 352 | [[package]] 353 | name = "resize-slice" 354 | version = "0.1.3" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "8a3cb2f74a9891e76958b9e0ccd269a25b466c3ae3bb3efd71db157248308c4a" 357 | dependencies = [ 358 | "uninitialized", 359 | ] 360 | 361 | [[package]] 362 | name = "serde" 363 | version = "1.0.149" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" 366 | 367 | [[package]] 368 | name = "serde_derive" 369 | version = "1.0.149" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" 372 | dependencies = [ 373 | "proc-macro2", 374 | "quote", 375 | "syn", 376 | ] 377 | 378 | [[package]] 379 | name = "serde_yaml" 380 | version = "0.7.5" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "ef8099d3df28273c99a1728190c7a9f19d444c941044f64adf986bee7ec53051" 383 | dependencies = [ 384 | "dtoa", 385 | "linked-hash-map", 386 | "serde", 387 | "yaml-rust", 388 | ] 389 | 390 | [[package]] 391 | name = "syn" 392 | version = "1.0.105" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" 395 | dependencies = [ 396 | "proc-macro2", 397 | "quote", 398 | "unicode-ident", 399 | ] 400 | 401 | [[package]] 402 | name = "thiserror" 403 | version = "1.0.37" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" 406 | dependencies = [ 407 | "thiserror-impl", 408 | ] 409 | 410 | [[package]] 411 | name = "thiserror-impl" 412 | version = "1.0.37" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" 415 | dependencies = [ 416 | "proc-macro2", 417 | "quote", 418 | "syn", 419 | ] 420 | 421 | [[package]] 422 | name = "udev" 423 | version = "0.2.0" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "47504d1a49b2ea1b133e7ddd1d9f0a83cf03feb9b440c2c470d06db4589cf301" 426 | dependencies = [ 427 | "libc", 428 | "libudev-sys", 429 | ] 430 | 431 | [[package]] 432 | name = "unicode-ident" 433 | version = "1.0.5" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 436 | 437 | [[package]] 438 | name = "uninitialized" 439 | version = "0.0.2" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "74c1aa4511c38276c548406f0b1f5f8b793f000cfb51e18f278a102abd057e81" 442 | 443 | [[package]] 444 | name = "void" 445 | version = "1.0.2" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 448 | 449 | [[package]] 450 | name = "widestring" 451 | version = "0.3.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "a212922ea58fbf5044f83663aa4fc6281ff890f1fd7546c0c3f52f5290831781" 454 | 455 | [[package]] 456 | name = "winapi" 457 | version = "0.3.9" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 460 | dependencies = [ 461 | "winapi-i686-pc-windows-gnu", 462 | "winapi-x86_64-pc-windows-gnu", 463 | ] 464 | 465 | [[package]] 466 | name = "winapi-i686-pc-windows-gnu" 467 | version = "0.4.0" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 470 | 471 | [[package]] 472 | name = "winapi-x86_64-pc-windows-gnu" 473 | version = "0.4.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 476 | 477 | [[package]] 478 | name = "yaml-rust" 479 | version = "0.4.5" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 482 | dependencies = [ 483 | "linked-hash-map", 484 | ] 485 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ddc-hi" 3 | version = "0.5.0" 4 | authors = ["arcnmx"] 5 | build = "build.rs" 6 | edition = "2021" 7 | 8 | description = "High level DDC/CI monitor control" 9 | keywords = ["ddc", "mccs", "vcp", "vesa"] 10 | 11 | documentation = "http://docs.rs/ddc-hi/" 12 | repository = "https://github.com/arcnmx/ddc-hi-rs" 13 | readme = "README.md" 14 | license = "MIT" 15 | 16 | include = [ 17 | "/src/**/*.rs", 18 | "/build.rs", 19 | "/README*", 20 | "/COPYING*", 21 | ] 22 | 23 | [badges] 24 | maintenance = { status = "passively-maintained" } 25 | 26 | [dependencies] 27 | ddc = "0.2" 28 | edid = "0.3" 29 | mccs = "0.1" 30 | mccs-caps = "0.1" 31 | mccs-db = "0.1" 32 | thiserror = "1" 33 | log = "0.4" 34 | 35 | [target.'cfg(target_os = "linux")'.dependencies] 36 | ddc-i2c = { version = "0.2", default-features = false, features = ["with-linux", "with-linux-enumerate"], optional = false } 37 | 38 | [target.'cfg(windows)'.dependencies] 39 | ddc-winapi = { version = "0.2", optional = true } 40 | nvapi = { version = "0.1", default-features = false, features = ["i2c"], optional = true } 41 | ddc-i2c = { version = "0.2", optional = true } 42 | 43 | [target.'cfg(target_os = "macos")'.dependencies] 44 | ddc-macos = { version = "0.2", optional = true } 45 | 46 | [features] 47 | default = ["ddc-i2c", "ddc-winapi", "nvapi", "ddc-macos"] 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ddc-hi 2 | 3 | [![release-badge][]][cargo] [![docs-badge][]][docs] [![license-badge][]][license] 4 | 5 | High level DDC/CI monitor controls. 6 | 7 | ## [Documentation][docs] 8 | 9 | See the [documentation][docs] for up to date information. 10 | 11 | [release-badge]: https://img.shields.io/crates/v/ddc-hi.svg?style=flat-square 12 | [cargo]: https://crates.io/crates/ddc-hi 13 | [docs-badge]: https://img.shields.io/badge/API-docs-blue.svg?style=flat-square 14 | [docs]: http://docs.rs/ddc-hi/ 15 | [license-badge]: https://img.shields.io/badge/license-MIT-ff69b4.svg?style=flat-square 16 | [license]: https://github.com/arcnmx/ddc-hi-rs/blob/master/COPYING 17 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env::var; 2 | 3 | fn feature_enabled(name: &str) -> bool { 4 | var(&format!("CARGO_FEATURE_{}", name.to_uppercase().replace("-", "_"))).is_ok() 5 | } 6 | 7 | fn emit_feature(name: &str) { 8 | println!("cargo:rustc-cfg=feature=\"{}\"", name); 9 | } 10 | 11 | fn main() { 12 | if var("CARGO_CFG_TARGET_OS") == Ok("macos".into()) { 13 | if feature_enabled("ddc-macos") { 14 | emit_feature("has-ddc-macos"); 15 | } 16 | } else if var("CARGO_CFG_UNIX").is_ok() { 17 | if feature_enabled("ddc-i2c") { 18 | emit_feature("has-ddc-i2c"); 19 | } 20 | } 21 | 22 | if var("CARGO_CFG_WINDOWS").is_ok() { 23 | if feature_enabled("ddc-winapi") { 24 | emit_feature("has-ddc-winapi"); 25 | } 26 | if feature_enabled("nvapi") { 27 | emit_feature("has-nvapi"); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ci.nix: -------------------------------------------------------------------------------- 1 | { config, channels, pkgs, lib, ... }: with pkgs; with lib; let 2 | inherit (import ./. { inherit pkgs; }) checks; 3 | in { 4 | config = { 5 | name = "ddc-hi"; 6 | ci = { 7 | version = "v0.6"; 8 | gh-actions.enable = true; 9 | }; 10 | cache.cachix.arc.enable = true; 11 | channels = { 12 | nixpkgs = "22.11"; 13 | }; 14 | tasks = { 15 | build.inputs = singleton checks.test; 16 | }; 17 | jobs = { 18 | nixos = { 19 | tasks = { 20 | windows.inputs = singleton checks.windows; 21 | fmt.inputs = singleton checks.rustfmt; 22 | }; 23 | }; 24 | macos.system = "x86_64-darwin"; 25 | }; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | lockData = builtins.fromJSON (builtins.readFile ./flake.lock); 3 | sourceInfo = lockData.nodes.std.locked; 4 | src = fetchTarball { 5 | url = "https://github.com/${sourceInfo.owner}/${sourceInfo.repo}/archive/${sourceInfo.rev}.tar.gz"; 6 | sha256 = sourceInfo.narHash; 7 | }; 8 | in (import src).Flake.Bootstrap { 9 | path = ./.; 10 | inherit lockData; 11 | loadWith.defaultPackage = null; 12 | } 13 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fl-config": { 4 | "locked": { 5 | "lastModified": 1653159448, 6 | "narHash": "sha256-PvB9ha0r4w6p412MBPP71kS/ZTBnOjxL0brlmyucPBA=", 7 | "owner": "flakelib", 8 | "repo": "fl", 9 | "rev": "fcefb9738d5995308a24cda018a083ccb6b0f460", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "flakelib", 14 | "ref": "config", 15 | "repo": "fl", 16 | "type": "github" 17 | } 18 | }, 19 | "flakelib": { 20 | "inputs": { 21 | "fl-config": "fl-config", 22 | "std": "std" 23 | }, 24 | "locked": { 25 | "lastModified": 1669759641, 26 | "narHash": "sha256-Zc6rZC0gTLZ9AzGG6vlImkYZGRf0bdMNk1fwS06ZqnU=", 27 | "owner": "flakelib", 28 | "repo": "fl", 29 | "rev": "ac03e98777f3f03e694f1bcf0c6a6a304734a263", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "flakelib", 34 | "repo": "fl", 35 | "type": "github" 36 | } 37 | }, 38 | "nix-std": { 39 | "locked": { 40 | "lastModified": 1652644856, 41 | "narHash": "sha256-tRRx4bBFwctWrUhnCNrcRee8Wiol1M7ypQTOPrLLtkc=", 42 | "owner": "flakelib", 43 | "repo": "std", 44 | "rev": "194230d45e99ce84cf25ea8b3ae4774601fc097c", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "flakelib", 49 | "ref": "master", 50 | "repo": "std", 51 | "type": "github" 52 | } 53 | }, 54 | "nixpkgs": { 55 | "locked": { 56 | "lastModified": 1670230006, 57 | "narHash": "sha256-a6XPSCwCQQPdIedTXQKFXTkpgB7vm/iwFje8PEju5HM=", 58 | "owner": "NixOS", 59 | "repo": "nixpkgs", 60 | "rev": "5b9b93b9be4234aaf9cd53e3247a927225095514", 61 | "type": "github" 62 | }, 63 | "original": { 64 | "id": "nixpkgs", 65 | "type": "indirect" 66 | } 67 | }, 68 | "root": { 69 | "inputs": { 70 | "flakelib": "flakelib", 71 | "nixpkgs": "nixpkgs", 72 | "rust": "rust" 73 | } 74 | }, 75 | "rust": { 76 | "inputs": { 77 | "nixpkgs": [ 78 | "nixpkgs" 79 | ] 80 | }, 81 | "locked": { 82 | "lastModified": 1670892108, 83 | "narHash": "sha256-CmfMxBxQNV4j56EgbCRIu++Npwl1xtwoYWoZPnhsSTo=", 84 | "owner": "arcnmx", 85 | "repo": "nixexprs-rust", 86 | "rev": "e7caa9e978907aa7d4a6c244eb69bff1fc987d10", 87 | "type": "github" 88 | }, 89 | "original": { 90 | "owner": "arcnmx", 91 | "repo": "nixexprs-rust", 92 | "type": "github" 93 | } 94 | }, 95 | "std": { 96 | "inputs": { 97 | "nix-std": "nix-std" 98 | }, 99 | "locked": { 100 | "lastModified": 1669759267, 101 | "narHash": "sha256-jBAtT8Hb9XwiW5cBDl2SmOMbRivGoi9H6t78fJUO53g=", 102 | "owner": "flakelib", 103 | "repo": "std", 104 | "rev": "628acb137d798d59d638999da4e1715572f18edb", 105 | "type": "github" 106 | }, 107 | "original": { 108 | "owner": "flakelib", 109 | "repo": "std", 110 | "type": "github" 111 | } 112 | } 113 | }, 114 | "root": "root", 115 | "version": 7 116 | } 117 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "High level DDC/CI monitor control"; 3 | inputs = { 4 | flakelib.url = "github:flakelib/fl"; 5 | nixpkgs = { }; 6 | rust = { 7 | url = "github:arcnmx/nixexprs-rust"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | }; 11 | outputs = { self, flakelib, nixpkgs, rust, ... }@inputs: let 12 | nixlib = nixpkgs.lib; 13 | in flakelib { 14 | inherit inputs; 15 | systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 16 | devShells = { 17 | plain = { 18 | mkShell, hostPlatform 19 | , udev, libiconv 20 | , pkg-config 21 | , CoreGraphics ? darwin.apple_sdk.frameworks.CoreGraphics, darwin ? { } 22 | , enableRust ? true, cargo 23 | , rustTools ? [ ] 24 | , generate 25 | }: mkShell { 26 | inherit rustTools; 27 | buildInputs = 28 | nixlib.optional hostPlatform.isLinux udev 29 | ++ nixlib.optionals hostPlatform.isDarwin [ libiconv CoreGraphics ]; 30 | nativeBuildInputs = nixlib.optional hostPlatform.isLinux pkg-config 31 | ++ nixlib.optional enableRust cargo 32 | ++ [ generate ]; 33 | }; 34 | stable = { rust'stable, outputs'devShells'plain }: let 35 | in outputs'devShells'plain.override { 36 | inherit (rust'stable) mkShell; 37 | enableRust = false; 38 | }; 39 | dev = { rust'unstable, rust-w64-overlay, outputs'devShells'plain }: let 40 | channel = rust'unstable.override { 41 | channelOverlays = [ rust-w64-overlay ]; 42 | }; 43 | in outputs'devShells'plain.override { 44 | inherit (channel) mkShell; 45 | enableRust = false; 46 | rustTools = [ "rust-analyzer" ]; 47 | }; 48 | default = { outputs'devShells }: outputs'devShells.plain; 49 | }; 50 | packages = { 51 | }; 52 | legacyPackages = { callPackageSet }: callPackageSet { 53 | source = { rust'builders }: rust'builders.wrapSource self.lib.crate.src; 54 | 55 | rust-w64 = { pkgsCross'mingwW64 }: import inputs.rust { inherit (pkgsCross'mingwW64) pkgs; }; 56 | rust-w64-overlay = { rust-w64 }: let 57 | target = rust-w64.lib.rustTargetEnvironment { 58 | inherit (rust-w64) pkgs; 59 | rustcFlags = [ "-L native=${rust-w64.pkgs.windows.pthreads}/lib" ]; 60 | }; 61 | in cself: csuper: { 62 | sysroot-std = csuper.sysroot-std ++ [ cself.manifest.targets.${target.triple}.rust-std ]; 63 | cargo-cc = csuper.cargo-cc // cself.context.rlib.cargoEnv { 64 | inherit target; 65 | }; 66 | rustc-cc = csuper.rustc-cc // cself.context.rlib.rustcCcEnv { 67 | inherit target; 68 | }; 69 | }; 70 | 71 | generate = { rust'builders, outputHashes }: rust'builders.generateFiles { 72 | paths = { 73 | "lock.nix" = outputHashes; 74 | }; 75 | }; 76 | outputHashes = { rust'builders }: rust'builders.cargoOutputHashes { 77 | inherit (self.lib) crate; 78 | }; 79 | } { }; 80 | checks = { 81 | rustfmt = { rust'builders, source }: rust'builders.check-rustfmt-unstable { 82 | src = source; 83 | config = ./.rustfmt.toml; 84 | }; 85 | versions = { rust'builders, source }: rust'builders.check-contents { 86 | src = source; 87 | patterns = [ 88 | { path = "src/lib.rs"; docs'rs = { 89 | inherit (self.lib.crate) name version; 90 | }; } 91 | ]; 92 | }; 93 | test = { rustPlatform, outputs'devShells'plain, source }: rustPlatform.buildRustPackage { 94 | pname = self.lib.crate.package.name; 95 | inherit (self.lib.crate) cargoLock version; 96 | inherit (outputs'devShells'plain.override { enableRust = false; }) buildInputs nativeBuildInputs; 97 | src = source; 98 | cargoBuildNoDefaultFeatures = true; 99 | cargoTestFlags = [ "--all-targets" ]; 100 | buildType = "debug"; 101 | meta.name = "cargo test"; 102 | }; 103 | windows = { outputs'checks'test, rust-w64 }: rust-w64.latest.rustPlatform.buildRustPackage { 104 | inherit (outputs'checks'test) pname version src buildType cargoBuildNoDefaultFeatures cargoTestFlags; 105 | inherit (self.lib.crate) cargoLock; 106 | }; 107 | }; 108 | lib = with nixlib; { 109 | crate = rust.lib.importCargo { 110 | inherit self; 111 | path = ./Cargo.toml; 112 | inherit (import ./lock.nix) outputHashes; 113 | }; 114 | inherit (self.lib.crate.package) version; 115 | releaseTag = "v${self.lib.version}"; 116 | }; 117 | config = rec { 118 | name = "ddc-hi-rs"; 119 | packages.namespace = [ name ]; 120 | }; 121 | }; 122 | } 123 | -------------------------------------------------------------------------------- /lock.nix: -------------------------------------------------------------------------------- 1 | { 2 | outputHashes = { 3 | 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: (import ./. { inherit pkgs; }).devShells.default 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![doc(html_root_url = "https://docs.rs/ddc-hi/0.5.0")] 3 | 4 | //! High level DDC/CI monitor controls. 5 | //! 6 | //! # Example 7 | //! 8 | //! ```rust,no_run 9 | //! use ddc_hi::{Ddc, Display}; 10 | //! 11 | //! for mut display in Display::enumerate() { 12 | //! display.update_capabilities().unwrap(); 13 | //! println!("{:?} {}: {:?} {:?}", 14 | //! display.info.backend, display.info.id, 15 | //! display.info.manufacturer_id, display.info.model_name 16 | //! ); 17 | //! if let Some(feature) = display.info.mccs_database.get(0xdf) { 18 | //! let value = display.handle.get_vcp_feature(feature.code).unwrap(); 19 | //! println!("{}: {:?}", feature.name.as_ref().unwrap(), value); 20 | //! } 21 | //! } 22 | //! ``` 23 | 24 | pub use ddc::{Ddc, DdcHost, DdcTable, FeatureCode, TimingMessage, VcpValue, VcpValueType}; 25 | use { 26 | ddc::Edid, 27 | log::{trace, warn}, 28 | std::{fmt, io, iter::FromIterator, str}, 29 | thiserror::Error, 30 | }; 31 | 32 | /// The error type for high level DDC/CI monitor operations. 33 | #[derive(Debug, Error)] 34 | pub enum Error { 35 | /// Unsupported operation. 36 | #[error("the backend does not support the operation")] 37 | UnsupportedOp, 38 | 39 | /// An error occurred while reading the capabilities. 40 | #[error("failed to read capabilities string: {0}")] 41 | CapabilitiesReadError(BackendError), 42 | 43 | /// An error occurred while parsing MCCS capabilities. 44 | #[error("failed to parse MCCS capabilities: {0}")] 45 | CapabilitiesParseError(io::Error), 46 | 47 | /// Low level errors. 48 | #[error("low level error: {0}")] 49 | LowLevelError(#[from] BackendError), 50 | } 51 | 52 | /// A wrapper for the DDC backend errors. 53 | #[derive(Debug, Error)] 54 | pub enum BackendError { 55 | #[cfg(feature = "has-ddc-i2c")] 56 | /// I2c error. 57 | #[error("i2c error: {0}")] 58 | I2cDeviceError(ddc_i2c::Error), 59 | 60 | #[cfg(feature = "has-ddc-winapi")] 61 | /// Windows API error. 62 | #[error("winapi error: {0}")] 63 | WinApiError(::Error), 64 | 65 | #[cfg(feature = "has-ddc-macos")] 66 | /// MacOS API error. 67 | #[error("macOS API error: {0}")] 68 | MacOsError(::Error), 69 | 70 | // NOTE: We use ddc-i2c instead of has-... because the latter actually means 71 | // ddc-i2c enabled on a Unix platform. 72 | #[cfg(all(feature = "has-nvapi", feature = "ddc-i2c"))] 73 | /// Nvapi error. 74 | #[error("nvapi error: {0}")] 75 | NvapiError(ddc_i2c::Error), 76 | } 77 | 78 | /// Identifying information about an attached display. 79 | /// 80 | /// Not all information will be available, particularly on backends like 81 | /// WinAPI that do not support EDID. 82 | //#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 83 | #[derive(Clone, Debug)] 84 | pub struct DisplayInfo { 85 | /// Identifies the backend or driver used to communicate with the display. 86 | pub backend: Backend, 87 | /// A unique identifier for the display, format is specific to the backend. 88 | pub id: String, 89 | /// A three-character identifier of the manufacturer of the display. 90 | pub manufacturer_id: Option, 91 | /// A number that identifies the product model. 92 | pub model_id: Option, 93 | /// The version and revision of the product. 94 | pub version: Option<(u8, u8)>, 95 | /// Serial number of the device 96 | pub serial: Option, 97 | /// Year the display was manufactured. 98 | pub manufacture_year: Option, 99 | /// Week the display was manufactured. 100 | pub manufacture_week: Option, 101 | /// The model name of the display. 102 | pub model_name: Option, 103 | /// Human-readable serial number of the device. 104 | pub serial_number: Option, 105 | /// Raw EDID data provided by the display. 106 | pub edid_data: Option>, 107 | /// MCCS VCP version code. 108 | pub mccs_version: Option, 109 | /// MCCS VCP feature information. 110 | pub mccs_database: mccs_db::Database, 111 | } 112 | 113 | impl fmt::Display for DisplayInfo { 114 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 115 | write!(f, "{}:{}", self.backend, self.id)?; 116 | 117 | if let Some(s) = &self.manufacturer_id { 118 | write!(f, " {}", s)?; 119 | } 120 | 121 | if let Some(s) = &self.model_name { 122 | write!(f, " {}", s)?; 123 | } else if let Some(s) = &self.model_id { 124 | write!(f, " {}", s)?; 125 | } 126 | 127 | Ok(()) 128 | } 129 | } 130 | 131 | impl DisplayInfo { 132 | /// Create an empty `DisplayInfo`. 133 | pub fn new(backend: Backend, id: String) -> Self { 134 | DisplayInfo { 135 | backend, 136 | id, 137 | manufacturer_id: None, 138 | model_id: None, 139 | version: None, 140 | serial: None, 141 | manufacture_year: None, 142 | manufacture_week: None, 143 | model_name: None, 144 | serial_number: None, 145 | edid_data: None, 146 | mccs_version: None, 147 | mccs_database: Default::default(), 148 | } 149 | } 150 | 151 | /// Creates a new `DisplayInfo` from unparsed EDID data. 152 | /// 153 | /// May fail to parse the EDID data. 154 | pub fn from_edid(backend: Backend, id: String, edid_data: Vec) -> io::Result { 155 | trace!("DisplayInfo::from_edid({:?}, {})", backend, id); 156 | 157 | let edid = edid::parse(&edid_data) 158 | .to_result() 159 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 160 | 161 | let mut model_name = None; 162 | let mut serial_number = None; 163 | 164 | for desc in edid.descriptors { 165 | match desc { 166 | edid::Descriptor::SerialNumber(serial) => serial_number = Some(serial), 167 | edid::Descriptor::ProductName(model) => model_name = Some(model), 168 | _ => (), 169 | } 170 | } 171 | 172 | Ok(DisplayInfo { 173 | backend, 174 | id, 175 | edid_data: Some(edid_data), 176 | manufacturer_id: Some(String::from_iter(edid.header.vendor.iter())), 177 | model_id: Some(edid.header.product), 178 | serial: Some(edid.header.serial), 179 | version: Some((edid.header.version, edid.header.revision)), 180 | manufacture_year: Some(edid.header.year), 181 | manufacture_week: Some(edid.header.week), 182 | model_name, 183 | serial_number, 184 | mccs_version: None, 185 | mccs_database: Default::default(), 186 | }) 187 | } 188 | 189 | /// Create a new `DisplayInfo` from parsed capabilities. 190 | pub fn from_capabilities(backend: Backend, id: String, caps: &mccs::Capabilities) -> Self { 191 | trace!("DisplayInfo::from_capabilities({:?}, {})", backend, id); 192 | 193 | let edid = caps 194 | .edid 195 | .clone() 196 | .map(|edid| Self::from_edid(backend, id.clone(), edid)) 197 | .transpose(); 198 | 199 | let mut res = DisplayInfo { 200 | backend, 201 | id, 202 | model_name: caps.model.clone(), 203 | mccs_version: caps.mccs_version.clone(), 204 | edid_data: caps.edid.clone(), 205 | // TODO: VDIF 206 | serial_number: None, 207 | manufacturer_id: None, 208 | model_id: None, 209 | serial: None, 210 | version: None, 211 | manufacture_year: None, 212 | manufacture_week: None, 213 | mccs_database: Default::default(), 214 | }; 215 | 216 | if let Some(ver) = res.mccs_version.as_ref() { 217 | res.mccs_database = mccs_db::Database::from_version(ver); 218 | res.mccs_database.apply_capabilities(caps); 219 | } 220 | 221 | match edid { 222 | Ok(Some(edid)) => { 223 | // TODO: should this be edid.update_from(&res) instead? 224 | res.update_from(&edid); 225 | }, 226 | Ok(None) => (), 227 | Err(e) => { 228 | warn!("Failed to parse edid from caps of {}: {}", res, e); 229 | }, 230 | } 231 | 232 | res 233 | } 234 | 235 | /// Merge in any missing information from another `DisplayInfo` 236 | pub fn update_from(&mut self, info: &DisplayInfo) { 237 | if self.manufacturer_id.is_none() { 238 | self.manufacturer_id = info.manufacturer_id.clone() 239 | } 240 | 241 | if self.model_id.is_none() { 242 | self.model_id = info.model_id.clone() 243 | } 244 | 245 | if self.version.is_none() { 246 | self.version = info.version.clone() 247 | } 248 | 249 | if self.serial.is_none() { 250 | self.serial = info.serial.clone() 251 | } 252 | 253 | if self.manufacture_year.is_none() { 254 | self.manufacture_year = info.manufacture_year.clone() 255 | } 256 | 257 | if self.manufacture_week.is_none() { 258 | self.manufacture_week = info.manufacture_week.clone() 259 | } 260 | 261 | if self.model_name.is_none() { 262 | self.model_name = info.model_name.clone() 263 | } 264 | 265 | if self.serial_number.is_none() { 266 | self.serial_number = info.serial_number.clone() 267 | } 268 | 269 | if self.edid_data.is_none() { 270 | self.edid_data = info.edid_data.clone() 271 | } 272 | 273 | if self.mccs_version.is_none() { 274 | self.mccs_version = info.mccs_version.clone() 275 | } 276 | 277 | if self.mccs_database.get(0xdf).is_none() { 278 | if info.mccs_version.is_some() { 279 | self.mccs_version = info.mccs_version.clone() 280 | } 281 | self.mccs_database = info.mccs_database.clone() 282 | } 283 | } 284 | 285 | /// Populate information from a DDC connection. 286 | /// 287 | /// This will read the VCP Version (`0xdf`) and fill in the `mccs_database`. 288 | /// This data will be incomplete compared to being filled in from a capability 289 | /// string. 290 | pub fn update_from_ddc(&mut self, ddc: &mut D) -> Result<(), D::Error> { 291 | if self.mccs_version.is_none() { 292 | trace!("DisplayInfo::update_from_ddc"); 293 | 294 | let version = ddc.get_vcp_feature(0xdf)?; 295 | let version = mccs::Version::new(version.sh, version.sl); 296 | if version != mccs::Version::default() { 297 | self.mccs_version = Some(version); 298 | self.mccs_database = mccs_db::Database::from_version(&version); 299 | } 300 | } 301 | 302 | Ok(()) 303 | } 304 | } 305 | 306 | /// A query to filter out matching displays. 307 | /// 308 | /// Most comparisons must match the full string. 309 | pub enum Query { 310 | /// Matches any display 311 | Any, 312 | /// Matches a display on the given backend 313 | Backend(Backend), 314 | /// Matches a display with the specified ID 315 | Id(String), 316 | /// Matches a display with the specified manufacturer 317 | ManufacturerId(String), 318 | /// Matches a display with the specified model name 319 | ModelName(String), 320 | /// Matches a display with the specified serial number 321 | SerialNumber(String), 322 | /// At least one of the queries must match 323 | Or(Vec), 324 | /// All of the queries must match 325 | And(Vec), 326 | } 327 | 328 | impl Query { 329 | /// Queries whether the provided display info is a match. 330 | pub fn matches(&self, info: &DisplayInfo) -> bool { 331 | match *self { 332 | Query::Any => true, 333 | Query::Backend(backend) => info.backend == backend, 334 | Query::Id(ref id) => &info.id == id, 335 | Query::ManufacturerId(ref id) => info.manufacturer_id.as_ref() == Some(id), 336 | Query::ModelName(ref model) => info.model_name.as_ref() == Some(model), 337 | Query::SerialNumber(ref serial) => info.serial_number.as_ref() == Some(serial), 338 | Query::Or(ref query) => query.iter().any(|q| q.matches(info)), 339 | Query::And(ref query) => query.iter().all(|q| q.matches(info)), 340 | } 341 | } 342 | } 343 | 344 | /// Identifies the backend driver used to communicate with a display. 345 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 346 | pub enum Backend { 347 | /// Linux i2c-dev driver 348 | I2cDevice, 349 | /// Windows Monitor Configuration API 350 | WinApi, 351 | /// NVIDIA NVAPI driver 352 | Nvapi, 353 | /// MacOS APIs 354 | MacOS, 355 | } 356 | 357 | impl fmt::Display for Backend { 358 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 359 | write!(f, "{}", match *self { 360 | Backend::I2cDevice => "i2c-dev", 361 | Backend::WinApi => "winapi", 362 | Backend::Nvapi => "nvapi", 363 | Backend::MacOS => "macos", 364 | }) 365 | } 366 | } 367 | 368 | impl str::FromStr for Backend { 369 | type Err = (); 370 | 371 | fn from_str(s: &str) -> Result { 372 | Ok(match s { 373 | "i2c-dev" => Backend::I2cDevice, 374 | "winapi" => Backend::WinApi, 375 | "nvapi" => Backend::Nvapi, 376 | "macos" => Backend::MacOS, 377 | _ => return Err(()), 378 | }) 379 | } 380 | } 381 | 382 | impl Backend { 383 | /// Enumerate the possible backends. 384 | /// 385 | /// Backends not supported for the current platform will be excluded. 386 | pub fn values() -> &'static [Backend] { 387 | &[ 388 | #[cfg(feature = "has-ddc-i2c")] 389 | Backend::I2cDevice, 390 | #[cfg(feature = "has-ddc-winapi")] 391 | Backend::WinApi, 392 | #[cfg(feature = "has-nvapi")] 393 | Backend::Nvapi, 394 | #[cfg(feature = "has-ddc-macos")] 395 | Backend::MacOS, 396 | ] 397 | } 398 | } 399 | 400 | /// An active handle to a connected display. 401 | pub struct Display { 402 | /// The inner communication handle used for DDC commands. 403 | pub handle: Handle, 404 | /// Information about the connected display. 405 | pub info: DisplayInfo, 406 | filled_caps: bool, 407 | } 408 | 409 | impl Display { 410 | /// Create a new display from the specified handle. 411 | pub fn new(handle: Handle, info: DisplayInfo) -> Self { 412 | Display { 413 | handle, 414 | info, 415 | filled_caps: false, 416 | } 417 | } 418 | 419 | /// Enumerate all detected displays. 420 | pub fn enumerate() -> Vec { 421 | let mut displays = Vec::new(); 422 | 423 | #[cfg(feature = "has-ddc-i2c")] 424 | { 425 | use std::os::unix::fs::MetadataExt; 426 | 427 | if let Ok(devs) = ddc_i2c::I2cDeviceEnumerator::new() { 428 | displays.extend( 429 | devs.map(|mut ddc| -> Result<_, String> { 430 | let id = ddc 431 | .inner_ref() 432 | .inner_ref() 433 | .metadata() 434 | .map(|meta| meta.rdev()) 435 | .unwrap_or(Default::default()); 436 | let mut edid = vec![0u8; 0x100]; 437 | ddc.read_edid(0, &mut edid) 438 | .map_err(|e| format!("failed to read EDID for i2c-{}: {}", id, e))?; 439 | let info = DisplayInfo::from_edid(Backend::I2cDevice, id.to_string(), edid) 440 | .map_err(|e| format!("failed to parse EDID for i2c-{}: {}", id, e))?; 441 | Ok(Display::new(Handle::I2cDevice(ddc), info)) 442 | }) 443 | .filter_map(|d| match d { 444 | Ok(v) => Some(v), 445 | Err(e) => { 446 | warn!("Failed to enumerate a display: {}", e); 447 | None 448 | }, 449 | }), 450 | ) 451 | } 452 | } 453 | 454 | #[cfg(feature = "has-ddc-winapi")] 455 | { 456 | if let Ok(devs) = ddc_winapi::Monitor::enumerate() { 457 | displays.extend(devs.into_iter().map(|ddc| { 458 | let info = DisplayInfo::new(Backend::WinApi, ddc.description()); 459 | Display::new(Handle::WinApi(ddc), info) 460 | })) 461 | } 462 | } 463 | 464 | #[cfg(feature = "has-ddc-macos")] 465 | { 466 | if let Ok(devs) = ddc_macos::Monitor::enumerate() { 467 | displays.extend(devs.into_iter().map(|ddc| { 468 | let info = ddc 469 | .edid() 470 | .and_then(|edid| DisplayInfo::from_edid(Backend::MacOS, ddc.description(), edid).ok()) 471 | .unwrap_or(DisplayInfo::new(Backend::MacOS, ddc.description())); 472 | Display::new(Handle::MacOS(ddc), info) 473 | })) 474 | } 475 | } 476 | 477 | #[cfg(feature = "has-nvapi")] 478 | { 479 | use std::rc::Rc; 480 | 481 | if let Ok(_) = nvapi::initialize() { 482 | if let Ok(gpus) = nvapi::PhysicalGpu::enumerate() { 483 | for gpu in gpus { 484 | let gpu = Rc::new(gpu); 485 | let id_prefix = gpu.short_name().unwrap_or("NVAPI".into()); 486 | if let Ok(ids) = gpu.display_ids_connected(nvapi::ConnectedIdsFlags::empty()) { 487 | for id in ids { 488 | // TODO: it says mask, is it actually `1< displays.push(ddc), 514 | Err(e) => warn!( 515 | "Failed to enumerate NVAPI display {}/{}:{:?}: {}", 516 | id_prefix, id.display_id, id.connector, e 517 | ), 518 | } 519 | } 520 | } 521 | } 522 | } 523 | } 524 | } 525 | 526 | displays 527 | } 528 | 529 | /// Updates the display info with data retrieved from the device's 530 | /// reported capabilities. 531 | pub fn update_capabilities(&mut self) -> Result<(), Error> { 532 | if !self.filled_caps { 533 | let (backend, id) = (self.info.backend, self.info.id.clone()); 534 | let caps = self.handle.capabilities()?; 535 | let info = DisplayInfo::from_capabilities(backend, id, &caps); 536 | if info.mccs_version.is_some() { 537 | self.info.mccs_database = Default::default(); 538 | } 539 | self.info.update_from(&info); 540 | } 541 | 542 | Ok(()) 543 | } 544 | 545 | /// Update some display info. 546 | pub fn update_from_ddc(&mut self) -> Result<(), Error> { 547 | self.info.update_from_ddc(&mut self.handle) 548 | } 549 | } 550 | 551 | /// A handle allowing communication with a display 552 | pub enum Handle { 553 | #[doc(hidden)] 554 | #[cfg(feature = "has-ddc-i2c")] 555 | I2cDevice(ddc_i2c::I2cDeviceDdc), 556 | #[doc(hidden)] 557 | #[cfg(feature = "has-ddc-winapi")] 558 | WinApi(ddc_winapi::Monitor), 559 | #[doc(hidden)] 560 | #[cfg(feature = "has-ddc-macos")] 561 | MacOS(ddc_macos::Monitor), 562 | #[doc(hidden)] 563 | #[cfg(feature = "has-nvapi")] 564 | Nvapi(ddc_i2c::I2cDdc>>), 565 | } 566 | 567 | impl Handle { 568 | /// Request and parse the display's capabilities string. 569 | pub fn capabilities(&mut self) -> Result { 570 | mccs_caps::parse_capabilities(&self.capabilities_string()?).map_err(Error::CapabilitiesParseError) 571 | } 572 | } 573 | 574 | impl ddc::DdcHost for Handle { 575 | type Error = Error; 576 | 577 | fn sleep(&mut self) { 578 | match *self { 579 | #[cfg(feature = "has-ddc-i2c")] 580 | Handle::I2cDevice(ref mut i2c) => i2c.sleep(), 581 | #[cfg(feature = "has-ddc-winapi")] 582 | Handle::WinApi(ref mut monitor) => monitor.sleep(), 583 | #[cfg(feature = "has-ddc-macos")] 584 | Handle::MacOS(ref mut monitor) => monitor.sleep(), 585 | #[cfg(feature = "has-nvapi")] 586 | Handle::Nvapi(ref mut i2c) => i2c.sleep(), 587 | } 588 | } 589 | } 590 | 591 | impl Ddc for Handle { 592 | fn capabilities_string(&mut self) -> Result, Self::Error> { 593 | match *self { 594 | #[cfg(feature = "has-ddc-i2c")] 595 | Handle::I2cDevice(ref mut i2c) => i2c.capabilities_string().map_err(BackendError::I2cDeviceError), 596 | #[cfg(feature = "has-ddc-winapi")] 597 | Handle::WinApi(ref mut monitor) => monitor.capabilities_string().map_err(BackendError::WinApiError), 598 | #[cfg(feature = "has-ddc-macos")] 599 | Handle::MacOS(ref mut monitor) => monitor.capabilities_string().map_err(BackendError::MacOsError), 600 | #[cfg(feature = "has-nvapi")] 601 | Handle::Nvapi(ref mut i2c) => i2c.capabilities_string().map_err(BackendError::NvapiError), 602 | } 603 | .map_err(Error::CapabilitiesReadError) 604 | } 605 | 606 | fn get_vcp_feature(&mut self, code: FeatureCode) -> Result { 607 | match *self { 608 | #[cfg(feature = "has-ddc-i2c")] 609 | Handle::I2cDevice(ref mut i2c) => i2c.get_vcp_feature(code).map_err(BackendError::I2cDeviceError), 610 | #[cfg(feature = "has-ddc-winapi")] 611 | Handle::WinApi(ref mut monitor) => monitor.get_vcp_feature(code).map_err(BackendError::WinApiError), 612 | #[cfg(feature = "has-ddc-macos")] 613 | Handle::MacOS(ref mut monitor) => monitor.get_vcp_feature(code).map_err(BackendError::MacOsError), 614 | #[cfg(feature = "has-nvapi")] 615 | Handle::Nvapi(ref mut i2c) => i2c.get_vcp_feature(code).map_err(BackendError::NvapiError), 616 | } 617 | .map_err(From::from) 618 | } 619 | 620 | fn set_vcp_feature(&mut self, code: FeatureCode, value: u16) -> Result<(), Self::Error> { 621 | match *self { 622 | #[cfg(feature = "has-ddc-i2c")] 623 | Handle::I2cDevice(ref mut i2c) => i2c.set_vcp_feature(code, value).map_err(BackendError::I2cDeviceError), 624 | #[cfg(feature = "has-ddc-winapi")] 625 | Handle::WinApi(ref mut monitor) => monitor.set_vcp_feature(code, value).map_err(BackendError::WinApiError), 626 | #[cfg(feature = "has-ddc-macos")] 627 | Handle::MacOS(ref mut monitor) => monitor.set_vcp_feature(code, value).map_err(BackendError::MacOsError), 628 | #[cfg(feature = "has-nvapi")] 629 | Handle::Nvapi(ref mut i2c) => i2c.set_vcp_feature(code, value).map_err(BackendError::NvapiError), 630 | } 631 | .map_err(From::from) 632 | } 633 | 634 | fn save_current_settings(&mut self) -> Result<(), Self::Error> { 635 | match *self { 636 | #[cfg(feature = "has-ddc-i2c")] 637 | Handle::I2cDevice(ref mut i2c) => i2c.save_current_settings().map_err(BackendError::I2cDeviceError), 638 | #[cfg(feature = "has-ddc-winapi")] 639 | Handle::WinApi(ref mut monitor) => monitor.save_current_settings().map_err(BackendError::WinApiError), 640 | #[cfg(feature = "has-ddc-macos")] 641 | Handle::MacOS(ref mut monitor) => monitor.save_current_settings().map_err(BackendError::MacOsError), 642 | #[cfg(feature = "has-nvapi")] 643 | Handle::Nvapi(ref mut i2c) => i2c.save_current_settings().map_err(BackendError::NvapiError), 644 | } 645 | .map_err(From::from) 646 | } 647 | 648 | fn get_timing_report(&mut self) -> Result { 649 | match *self { 650 | #[cfg(feature = "has-ddc-i2c")] 651 | Handle::I2cDevice(ref mut i2c) => i2c.get_timing_report().map_err(BackendError::I2cDeviceError), 652 | #[cfg(feature = "has-ddc-winapi")] 653 | Handle::WinApi(ref mut monitor) => monitor.get_timing_report().map_err(BackendError::WinApiError), 654 | #[cfg(feature = "has-ddc-macos")] 655 | Handle::MacOS(ref mut monitor) => monitor.get_timing_report().map_err(BackendError::MacOsError), 656 | #[cfg(feature = "has-nvapi")] 657 | Handle::Nvapi(ref mut i2c) => i2c.get_timing_report().map_err(BackendError::NvapiError), 658 | } 659 | .map_err(From::from) 660 | } 661 | } 662 | 663 | impl DdcTable for Handle { 664 | fn table_read(&mut self, code: FeatureCode) -> Result, Self::Error> { 665 | match *self { 666 | #[cfg(feature = "has-ddc-i2c")] 667 | Handle::I2cDevice(ref mut i2c) => i2c 668 | .table_read(code) 669 | .map_err(|e| Error::LowLevelError(BackendError::I2cDeviceError(e))), 670 | #[cfg(feature = "has-ddc-macos")] 671 | Handle::MacOS(ref mut i2c) => i2c 672 | .table_read(code) 673 | .map_err(|e| Error::LowLevelError(BackendError::MacOsError(e))), 674 | #[cfg(feature = "has-ddc-winapi")] 675 | Handle::WinApi(_) => Err(Error::UnsupportedOp), 676 | #[cfg(feature = "has-nvapi")] 677 | Handle::Nvapi(ref mut i2c) => i2c 678 | .table_read(code) 679 | .map_err(|e| Error::LowLevelError(BackendError::NvapiError(e))), 680 | } 681 | } 682 | 683 | fn table_write(&mut self, code: FeatureCode, offset: u16, value: &[u8]) -> Result<(), Self::Error> { 684 | match *self { 685 | #[cfg(feature = "has-ddc-i2c")] 686 | Handle::I2cDevice(ref mut i2c) => i2c 687 | .table_write(code, offset, value) 688 | .map_err(|e| Error::LowLevelError(BackendError::I2cDeviceError(e))), 689 | #[cfg(feature = "has-ddc-macos")] 690 | Handle::MacOS(ref mut i2c) => i2c 691 | .table_write(code, offset, value) 692 | .map_err(|e| Error::LowLevelError(BackendError::MacOsError(e))), 693 | #[cfg(feature = "has-ddc-winapi")] 694 | Handle::WinApi(_) => Err(Error::UnsupportedOp), 695 | #[cfg(feature = "has-nvapi")] 696 | Handle::Nvapi(ref mut i2c) => i2c 697 | .table_write(code, offset, value) 698 | .map_err(|e| Error::LowLevelError(BackendError::NvapiError(e))), 699 | } 700 | } 701 | } 702 | --------------------------------------------------------------------------------