├── .github └── workflows │ └── ddc-rs.yml ├── .gitignore ├── .rustfmt.toml ├── COPYING ├── Cargo.lock ├── Cargo.toml ├── README.md ├── ci.nix ├── default.nix ├── flake.lock ├── flake.nix ├── lock.nix ├── shell.nix └── src ├── commands.rs ├── delay.rs └── lib.rs /.github/workflows/ddc-rs.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-rs check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - id: checkout 11 | name: git clone 12 | uses: actions/checkout@v4 13 | with: 14 | submodules: true 15 | - id: nix-install 16 | name: nix install 17 | uses: arcnmx/ci/actions/nix/install@v0.7 18 | - id: ci-action-build 19 | name: nix build ci.gh-actions.configFile 20 | uses: arcnmx/ci/actions/nix/build@v0.7 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.7 27 | with: 28 | args: -u .github/workflows/ddc-rs.yml .ci/workflow.yml 29 | attrs: nixpkgs.diffutils 30 | command: diff 31 | macos: 32 | name: ddc-rs-macos 33 | runs-on: macos-13 34 | steps: 35 | - id: checkout 36 | name: git clone 37 | uses: actions/checkout@v4 38 | with: 39 | submodules: true 40 | - id: nix-install 41 | name: nix install 42 | uses: arcnmx/ci/actions/nix/install@v0.7 43 | - id: ci-setup 44 | name: nix setup 45 | uses: arcnmx/ci/actions/nix/run@v0.7 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.7 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.7 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.7 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.7 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-rs-nixos 90 | runs-on: ubuntu-latest 91 | steps: 92 | - id: checkout 93 | name: git clone 94 | uses: actions/checkout@v4 95 | with: 96 | submodules: true 97 | - id: nix-install 98 | name: nix install 99 | uses: arcnmx/ci/actions/nix/install@v0.7 100 | - id: ci-setup 101 | name: nix setup 102 | uses: arcnmx/ci/actions/nix/run@v0.7 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.7 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.7 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.7 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.7 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-rs 146 | 'on': 147 | - push 148 | - pull_request 149 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.cargo/ 3 | /.gitattributes 4 | /result* 5 | *.swp 6 | -------------------------------------------------------------------------------- /.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 = "ddc" 7 | version = "0.3.0" 8 | dependencies = [ 9 | "mccs", 10 | ] 11 | 12 | [[package]] 13 | name = "mccs" 14 | version = "0.2.0" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "2a63ab5297c9a7d5f8298a076b5f858c3c51ce84bf2f57a302d1d67ff9323360" 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ddc" 3 | version = "0.3.0" # keep in sync with html_root_url 4 | authors = ["arcnmx"] 5 | edition = "2021" 6 | 7 | description = "DDC/CI monitor control" 8 | keywords = ["ddc", "mccs", "vcp", "vesa"] 9 | categories = ["hardware-support"] 10 | 11 | documentation = "https://docs.rs/ddc/" 12 | repository = "https://github.com/arcnmx/ddc-rs" 13 | readme = "README.md" 14 | license = "MIT" 15 | 16 | include = [ 17 | "/src/**/*.rs", 18 | "/README*", 19 | "/COPYING*", 20 | ] 21 | 22 | [badges] 23 | maintenance = { status = "passively-maintained" } 24 | 25 | [dependencies] 26 | mccs = "0.2" 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ddc 2 | 3 | [![release-badge][]][cargo] [![docs-badge][]][docs] [![license-badge][]][license] 4 | 5 | `ddc` is a Rust crate for controlling monitors with [DDC/CI](https://en.wikipedia.org/wiki/Display_Data_Channel). 6 | 7 | ## Implementations 8 | 9 | `ddc` only provides traits for working with DDC, and these must be implemented 10 | with an underlying backend in order to be used. The following crates may be 11 | helpful: 12 | 13 | - [ddc-i2c](https://crates.io/crates/ddc-i2c) supports DDC using an I2C capable 14 | master - in particular Linux's i2c-dev. 15 | - [ddc-winapi](https://crates.io/crates/ddc-winapi) implements DDC using the 16 | Windows API. It is more limited than the generic I2C interface, and cannot be 17 | used to read monitor EDID info. 18 | - [Any other downstream crates](https://crates.io/crates/ddc/reverse_dependencies) 19 | 20 | ## [Documentation][docs] 21 | 22 | See the [documentation][docs] for up to date information. 23 | 24 | [release-badge]: https://img.shields.io/crates/v/ddc.svg?style=flat-square 25 | [cargo]: https://crates.io/crates/ddc 26 | [docs-badge]: https://img.shields.io/badge/API-docs-blue.svg?style=flat-square 27 | [docs]: https://docs.rs/ddc/ 28 | [license-badge]: https://img.shields.io/badge/license-MIT-ff69b4.svg?style=flat-square 29 | [license]: https://github.com/arcnmx/ddc-rs/blob/master/COPYING 30 | -------------------------------------------------------------------------------- /ci.nix: -------------------------------------------------------------------------------- 1 | { config, channels, pkgs, lib, ... }: with pkgs; with lib; let 2 | inherit (import ./. { inherit pkgs; }) checks; 3 | in { 4 | name = "ddc-rs"; 5 | ci = { 6 | version = "v0.7"; 7 | gh-actions.enable = true; 8 | }; 9 | cache.cachix = { 10 | ci.signingKey = ""; 11 | arc.enable = true; 12 | }; 13 | channels = { 14 | nixpkgs = "24.05"; 15 | }; 16 | tasks = { 17 | build.inputs = singleton checks.test; 18 | }; 19 | jobs = { 20 | nixos = { 21 | tasks = { 22 | rustfmt.inputs = singleton checks.rustfmt; 23 | version.inputs = singleton checks.version; 24 | }; 25 | }; 26 | macos.system = "x86_64-darwin"; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /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": 1701802971, 26 | "narHash": "sha256-Zo5fJpXbe+xXOTiDT4JG2rExobMJTmFZ72+3XTMMHrQ=", 27 | "owner": "flakelib", 28 | "repo": "fl", 29 | "rev": "b71a91517f6b16aa5faefe8ec491d9f3062d7a20", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "flakelib", 34 | "repo": "fl", 35 | "type": "github" 36 | } 37 | }, 38 | "nix-std": { 39 | "locked": { 40 | "lastModified": 1701658249, 41 | "narHash": "sha256-KIt1TUuBvldhaVRta010MI5FeQlB8WadjqljybjesN0=", 42 | "owner": "chessai", 43 | "repo": "nix-std", 44 | "rev": "715db541ffff4194620e48d210b76f73a74b5b5d", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "chessai", 49 | "repo": "nix-std", 50 | "type": "github" 51 | } 52 | }, 53 | "nixpkgs": { 54 | "locked": { 55 | "lastModified": 1721701671, 56 | "narHash": "sha256-b3YlA+FXmfUECrFkRNkB9l4sHzzuMUcU7PvI4He8oCc=", 57 | "owner": "NixOS", 58 | "repo": "nixpkgs", 59 | "rev": "2874fc48ccff3f513a05ba7af1f6e44bf44044af", 60 | "type": "github" 61 | }, 62 | "original": { 63 | "id": "nixpkgs", 64 | "type": "indirect" 65 | } 66 | }, 67 | "root": { 68 | "inputs": { 69 | "flakelib": "flakelib", 70 | "nixpkgs": "nixpkgs", 71 | "rust": "rust" 72 | } 73 | }, 74 | "rust": { 75 | "inputs": { 76 | "nixpkgs": [ 77 | "nixpkgs" 78 | ] 79 | }, 80 | "locked": { 81 | "lastModified": 1715288797, 82 | "narHash": "sha256-E7tcuQWs2QZHPnV5eeB5wAfNZ6aeqqOOzANyypTPY6w=", 83 | "owner": "arcnmx", 84 | "repo": "nixexprs-rust", 85 | "rev": "32cce853be1fa59e3238256d06b229a7eece7724", 86 | "type": "github" 87 | }, 88 | "original": { 89 | "owner": "arcnmx", 90 | "repo": "nixexprs-rust", 91 | "type": "github" 92 | } 93 | }, 94 | "std": { 95 | "inputs": { 96 | "nix-std": "nix-std" 97 | }, 98 | "locked": { 99 | "lastModified": 1701802337, 100 | "narHash": "sha256-JCVCyjDZ6LA0xyVoDZzRXjy0OgWOZo3OpeZEVm/U97w=", 101 | "owner": "flakelib", 102 | "repo": "std", 103 | "rev": "443d1c8246b3d96a4822b02af907ca0d833e8b63", 104 | "type": "github" 105 | }, 106 | "original": { 107 | "owner": "flakelib", 108 | "repo": "std", 109 | "type": "github" 110 | } 111 | } 112 | }, 113 | "root": "root", 114 | "version": 7 115 | } 116 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "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 | impure = builtins ? currentSystem; 14 | in flakelib { 15 | inherit inputs; 16 | systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 17 | devShells = { 18 | plain = { 19 | mkShell, hostPlatform 20 | , enableRust ? true, cargo 21 | , rustTools ? [ ] 22 | , generate 23 | }: mkShell { 24 | inherit rustTools; 25 | nativeBuildInputs = nixlib.optional enableRust cargo ++ [ 26 | generate 27 | ]; 28 | }; 29 | stable = { rust'stable, outputs'devShells'plain }: outputs'devShells'plain.override { 30 | inherit (rust'stable) mkShell; 31 | enableRust = false; 32 | }; 33 | dev = { rust'unstable, outputs'devShells'plain }: outputs'devShells'plain.override { 34 | inherit (rust'unstable) mkShell; 35 | enableRust = false; 36 | rustTools = [ "rust-analyzer" ]; 37 | }; 38 | default = { outputs'devShells }: outputs'devShells.plain; 39 | }; 40 | checks = { 41 | rustfmt = { rust'builders, source }: rust'builders.check-rustfmt-unstable { 42 | src = source; 43 | config = ./.rustfmt.toml; 44 | }; 45 | version = { rust'builders, source }: rust'builders.check-contents { 46 | src = source; 47 | patterns = [ 48 | { path = "src/lib.rs"; docs'rs = { 49 | inherit (self.lib.crate) name version; 50 | }; } 51 | ]; 52 | }; 53 | test = { rustPlatform, source }: rustPlatform.buildRustPackage { 54 | pname = self.lib.crate.package.name; 55 | inherit (self.lib.crate) cargoLock version; 56 | src = source; 57 | buildType = "debug"; 58 | meta.name = "cargo test"; 59 | }; 60 | }; 61 | legacyPackages = { callPackageSet }: callPackageSet { 62 | source = { rust'builders }: rust'builders.wrapSource self.lib.crate.src; 63 | 64 | generate = { rust'builders, outputHashes }: rust'builders.generateFiles { 65 | paths = { 66 | "lock.nix" = outputHashes; 67 | }; 68 | }; 69 | outputHashes = { rust'builders }: rust'builders.cargoOutputHashes { 70 | inherit (self.lib) crate; 71 | }; 72 | } { }; 73 | lib = with nixlib; { 74 | crate = rust.lib.importCargo { 75 | inherit self; 76 | path = ./Cargo.toml; 77 | inherit (import ./lock.nix) outputHashes; 78 | }; 79 | inherit (self.lib.crate.package) version; 80 | }; 81 | config = rec { 82 | name = "ddc-rs"; 83 | packages.namespace = [ name ]; 84 | }; 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /lock.nix: -------------------------------------------------------------------------------- 1 | { 2 | outputHashes = { 3 | 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: (import ./. { inherit pkgs; }).devShells.default 2 | -------------------------------------------------------------------------------- /src/commands.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | use { 3 | crate::{ErrorCode, FeatureCode, VcpValue}, 4 | std::{fmt, mem}, 5 | }; 6 | 7 | pub trait Command { 8 | type Ok: CommandResult; 9 | const MIN_LEN: usize; 10 | const MAX_LEN: usize; 11 | const DELAY_RESPONSE_MS: u64; 12 | const DELAY_COMMAND_MS: u64; 13 | 14 | fn len(&self) -> usize; 15 | 16 | fn encode(&self, data: &mut [u8]) -> Result; 17 | } 18 | 19 | pub trait CommandResult: Sized { 20 | const MAX_LEN: usize; 21 | fn decode(data: &[u8]) -> Result; 22 | } 23 | 24 | #[derive(Copy, Clone, Debug)] 25 | pub struct GetVcpFeature { 26 | pub code: FeatureCode, 27 | } 28 | 29 | impl GetVcpFeature { 30 | pub fn new(code: FeatureCode) -> Self { 31 | GetVcpFeature { code } 32 | } 33 | } 34 | 35 | impl Command for GetVcpFeature { 36 | type Ok = VcpValue; 37 | 38 | // the spec omits this, but 50 corresponds with what all other commands suggest 39 | const DELAY_COMMAND_MS: u64 = 50; 40 | const DELAY_RESPONSE_MS: u64 = 40; 41 | const MAX_LEN: usize = 2; 42 | const MIN_LEN: usize = 2; 43 | 44 | fn len(&self) -> usize { 45 | 2 46 | } 47 | 48 | fn encode(&self, data: &mut [u8]) -> Result { 49 | assert!(data.len() >= 2); 50 | data[0] = 0x01; 51 | data[1] = self.code; 52 | 53 | Ok(2) 54 | } 55 | } 56 | 57 | #[derive(Copy, Clone, Debug)] 58 | pub struct SetVcpFeature { 59 | pub code: FeatureCode, 60 | pub value: u16, 61 | } 62 | 63 | impl SetVcpFeature { 64 | pub fn new(code: FeatureCode, value: u16) -> Self { 65 | SetVcpFeature { code, value } 66 | } 67 | } 68 | 69 | impl Command for SetVcpFeature { 70 | type Ok = (); 71 | 72 | const DELAY_COMMAND_MS: u64 = 50; 73 | const DELAY_RESPONSE_MS: u64 = 0; 74 | const MAX_LEN: usize = 4; 75 | const MIN_LEN: usize = 4; 76 | 77 | fn len(&self) -> usize { 78 | 4 79 | } 80 | 81 | fn encode(&self, data: &mut [u8]) -> Result { 82 | assert!(data.len() >= 4); 83 | 84 | data[0] = 0x03; 85 | data[1] = self.code; 86 | data[2] = (self.value >> 8) as _; 87 | data[3] = self.value as _; 88 | 89 | Ok(4) 90 | } 91 | } 92 | 93 | impl CommandResult for VcpValue { 94 | const MAX_LEN: usize = 8; 95 | 96 | fn decode(data: &[u8]) -> Result { 97 | if data.len() != 8 { 98 | return Err(ErrorCode::InvalidLength) 99 | } 100 | 101 | if data[0] != 0x02 { 102 | return Err(ErrorCode::InvalidOpcode) 103 | } 104 | 105 | match data[1] { 106 | // NoError 107 | 0x00 => (), 108 | 0x01 => return Err(ErrorCode::Invalid("Unsupported VCP code".into())), 109 | rc => return Err(ErrorCode::Invalid(format!("Unrecognized VCP error code 0x{:02x}", rc))), 110 | } 111 | 112 | // data[2] == vcp code from request 113 | 114 | Ok(VcpValue { 115 | ty: data[2], 116 | mh: data[4], 117 | ml: data[5], 118 | sh: data[6], 119 | sl: data[7], 120 | }) 121 | } 122 | } 123 | 124 | #[derive(Copy, Clone, Debug)] 125 | pub struct SaveCurrentSettings; 126 | 127 | impl Command for SaveCurrentSettings { 128 | type Ok = (); 129 | 130 | const DELAY_COMMAND_MS: u64 = 200; 131 | const DELAY_RESPONSE_MS: u64 = 0; 132 | const MAX_LEN: usize = 1; 133 | const MIN_LEN: usize = 1; 134 | 135 | fn len(&self) -> usize { 136 | 1 137 | } 138 | 139 | fn encode(&self, data: &mut [u8]) -> Result { 140 | assert!(data.len() >= 1); 141 | data[0] = 0x0c; 142 | 143 | Ok(1) 144 | } 145 | } 146 | 147 | #[derive(Copy, Clone, Debug)] 148 | pub struct TableWrite<'a> { 149 | pub code: FeatureCode, 150 | pub offset: u16, 151 | pub data: &'a [u8], 152 | } 153 | 154 | impl<'a> TableWrite<'a> { 155 | pub fn new(code: FeatureCode, offset: u16, data: &'a [u8]) -> Self { 156 | TableWrite { code, offset, data } 157 | } 158 | } 159 | 160 | impl<'a> Command for TableWrite<'a> { 161 | type Ok = (); 162 | 163 | const DELAY_COMMAND_MS: u64 = 50; 164 | const DELAY_RESPONSE_MS: u64 = 0; 165 | // Spec says this should be 3~35 but allows 32 bytes of data transfer?? how?? What does "P=1" mean? 166 | const MAX_LEN: usize = 4 + 32; 167 | const MIN_LEN: usize = 4; 168 | 169 | fn len(&self) -> usize { 170 | 4 + self.data.len() 171 | } 172 | 173 | fn encode(&self, data: &mut [u8]) -> Result { 174 | assert!(data.len() >= 4 + self.data.len()); 175 | assert!(self.data.len() <= 32); 176 | 177 | data[0] = 0xe7; 178 | data[1] = self.code; 179 | data[2] = (self.offset >> 8) as _; 180 | data[3] = self.offset as _; 181 | data[4..4 + self.data.len()].copy_from_slice(self.data); 182 | 183 | Ok(4 + self.data.len()) 184 | } 185 | } 186 | 187 | #[derive(Copy, Clone, Debug)] 188 | pub struct TableRead { 189 | pub code: FeatureCode, 190 | pub offset: u16, 191 | } 192 | 193 | impl TableRead { 194 | pub fn new(code: FeatureCode, offset: u16) -> Self { 195 | TableRead { code, offset } 196 | } 197 | } 198 | 199 | impl Command for TableRead { 200 | type Ok = TableResponse; 201 | 202 | const DELAY_COMMAND_MS: u64 = 50; 203 | const DELAY_RESPONSE_MS: u64 = 40; 204 | const MAX_LEN: usize = 4; 205 | const MIN_LEN: usize = 4; 206 | 207 | fn len(&self) -> usize { 208 | 4 209 | } 210 | 211 | fn encode(&self, data: &mut [u8]) -> Result { 212 | assert!(data.len() >= 4); 213 | 214 | data[0] = 0xe2; 215 | data[1] = self.code; 216 | data[2] = (self.offset >> 8) as _; 217 | data[3] = self.offset as _; 218 | 219 | Ok(4) 220 | } 221 | } 222 | 223 | #[derive(Copy, Clone, Debug)] 224 | pub struct CapabilitiesRequest { 225 | pub offset: u16, 226 | } 227 | 228 | impl CapabilitiesRequest { 229 | pub fn new(offset: u16) -> Self { 230 | CapabilitiesRequest { offset } 231 | } 232 | } 233 | 234 | impl Command for CapabilitiesRequest { 235 | type Ok = CapabilitiesReply; 236 | 237 | const DELAY_COMMAND_MS: u64 = 50; 238 | const DELAY_RESPONSE_MS: u64 = 40; 239 | const MAX_LEN: usize = 3; 240 | const MIN_LEN: usize = 3; 241 | 242 | fn len(&self) -> usize { 243 | 3 244 | } 245 | 246 | fn encode(&self, data: &mut [u8]) -> Result { 247 | assert!(data.len() >= 3); 248 | 249 | data[0] = 0xf3; 250 | data[1] = (self.offset >> 8) as _; 251 | data[2] = self.offset as _; 252 | 253 | Ok(3) 254 | } 255 | } 256 | 257 | #[derive(Copy, Clone)] 258 | pub struct TableResponse { 259 | pub offset: u16, 260 | data: [u8; 32], 261 | len: u8, 262 | } 263 | 264 | impl TableResponse { 265 | pub fn bytes(&self) -> &[u8] { 266 | &self.data[..self.len as usize] 267 | } 268 | } 269 | 270 | impl fmt::Debug for TableResponse { 271 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 272 | f.debug_struct("TableResponse") 273 | .field("offset", &self.offset) 274 | .field("bytes", &self.bytes()) 275 | .finish() 276 | } 277 | } 278 | 279 | impl Default for TableResponse { 280 | fn default() -> Self { 281 | unsafe { mem::zeroed() } 282 | } 283 | } 284 | 285 | impl CommandResult for TableResponse { 286 | const MAX_LEN: usize = 36; 287 | 288 | fn decode(data: &[u8]) -> Result { 289 | // spec says 3 - 35??? 290 | if data.len() < 4 || data.len() > 36 { 291 | return Err(ErrorCode::InvalidLength) 292 | } 293 | 294 | if data[0] != 0xe4 { 295 | return Err(ErrorCode::InvalidOpcode) 296 | } 297 | 298 | let mut table = TableResponse::default(); 299 | table.offset = ((data[1] as u16) << 8) | data[2] as u16; 300 | let data = &data[3..]; 301 | table.len = data.len() as u8; 302 | table.data[..data.len()].copy_from_slice(data); 303 | Ok(table) 304 | } 305 | } 306 | 307 | #[derive(Clone, Debug)] 308 | pub struct CapabilitiesReply { 309 | pub offset: u16, 310 | pub data: Box<[u8]>, 311 | } 312 | 313 | impl CommandResult for CapabilitiesReply { 314 | const MAX_LEN: usize = 35; 315 | 316 | fn decode(data: &[u8]) -> Result { 317 | if data.len() < 3 || data.len() > 35 { 318 | return Err(ErrorCode::InvalidLength) 319 | } 320 | 321 | if data[0] != 0xe3 { 322 | return Err(ErrorCode::InvalidOpcode) 323 | } 324 | 325 | Ok(CapabilitiesReply { 326 | offset: ((data[1] as u16) << 8) | data[2] as u16, 327 | data: data[3..].to_owned().into_boxed_slice(), 328 | }) 329 | } 330 | } 331 | 332 | #[derive(Copy, Clone, Debug)] 333 | pub struct GetTimingReport; 334 | 335 | impl Command for GetTimingReport { 336 | type Ok = TimingMessage; 337 | 338 | const DELAY_COMMAND_MS: u64 = 50; 339 | const DELAY_RESPONSE_MS: u64 = 40; 340 | const MAX_LEN: usize = 1; 341 | const MIN_LEN: usize = 1; 342 | 343 | fn len(&self) -> usize { 344 | 1 345 | } 346 | 347 | fn encode(&self, data: &mut [u8]) -> Result { 348 | assert!(data.len() >= 1); 349 | data[0] = 0x07; 350 | 351 | Ok(1) 352 | } 353 | } 354 | 355 | #[derive(Clone, Debug)] 356 | pub struct TimingMessage { 357 | pub timing_status: u8, 358 | pub horizontal_frequency: u16, 359 | pub vertical_frequency: u16, 360 | } 361 | 362 | impl CommandResult for TimingMessage { 363 | const MAX_LEN: usize = 6; 364 | 365 | fn decode(data: &[u8]) -> Result { 366 | if data.len() != 6 { 367 | return Err(ErrorCode::InvalidLength) 368 | } 369 | 370 | if data[0] != 0x4e { 371 | return Err(ErrorCode::InvalidOpcode) 372 | } 373 | 374 | Ok(TimingMessage { 375 | timing_status: data[1], 376 | horizontal_frequency: ((data[2] as u16) << 8) | data[3] as u16, 377 | vertical_frequency: ((data[4] as u16) << 8) | data[5] as u16, 378 | }) 379 | } 380 | } 381 | 382 | impl CommandResult for () { 383 | const MAX_LEN: usize = 0; 384 | 385 | fn decode(data: &[u8]) -> Result { 386 | if data.is_empty() { 387 | Ok(()) 388 | } else { 389 | Err(ErrorCode::InvalidLength) 390 | } 391 | } 392 | } 393 | 394 | impl<'a, C: Command> Command for &'a C { 395 | type Ok = C::Ok; 396 | 397 | const DELAY_COMMAND_MS: u64 = C::DELAY_COMMAND_MS; 398 | const DELAY_RESPONSE_MS: u64 = C::DELAY_RESPONSE_MS; 399 | const MAX_LEN: usize = C::MAX_LEN; 400 | const MIN_LEN: usize = C::MIN_LEN; 401 | 402 | fn len(&self) -> usize { 403 | (*self).len() 404 | } 405 | 406 | fn encode(&self, data: &mut [u8]) -> Result { 407 | (*self).encode(data) 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /src/delay.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | thread::sleep, 3 | time::{Duration, Instant}, 4 | }; 5 | 6 | /// A type that can help with implementing the DDC specification delays. 7 | #[derive(Clone, Debug)] 8 | pub struct Delay { 9 | time: Option, 10 | delay: Duration, 11 | } 12 | 13 | impl Delay { 14 | /// Creates a new delay starting now. 15 | pub fn new(delay: Duration) -> Self { 16 | Delay { 17 | time: Some(Instant::now()), 18 | delay, 19 | } 20 | } 21 | 22 | /// The time remaining in this delay. 23 | pub fn remaining(&self) -> Duration { 24 | self.time 25 | .as_ref() 26 | .and_then(|time| self.delay.checked_sub(time.elapsed())) 27 | .unwrap_or(Duration::default()) 28 | } 29 | 30 | /// Waits out the remaining time in this delay. 31 | pub fn sleep(&mut self) { 32 | if let Some(delay) = self.time.take().and_then(|time| self.delay.checked_sub(time.elapsed())) { 33 | sleep(delay); 34 | } 35 | } 36 | } 37 | 38 | impl Default for Delay { 39 | fn default() -> Self { 40 | Delay { 41 | time: None, 42 | delay: Default::default(), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![doc(html_root_url = "https://docs.rs/ddc/0.3.0/")] 3 | 4 | //! Control displays using the DDC/CI protocol. 5 | //! 6 | //! Provides generic traits and utilities for working with DDC. See [downstream 7 | //! crates](https://crates.io/crates/ddc/reverse_dependencies) for usable 8 | //! concrete implementations. 9 | 10 | extern crate mccs; 11 | 12 | use std::{error, fmt, iter, time::Duration}; 13 | pub use { 14 | self::{ 15 | commands::{Command, CommandResult, TimingMessage}, 16 | delay::Delay, 17 | }, 18 | mccs::{FeatureCode, Value as VcpValue, ValueType as VcpValueType}, 19 | }; 20 | 21 | /// DDC/CI command request and response types. 22 | pub mod commands; 23 | mod delay; 24 | 25 | /// EDID EEPROM I2C address 26 | pub const I2C_ADDRESS_EDID: u16 = 0x50; 27 | 28 | /// E-DDC EDID segment register I2C address 29 | pub const I2C_ADDRESS_EDID_SEGMENT: u16 = 0x30; 30 | 31 | /// DDC/CI command and control I2C address 32 | pub const I2C_ADDRESS_DDC_CI: u16 = 0x37; 33 | 34 | /// DDC sub-address command prefix 35 | pub const SUB_ADDRESS_DDC_CI: u8 = 0x51; 36 | 37 | /// DDC delay required before retrying a request 38 | pub const DELAY_COMMAND_FAILED_MS: u64 = 40; 39 | 40 | /// A trait that allows retrieving Extended Display Identification Data (EDID) 41 | /// from a device. 42 | pub trait Edid { 43 | /// An error that can occur when reading the EDID from a device. 44 | type EdidError; 45 | 46 | /// Read up to 256 bytes of the monitor's EDID. 47 | fn read_edid(&mut self, offset: u8, data: &mut [u8]) -> Result; 48 | } 49 | 50 | /// E-DDC allows reading extensions of Enhanced EDID. 51 | pub trait Eddc: Edid { 52 | /// Read part of the EDID using the segments added in the Enhanced Display 53 | /// Data Channel (E-DDC) protocol. 54 | fn read_eddc_edid(&mut self, segment: u8, offset: u8, data: &mut [u8]) -> Result; 55 | } 56 | 57 | /// A DDC host is able to communicate with a DDC device such as a display. 58 | pub trait DdcHost { 59 | /// An error that can occur when communicating with a DDC device. 60 | /// 61 | /// Usually impls `From`. 62 | type Error; 63 | 64 | /// Wait for any previous commands to complete. 65 | /// 66 | /// The DDC specification defines delay intervals that must occur between 67 | /// execution of two subsequent commands, this waits for the amount of time 68 | /// remaining since the last command was executed. This is normally done 69 | /// internally and shouldn't need to be called manually unless synchronizing 70 | /// with an external process or another handle to the same device. It may 71 | /// however be desireable to run this before program exit. 72 | fn sleep(&mut self) {} 73 | } 74 | 75 | /// Allows the execution of arbitrary low level DDC commands. 76 | pub trait DdcCommandRaw: DdcHost { 77 | /// Executes a raw DDC/CI command. 78 | /// 79 | /// A response should not be read unless `out` is not empty, and the delay 80 | /// should occur in between any write and read made to the device. A subslice 81 | /// of `out` excluding DDC packet headers should be returned. 82 | fn execute_raw<'a>( 83 | &mut self, 84 | data: &[u8], 85 | out: &'a mut [u8], 86 | response_delay: Duration, 87 | ) -> Result<&'a mut [u8], Self::Error>; 88 | } 89 | 90 | /// Using this marker trait will automatically implement the `DdcCommand` trait. 91 | pub trait DdcCommandRawMarker: DdcCommandRaw 92 | where 93 | Self::Error: From, 94 | { 95 | /// Sets an internal `Delay` that must expire before the next command is 96 | /// attempted. 97 | fn set_sleep_delay(&mut self, delay: Delay); 98 | } 99 | 100 | /// A (slightly) higher level interface to `DdcCommandRaw`. 101 | /// 102 | /// Some DDC implementations only provide access to the higher level commands 103 | /// exposed in the `Ddc` trait. 104 | pub trait DdcCommand: DdcHost { 105 | /// Execute a DDC/CI command. See the `commands` module for all available 106 | /// commands. The return type is dependent on the executed command. 107 | fn execute(&mut self, command: C) -> Result; 108 | 109 | /// Computes a DDC/CI packet checksum 110 | fn checksum>(iter: II) -> u8 { 111 | iter.into_iter().fold(0u8, |sum, v| sum ^ v) 112 | } 113 | 114 | /// Encodes a DDC/CI command into a packet. 115 | /// 116 | /// `packet.len()` must be 3 bytes larger than `data.len()` 117 | fn encode_command<'a>(data: &[u8], packet: &'a mut [u8]) -> &'a [u8] { 118 | packet[0] = SUB_ADDRESS_DDC_CI; 119 | packet[1] = 0x80 | data.len() as u8; 120 | packet[2..2 + data.len()].copy_from_slice(data); 121 | packet[2 + data.len()] = 122 | Self::checksum(iter::once((I2C_ADDRESS_DDC_CI as u8) << 1).chain(packet[..2 + data.len()].iter().cloned())); 123 | 124 | &packet[..3 + data.len()] 125 | } 126 | } 127 | 128 | /// Using this marker trait will automatically implement the `Ddc` and `DdcTable` 129 | /// traits. 130 | pub trait DdcCommandMarker: DdcCommand 131 | where 132 | Self::Error: From, 133 | { 134 | } 135 | 136 | /// A high level interface to DDC commands. 137 | pub trait Ddc: DdcHost { 138 | /// Retrieve the capability string from the device. 139 | /// 140 | /// This executes multiple `CapabilitiesRequest` commands to construct the entire string. 141 | fn capabilities_string(&mut self) -> Result, Self::Error>; 142 | 143 | /// Gets the current value of an MCCS VCP feature. 144 | fn get_vcp_feature(&mut self, code: FeatureCode) -> Result; 145 | 146 | /// Sets a VCP feature to the specified value. 147 | fn set_vcp_feature(&mut self, code: FeatureCode, value: u16) -> Result<(), Self::Error>; 148 | 149 | /// Instructs the device to save its current settings. 150 | fn save_current_settings(&mut self) -> Result<(), Self::Error>; 151 | 152 | /// Retrieves a timing report from the device. 153 | fn get_timing_report(&mut self) -> Result; 154 | } 155 | 156 | /// Table commands can read and write arbitrary binary data to a VCP feature. 157 | /// 158 | /// Tables were introduced in MCCS specification versions 3.0 and 2.2. 159 | pub trait DdcTable: DdcHost { 160 | /// Read a table value from the device. 161 | fn table_read(&mut self, code: FeatureCode) -> Result, Self::Error>; 162 | 163 | /// Write a table value to the device. 164 | fn table_write(&mut self, code: FeatureCode, offset: u16, value: &[u8]) -> Result<(), Self::Error>; 165 | } 166 | 167 | /// DDC/CI protocol errors 168 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 169 | pub enum ErrorCode { 170 | /// Expected matching offset from DDC/CI 171 | InvalidOffset, 172 | /// DDC/CI invalid packet length 173 | InvalidLength, 174 | /// Checksum mismatch 175 | InvalidChecksum, 176 | /// Expected opcode mismatch 177 | InvalidOpcode, 178 | /// Expected data mismatch 179 | InvalidData, 180 | /// Custom unspecified error 181 | Invalid(String), 182 | } 183 | 184 | impl error::Error for ErrorCode {} 185 | 186 | impl fmt::Display for ErrorCode { 187 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 188 | f.write_str(match *self { 189 | ErrorCode::InvalidOffset => "invalid offset returned from DDC/CI", 190 | ErrorCode::InvalidLength => "invalid DDC/CI length", 191 | ErrorCode::InvalidChecksum => "DDC/CI checksum mismatch", 192 | ErrorCode::InvalidOpcode => "DDC/CI VCP opcode mismatch", 193 | ErrorCode::InvalidData => "invalid DDC/CI data", 194 | ErrorCode::Invalid(ref s) => s, 195 | }) 196 | } 197 | } 198 | 199 | impl Ddc for D 200 | where 201 | D::Error: From, 202 | { 203 | fn capabilities_string(&mut self) -> Result, Self::Error> { 204 | let mut string = Vec::new(); 205 | let mut offset = 0; 206 | loop { 207 | let caps = self.execute(commands::CapabilitiesRequest::new(offset))?; 208 | if caps.offset != offset { 209 | return Err(ErrorCode::InvalidOffset.into()) 210 | } else if caps.data.is_empty() { 211 | break 212 | } 213 | 214 | string.extend(caps.data.iter()); 215 | 216 | offset += caps.data.len() as u16; 217 | } 218 | 219 | Ok(string) 220 | } 221 | 222 | fn get_vcp_feature(&mut self, code: FeatureCode) -> Result { 223 | self.execute(commands::GetVcpFeature::new(code)) 224 | } 225 | 226 | fn set_vcp_feature(&mut self, code: FeatureCode, value: u16) -> Result<(), Self::Error> { 227 | self.execute(commands::SetVcpFeature::new(code, value)) 228 | } 229 | 230 | fn save_current_settings(&mut self) -> Result<(), Self::Error> { 231 | self.execute(commands::SaveCurrentSettings) 232 | } 233 | 234 | fn get_timing_report(&mut self) -> Result { 235 | self.execute(commands::GetTimingReport) 236 | } 237 | } 238 | 239 | impl DdcTable for D 240 | where 241 | D::Error: From, 242 | { 243 | fn table_read(&mut self, code: FeatureCode) -> Result, Self::Error> { 244 | let mut value = Vec::new(); 245 | let mut offset = 0; 246 | loop { 247 | let table = self.execute(commands::TableRead::new(code, offset))?; 248 | if table.offset != offset { 249 | return Err(ErrorCode::InvalidOffset.into()) 250 | } else if table.bytes().is_empty() { 251 | break 252 | } 253 | 254 | value.extend(table.bytes().iter()); 255 | 256 | offset += table.bytes().len() as u16; 257 | } 258 | 259 | Ok(value) 260 | } 261 | 262 | fn table_write(&mut self, code: FeatureCode, mut offset: u16, value: &[u8]) -> Result<(), Self::Error> { 263 | for chunk in value.chunks(32) { 264 | self.execute(commands::TableWrite::new(code, offset, chunk))?; 265 | offset += chunk.len() as u16; 266 | } 267 | 268 | Ok(()) 269 | } 270 | } 271 | 272 | impl DdcCommand for D 273 | where 274 | D::Error: From, 275 | { 276 | fn execute(&mut self, command: C) -> Result { 277 | // TODO: once associated consts work... 278 | //let mut data = [0u8; C::MAX_LEN]; 279 | let mut data = [0u8; 36]; 280 | command.encode(&mut data)?; 281 | 282 | // TODO: once associated consts work... 283 | //let mut out = [0u8; C::Ok::MAX_LEN + 3]; 284 | let mut out = [0u8; 36 + 3]; 285 | let out = if C::Ok::MAX_LEN > 0 { 286 | &mut out[..C::Ok::MAX_LEN + 3] 287 | } else { 288 | &mut [] 289 | }; 290 | let res = self.execute_raw( 291 | &data[..command.len()], 292 | out, 293 | Duration::from_millis(C::DELAY_RESPONSE_MS as _), 294 | ); 295 | let res = match res { 296 | Ok(res) => { 297 | self.set_sleep_delay(Delay::new(Duration::from_millis(C::DELAY_COMMAND_MS))); 298 | res 299 | }, 300 | Err(e) => { 301 | self.set_sleep_delay(Delay::new(Duration::from_millis(DELAY_COMMAND_FAILED_MS))); 302 | return Err(e) 303 | }, 304 | }; 305 | 306 | let res = C::Ok::decode(res); 307 | 308 | if res.is_err() { 309 | self.set_sleep_delay(Delay::new(Duration::from_millis(DELAY_COMMAND_FAILED_MS))); 310 | } 311 | 312 | res.map_err(From::from) 313 | } 314 | } 315 | --------------------------------------------------------------------------------