├── .github └── workflows │ └── mccs.yml ├── .gitignore ├── .rustfmt.toml ├── COPYING ├── Cargo.lock ├── Cargo.toml ├── README.md ├── caps ├── .gitignore ├── Cargo.toml ├── README.md └── src │ ├── caps.rs │ ├── entries.rs │ ├── lib.rs │ └── testdata.rs ├── ci.nix ├── db ├── .gitignore ├── Cargo.toml ├── README.md ├── data │ └── mccs.yml └── src │ ├── lib.rs │ └── version_req.rs ├── default.nix ├── flake.lock ├── flake.nix ├── lock.nix ├── shell.nix └── src └── lib.rs /.github/workflows/mccs.yml: -------------------------------------------------------------------------------- 1 | env: 2 | CI_ALLOW_ROOT: '1' 3 | CI_CONFIG: ./ci.nix 4 | CI_PLATFORM: gh-actions 5 | jobs: 6 | ci: 7 | name: mccs 8 | runs-on: ubuntu-latest 9 | steps: 10 | - id: checkout 11 | name: git clone 12 | uses: actions/checkout@v1 13 | with: 14 | submodules: true 15 | - id: nix-install 16 | name: nix install 17 | uses: arcnmx/ci/actions/nix/install@master 18 | - id: ci-setup 19 | name: nix setup 20 | uses: arcnmx/ci/actions/nix/run@master 21 | with: 22 | attrs: ci.run.setup 23 | quiet: false 24 | - id: ci-dirty 25 | name: nix test dirty 26 | uses: arcnmx/ci/actions/nix/run@master 27 | with: 28 | attrs: ci.run.test 29 | command: ci-build-dirty 30 | quiet: false 31 | stdout: ${{ runner.temp }}/ci.build.dirty 32 | - id: ci-test 33 | name: nix test build 34 | uses: arcnmx/ci/actions/nix/run@master 35 | with: 36 | attrs: ci.run.test 37 | command: ci-build-realise 38 | ignore-exit-code: true 39 | quiet: false 40 | stdin: ${{ runner.temp }}/ci.build.dirty 41 | - env: 42 | CI_EXIT_CODE: ${{ steps.ci-test.outputs.exit-code }} 43 | id: ci-summary 44 | name: nix test results 45 | uses: arcnmx/ci/actions/nix/run@master 46 | with: 47 | attrs: ci.run.test 48 | command: ci-build-summarise 49 | quiet: false 50 | stdin: ${{ runner.temp }}/ci.build.dirty 51 | stdout: ${{ runner.temp }}/ci.build.cache 52 | - env: 53 | CACHIX_SIGNING_KEY: ${{ secrets.CACHIX_SIGNING_KEY }} 54 | id: ci-cache 55 | if: always() 56 | name: nix test cache 57 | uses: arcnmx/ci/actions/nix/run@master 58 | with: 59 | attrs: ci.run.test 60 | command: ci-build-cache 61 | quiet: false 62 | stdin: ${{ runner.temp }}/ci.build.cache 63 | ci-check: 64 | name: mccs check 65 | runs-on: ubuntu-latest 66 | steps: 67 | - id: checkout 68 | name: git clone 69 | uses: actions/checkout@v1 70 | with: 71 | submodules: true 72 | - id: nix-install 73 | name: nix install 74 | uses: arcnmx/ci/actions/nix/install@master 75 | - id: ci-action-build 76 | name: nix build ci.gh-actions.configFile 77 | uses: arcnmx/ci/actions/nix/build@master 78 | with: 79 | attrs: ci.gh-actions.configFile 80 | out-link: .ci/workflow.yml 81 | - id: ci-action-compare 82 | name: gh-actions compare 83 | uses: arcnmx/ci/actions/nix/run@master 84 | with: 85 | args: -u .github/workflows/mccs.yml .ci/workflow.yml 86 | attrs: nixpkgs.diffutils 87 | command: diff 88 | name: mccs 89 | 'on': 90 | - push 91 | - pull_request 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /result* 2 | /target/ 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 = "autocfg" 7 | version = "1.1.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 10 | 11 | [[package]] 12 | name = "hashbrown" 13 | version = "0.12.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 16 | 17 | [[package]] 18 | name = "indexmap" 19 | version = "1.9.2" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 22 | dependencies = [ 23 | "autocfg", 24 | "hashbrown", 25 | ] 26 | 27 | [[package]] 28 | name = "itoa" 29 | version = "1.0.5" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 32 | 33 | [[package]] 34 | name = "mccs" 35 | version = "0.2.0" 36 | 37 | [[package]] 38 | name = "mccs-caps" 39 | version = "0.2.0" 40 | dependencies = [ 41 | "mccs", 42 | "nom", 43 | ] 44 | 45 | [[package]] 46 | name = "mccs-db" 47 | version = "0.2.0" 48 | dependencies = [ 49 | "mccs", 50 | "mccs-caps", 51 | "nom", 52 | "serde", 53 | "serde_yaml", 54 | ] 55 | 56 | [[package]] 57 | name = "memchr" 58 | version = "2.5.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 61 | 62 | [[package]] 63 | name = "minimal-lexical" 64 | version = "0.2.1" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 67 | 68 | [[package]] 69 | name = "nom" 70 | version = "7.1.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 73 | dependencies = [ 74 | "memchr", 75 | "minimal-lexical", 76 | ] 77 | 78 | [[package]] 79 | name = "proc-macro2" 80 | version = "1.0.51" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" 83 | dependencies = [ 84 | "unicode-ident", 85 | ] 86 | 87 | [[package]] 88 | name = "quote" 89 | version = "1.0.23" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 92 | dependencies = [ 93 | "proc-macro2", 94 | ] 95 | 96 | [[package]] 97 | name = "ryu" 98 | version = "1.0.12" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" 101 | 102 | [[package]] 103 | name = "serde" 104 | version = "1.0.152" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 107 | dependencies = [ 108 | "serde_derive", 109 | ] 110 | 111 | [[package]] 112 | name = "serde_derive" 113 | version = "1.0.152" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" 116 | dependencies = [ 117 | "proc-macro2", 118 | "quote", 119 | "syn", 120 | ] 121 | 122 | [[package]] 123 | name = "serde_yaml" 124 | version = "0.9.17" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567" 127 | dependencies = [ 128 | "indexmap", 129 | "itoa", 130 | "ryu", 131 | "serde", 132 | "unsafe-libyaml", 133 | ] 134 | 135 | [[package]] 136 | name = "syn" 137 | version = "1.0.107" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 140 | dependencies = [ 141 | "proc-macro2", 142 | "quote", 143 | "unicode-ident", 144 | ] 145 | 146 | [[package]] 147 | name = "unicode-ident" 148 | version = "1.0.6" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 151 | 152 | [[package]] 153 | name = "unsafe-libyaml" 154 | version = "0.2.5" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "bc7ed8ba44ca06be78ea1ad2c3682a43349126c8818054231ee6f4748012aed2" 157 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mccs" 3 | version = "0.2.0" # keep in sync with README and html_root_url 4 | authors = ["arcnmx"] 5 | edition = "2021" 6 | 7 | description = "VESA Monitor Control Command Set" 8 | keywords = ["ddc", "mccs", "vcp", "vesa"] 9 | categories = ["hardware-support"] 10 | 11 | documentation = "https://docs.rs/mccs" 12 | repository = "https://github.com/arcnmx/mccs-rs" 13 | readme = "README.md" 14 | license = "MIT" 15 | 16 | include = [ 17 | "/src/**/*.rs", 18 | "/README*", 19 | "/COPYING*", 20 | ] 21 | 22 | [workspace] 23 | members = ["caps", "db"] 24 | 25 | [badges] 26 | maintenance = { status = "passively-maintained" } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mccs 2 | 3 | [![release-badge][]][cargo] [![docs-badge][]][docs] [![license-badge][]][license] 4 | 5 | `mccs` implements the VESA [Monitor Control Command Set](https://en.wikipedia.org/wiki/Monitor_Control_Command_Set). 6 | The library is split up into a few sub-crates: 7 | 8 | - [`mccs`](https://crates.io/crates/mccs) contains the common types describing the MCCS and VCP data structures. 9 | - [`mccs-caps`](https://crates.io/crates/mccs-caps) provides a parser for the MCCS capability string. 10 | - [`mccs-db`](https://crates.io/crates/mccs-db) contains the human-readable descriptions of VCP features from the 11 | MCCS spec. 12 | 13 | ## [Documentation][docs] 14 | 15 | See the [documentation][docs] for up to date information. 16 | 17 | [release-badge]: https://img.shields.io/crates/v/mccs.svg?style=flat-square 18 | [cargo]: https://crates.io/crates/mccs 19 | [docs-badge]: https://img.shields.io/badge/API-docs-blue.svg?style=flat-square 20 | [docs]: http://docs.rs/mccs/ 21 | [license-badge]: https://img.shields.io/badge/license-MIT-ff69b4.svg?style=flat-square 22 | [license]: https://github.com/arcnmx/mccs-rs/blob/main/COPYING 23 | -------------------------------------------------------------------------------- /caps/.gitignore: -------------------------------------------------------------------------------- 1 | /Cargo.lock 2 | /target/ 3 | -------------------------------------------------------------------------------- /caps/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mccs-caps" 3 | version = "0.2.0" # keep in sync with html_root_url 4 | authors = ["arcnmx"] 5 | edition = "2021" 6 | 7 | description = "MCCS capability string parser" 8 | keywords = ["ddc", "mccs", "vcp", "vesa"] 9 | categories = ["hardware-support", "parser-implementations"] 10 | 11 | documentation = "https://docs.rs/mccs-caps" 12 | repository = "https://github.com/arcnmx/mccs-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 = { version = "0.2", path = "../" } 27 | nom = "7" 28 | -------------------------------------------------------------------------------- /caps/README.md: -------------------------------------------------------------------------------- 1 | # MCCS Capabilities String Parser 2 | 3 | [![release-badge][]][cargo] [![docs-badge][]][docs] [![license-badge][]][license] 4 | 5 | `mccs-caps` provides a parser for the [MCCS](https://en.wikipedia.org/wiki/Monitor_Control_Command_Set) 6 | capability string. 7 | 8 | ## [Documentation][docs] 9 | 10 | See the [documentation][docs] for up to date information. 11 | 12 | [release-badge]: https://img.shields.io/crates/v/mccs-caps.svg?style=flat-square 13 | [cargo]: https://crates.io/crates/mccs-caps 14 | [docs-badge]: https://img.shields.io/badge/API-docs-blue.svg?style=flat-square 15 | [docs]: http://docs.rs/mccs-caps/ 16 | [license-badge]: https://img.shields.io/badge/license-MIT-ff69b4.svg?style=flat-square 17 | [license]: https://github.com/arcnmx/mccs-rs/blob/main/COPYING 18 | -------------------------------------------------------------------------------- /caps/src/caps.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{bracketed, map_err, trim_spaces, OResult, Value, ValueParser}, 3 | nom::{ 4 | branch::alt, 5 | bytes::complete::{is_not, tag, take}, 6 | character::complete::{char, space1, u8}, 7 | combinator::{all_consuming, map, map_parser, map_res, opt, rest}, 8 | multi::{fold_many0, many0, separated_list0}, 9 | sequence::{separated_pair, tuple}, 10 | Finish, IResult, 11 | }, 12 | std::{borrow::Cow, fmt, io, str}, 13 | }; 14 | 15 | #[derive(Clone, PartialEq, Eq)] 16 | pub struct VcpValue { 17 | pub value: u8, 18 | pub sub_values: Option>, 19 | } 20 | 21 | impl VcpValue { 22 | pub fn new(value: u8) -> Self { 23 | VcpValue { 24 | value, 25 | sub_values: None, 26 | } 27 | } 28 | 29 | pub fn sub_values(&self) -> &[u8] { 30 | self.sub_values.as_ref().map(|v| &v[..]).unwrap_or_default() 31 | } 32 | } 33 | 34 | impl fmt::Debug for VcpValue { 35 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 36 | let mut debug = f.debug_tuple("VcpValue"); 37 | let debug = debug.field(&self.value); 38 | match self.sub_values() { 39 | &[] => debug.finish(), 40 | values => debug.field(&values).finish(), 41 | } 42 | } 43 | } 44 | 45 | #[derive(Clone, PartialEq, Eq)] 46 | pub struct Vcp { 47 | pub feature: u8, 48 | pub values: Option>, 49 | } 50 | 51 | impl Vcp { 52 | pub fn values(&self) -> &[VcpValue] { 53 | self.values.as_ref().map(|v| &v[..]).unwrap_or_default() 54 | } 55 | } 56 | 57 | impl fmt::Debug for Vcp { 58 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 59 | let mut debug = f.debug_tuple("Vcp"); 60 | let debug = debug.field(&self.feature); 61 | match self.values() { 62 | &[] => debug.finish(), 63 | values => debug.field(&values).finish(), 64 | } 65 | } 66 | } 67 | 68 | #[derive(Clone, Debug, PartialEq, Eq)] 69 | pub struct VcpName<'i> { 70 | pub feature: u8, 71 | pub name: Option>, 72 | pub value_names: Option>>, 73 | } 74 | 75 | impl<'i> VcpName<'i> { 76 | pub fn value_names(&self) -> &[Cow<'i, str>] { 77 | self.value_names.as_ref().map(|v| &v[..]).unwrap_or_default() 78 | } 79 | } 80 | 81 | /// Parsed display capabilities string entry 82 | #[derive(Clone, Debug, PartialEq, Eq)] 83 | pub enum Cap<'a> { 84 | Protocol(&'a str), 85 | Type(&'a str), 86 | Model(&'a str), 87 | Commands(Vec), 88 | Whql(u8), 89 | MccsVersion(u8, u8), 90 | Vcp(Vec), 91 | VcpNames(Vec>), 92 | Edid(&'a [u8]), 93 | Vdif(&'a [u8]), 94 | Unknown(Value<'a>), 95 | } 96 | 97 | impl<'i> Cap<'i> { 98 | pub fn parse_entries(entries: ValueParser<'i>) -> impl Iterator>> + 'i { 99 | entries 100 | .nom_iter() 101 | .map(|e| e.and_then(|e| Self::parse_entry(e)).map_err(map_err)) 102 | } 103 | 104 | pub fn parse_entry(value: Value<'i>) -> OResult<'i, Cap<'i>> { 105 | match value { 106 | Value::String { tag, value } => Self::parse_string(tag, value), 107 | Value::Binary { tag, data } => Ok(Self::parse_data(tag, data)), 108 | } 109 | } 110 | 111 | pub fn parse_data(tag: &'i str, i: &'i [u8]) -> Cap<'i> { 112 | match tag { 113 | "edid" => Cap::Edid(i), 114 | "vdif" => Cap::Vdif(i), 115 | _ => Cap::Unknown(Value::Binary { tag, data: i }), 116 | } 117 | } 118 | 119 | pub fn parse_string(tag: &'i str, i: &'i [u8]) -> OResult<'i, Cap<'i>> { 120 | match tag { 121 | "prot" => all_consuming(map(value, Cap::Protocol))(i), 122 | "type" => all_consuming(map(value, Cap::Type))(i), 123 | "model" => all_consuming(map(value, Cap::Model))(i), 124 | "cmds" => all_consuming(map(hexarray, Cap::Commands))(i), 125 | "mswhql" => all_consuming(map(map_parser(take(1usize), u8), Cap::Whql))(i), 126 | "mccs_ver" => all_consuming(map(mccs_ver, |(major, minor)| Cap::MccsVersion(major, minor)))(i), 127 | // hack for Apple Cinema Display 128 | "vcp" | "VCP" => all_consuming(map(trim_spaces(many0(vcp)), Cap::Vcp))(i), 129 | "vcpname" => all_consuming(map(many0(vcpname), Cap::VcpNames))(i), 130 | _ => Ok((Default::default(), Cap::Unknown(Value::String { tag, value: i }))), 131 | } 132 | .finish() 133 | .map(|(_, c)| c) 134 | } 135 | } 136 | 137 | fn backslash_escape(i: &[u8]) -> IResult<&[u8], String> { 138 | // TODO: I'd use https://docs.rs/nom/7.1.1/nom/bytes/complete/fn.escaped_transform.html instead, 139 | // but it can't deal with dynamic transforms due to ExtendInto not being impl'd on anything useful 140 | // like Vec or [u8; N] or something... 141 | let escaped = |i| { 142 | let (i, _) = tag("\\x")(i)?; 143 | map_str(take(2usize), |h| u8::from_str_radix(h, 16).map(|v| v as char), i) 144 | }; 145 | fold_many0( 146 | alt(( 147 | escaped, 148 | // TODO: other escapes like \\ \n etc? unclear in access bus spec... 149 | map(take(1usize), |s: &[u8]| s[0] as char), // TODO, this isn't utf8 parsing, should it be? .-. 150 | )), 151 | || String::new(), 152 | |mut s: String, c| { 153 | s.push(c); 154 | s 155 | }, 156 | )(i) 157 | } 158 | 159 | fn value_escape_nospace(i: &[u8]) -> IResult<&[u8], Cow> { 160 | map_parser( 161 | is_not(" ()"), 162 | alt(( 163 | all_consuming(map(map_res(is_not("\\"), str::from_utf8), Cow::Borrowed)), 164 | map(all_consuming(backslash_escape), Cow::Owned), 165 | )), 166 | )(i) 167 | } 168 | 169 | fn value(i: &[u8]) -> IResult<&[u8], &str> { 170 | map_res(rest, str::from_utf8)(i) 171 | } 172 | 173 | fn hexarray(i: &[u8]) -> IResult<&[u8], Vec> { 174 | many0(trim_spaces(hexvalue))(i) 175 | } 176 | 177 | fn map_str<'i, O, E2, F, G>(mut parser: F, f: G, i: &'i [u8]) -> IResult<&'i [u8], O> 178 | where 179 | F: nom::Parser<&'i [u8], &'i [u8], nom::error::Error<&'i [u8]>>, 180 | G: FnMut(&'i str) -> Result, 181 | { 182 | use nom::Parser; 183 | 184 | let mut f = map_res(rest, f); 185 | let (i, s) = map_res(|i| parser.parse(i), |i| str::from_utf8(i.into()))(i)?; 186 | match f.parse(s) { 187 | Ok((_, v)) => Ok((i, v)), 188 | Err(e) => Err(e.map(|e: nom::error::Error<_>| nom::error::Error { input: i, code: e.code })), 189 | } 190 | } 191 | 192 | fn hexvalue(i: &[u8]) -> IResult<&[u8], u8> { 193 | map_str(take(2usize), |s| u8::from_str_radix(s, 16), i) 194 | } 195 | 196 | fn vcp_value(i: &[u8]) -> IResult<&[u8], VcpValue> { 197 | map( 198 | tuple((trim_spaces(hexvalue), opt(bracketed(many0(trim_spaces(hexvalue)))))), 199 | |(value, sub_values)| VcpValue { value, sub_values }, 200 | )(i) 201 | } 202 | 203 | fn vcp(i: &[u8]) -> IResult<&[u8], Vcp> { 204 | let featurevalues = bracketed(many0(trim_spaces(vcp_value))); 205 | map( 206 | tuple((trim_spaces(hexvalue), opt(featurevalues))), 207 | |(feature, values)| Vcp { feature, values }, 208 | )(i) 209 | } 210 | 211 | fn vcpname(i: &[u8]) -> IResult<&[u8], VcpName> { 212 | let (i, feature) = trim_spaces(hexvalue)(i)?; 213 | let (i, (name, value_names)) = bracketed(tuple(( 214 | opt(value_escape_nospace), 215 | opt(bracketed(trim_spaces(separated_list0(space1, value_escape_nospace)))), 216 | )))(i)?; 217 | Ok((i, VcpName { 218 | feature, 219 | name, 220 | value_names, 221 | })) 222 | } 223 | 224 | fn mccs_ver(i: &[u8]) -> IResult<&[u8], (u8, u8)> { 225 | alt(( 226 | separated_pair(u8, char('.'), u8), 227 | tuple((map_parser(take(2usize), u8), map_parser(take(2usize), u8))), 228 | ))(i) 229 | } 230 | 231 | #[test] 232 | fn vcpname_temp() { 233 | let testdata = br"14((9300 6500 5500))44(Rotate)80(Do\x20this(On Off))82(Fixit)"; 234 | let expected = [ 235 | VcpName { 236 | feature: 0x14, 237 | name: None, 238 | value_names: Some(vec!["9300".into(), "6500".into(), "5500".into()]), 239 | }, 240 | VcpName { 241 | feature: 0x44, 242 | name: Some("Rotate".into()), 243 | value_names: None, 244 | }, 245 | VcpName { 246 | feature: 0x80, 247 | name: Some("Do this".into()), 248 | value_names: Some(vec!["On".into(), "Off".into()]), 249 | }, 250 | VcpName { 251 | feature: 0x82, 252 | name: Some("Fixit".into()), 253 | value_names: None, 254 | }, 255 | ]; 256 | 257 | let (_, vcps) = all_consuming(many0(vcpname))(testdata).finish().unwrap(); 258 | assert_eq!(vcps.len(), expected.len()); 259 | 260 | for (vcp, exp) in vcps.into_iter().zip(expected) { 261 | assert_eq!(vcp, exp); 262 | } 263 | } 264 | 265 | #[test] 266 | fn vcpname_brightness() { 267 | let testdata = b"10(Brightness)"; 268 | let expected = VcpName { 269 | feature: 0x10, 270 | name: Some("Brightness".into()), 271 | value_names: None, 272 | }; 273 | let (_, vcp) = all_consuming(vcpname)(testdata).finish().unwrap(); 274 | 275 | assert_eq!(vcp, expected); 276 | } 277 | -------------------------------------------------------------------------------- /caps/src/entries.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{bracketed, map_err, trim_spaces, OResult, OResultI, Value}, 3 | nom::{ 4 | branch::alt, 5 | bytes::complete::{tag, take, take_while1, take_while_m_n}, 6 | character::{ 7 | complete::{alphanumeric1, char, space0, u32}, 8 | is_alphanumeric, 9 | }, 10 | combinator::{complete, fail, map, map_res, not, peek}, 11 | error::{self, ErrorKind}, 12 | sequence::{preceded, tuple}, 13 | IResult, Parser, 14 | }, 15 | std::{io, str}, 16 | }; 17 | 18 | #[derive(Clone, Default, Debug, PartialEq, Eq)] 19 | pub struct ValueParser<'i> { 20 | pub input: &'i [u8], 21 | pub brackets: Option, 22 | previous_tag: Option<&'i str>, 23 | } 24 | 25 | impl<'i> Iterator for ValueParser<'i> { 26 | type Item = io::Result>; 27 | 28 | fn next(&mut self) -> Option { 29 | if self.input.is_empty() { 30 | return None 31 | } 32 | 33 | Some(self.nom_result().map_err(map_err)) 34 | } 35 | } 36 | 37 | impl<'i> ValueParser<'i> { 38 | pub fn new(capability_string: &'i [u8]) -> ValueParser<'i> { 39 | Self { 40 | input: capability_string, 41 | brackets: None, 42 | previous_tag: None, 43 | } 44 | } 45 | 46 | pub fn nom_iter(mut self) -> impl Iterator>> + 'i { 47 | std::iter::from_fn(move || match self.input.is_empty() { 48 | true => None, 49 | false => Some(self.nom_result()), 50 | }) 51 | } 52 | 53 | pub fn nom_result(&mut self) -> OResult<'i, Value<'i>> { 54 | match self.nom() { 55 | Ok(o) => Ok(o), 56 | Err(nom::Err::Error(e) | nom::Err::Failure(e)) => Err(e), 57 | Err(nom::Err::Incomplete(_)) => Err(error::Error::new(self.input, ErrorKind::Eof)), 58 | } 59 | } 60 | 61 | pub fn nom(&mut self) -> OResultI<'i, Value<'i>> { 62 | self.parse(self.input).map(|(_, e)| e) 63 | } 64 | } 65 | 66 | impl<'i> Parser<&'i [u8], Value<'i>, error::Error<&'i [u8]>> for ValueParser<'i> { 67 | fn parse(&mut self, input: &'i [u8]) -> IResult<&'i [u8], Value<'i>> { 68 | let (input, mut brackets) = match self.brackets { 69 | None => { 70 | let (input, brackets) = caps_prefix(input)?; 71 | self.input = input; 72 | self.brackets = Some(brackets); 73 | (input, brackets) 74 | }, 75 | Some(brackets) => (input, brackets), 76 | }; 77 | 78 | let (input, e) = Value::parse_nom(input, self.previous_tag)?; 79 | self.previous_tag = Some(e.tag()); 80 | self.input = input; 81 | 82 | let input = match caps_suffix(brackets, input) { 83 | Ok((rest, _)) if rest == input => input, 84 | Ok((input, brackets_consumed)) => { 85 | brackets -= brackets_consumed; 86 | self.input = input; 87 | self.brackets = Some(brackets); 88 | input 89 | }, 90 | Err(_) => unreachable!(), 91 | }; 92 | 93 | Ok((input, e)) 94 | } 95 | } 96 | 97 | fn caps_prefix(i: &[u8]) -> IResult<&[u8], usize> { 98 | // hack around Apple Cinema Display and other displays without any surrounding brackets 99 | // and displays with too many brackets 100 | let (i, brackets) = take_while_m_n(0, 2, |c| c == b'(')(i)?; 101 | Ok((i, brackets.len())) 102 | } 103 | 104 | fn caps_suffix(mut brackets: usize, mut i: &[u8]) -> IResult<&[u8], usize> { 105 | let mut bracket_count = 0; 106 | loop { 107 | i = match i.split_first() { 108 | Some((&b')', i)) => match brackets.checked_sub(1) { 109 | Some(b) => { 110 | brackets = b; 111 | bracket_count += 1; 112 | i 113 | }, 114 | None => break, 115 | }, 116 | Some((&0 | &b' ', i)) => i, 117 | _ => break, 118 | }; 119 | } 120 | Ok((i, bracket_count)) 121 | } 122 | 123 | impl<'i> Value<'i> { 124 | pub fn parse_nom(input: &'i [u8], previous_tag: Option<&'i str>) -> IResult<&'i [u8], Self> { 125 | let (i, _) = space0(input)?; 126 | let (i, id) = alt(( 127 | map( 128 | preceded(tuple((tag("model"), peek(not(char('('))))), modelhack), 129 | |value| Err(Value::String { tag: "model", value }), 130 | ), 131 | map( 132 | |i| { 133 | if previous_tag == Some("type") { 134 | modelhack(i) 135 | } else { 136 | fail(i) 137 | } 138 | }, 139 | |value| Err(Value::String { tag: "model", value }), 140 | ), 141 | map(ident, Ok), 142 | ))(i)?; 143 | let (i, cap) = match id { 144 | Ok(id) => alt(( 145 | map(preceded(tag(" bin"), bracketed(binary)), |data| Value::Binary { 146 | tag: id, 147 | data, 148 | }), 149 | map(bracketed(balancedparens), |value| Value::String { tag: id, value }), 150 | ))(i), 151 | Err(corrupted_model) => Ok((i, corrupted_model)), 152 | }?; 153 | let (i, _) = space0(i)?; 154 | Ok((i, cap)) 155 | } 156 | 157 | pub fn nom_parser() -> ValueParser<'i> { 158 | Default::default() 159 | } 160 | } 161 | 162 | fn binary(i: &[u8]) -> IResult<&[u8], &[u8]> { 163 | let (i, count) = trim_spaces(u32)(i)?; 164 | bracketed(take(count))(i) 165 | } 166 | 167 | fn modelhack(i: &[u8]) -> IResult<&[u8], &[u8]> { 168 | let cmds = b"cmds"; 169 | let (rest, model) = alphanumeric1(i)?; 170 | if !model.ends_with(cmds) || model == cmds { 171 | let (_, ()) = fail(i)?; 172 | } 173 | let _ = peek(char('('))(rest)?; 174 | let model = &model[..model.len() - 4]; 175 | Ok((&i[model.len()..], model)) 176 | } 177 | 178 | fn ident(i: &[u8]) -> IResult<&[u8], &str> { 179 | map_res(take_while1(|c| is_alphanumeric(c) || c == b'_'), str::from_utf8)(i) 180 | } 181 | 182 | fn balancedparens(i: &[u8]) -> IResult<&[u8], &[u8]> { 183 | complete(balancedparens_incomplete)(i) 184 | } 185 | 186 | fn balancedparens_incomplete(i: &[u8]) -> IResult<&[u8], &[u8]> { 187 | use nom::InputTake; 188 | 189 | let mut depth = 0usize; 190 | for (x, &c) in i.iter().enumerate() { 191 | match c { 192 | b')' => match depth.checked_sub(1) { 193 | Some(v) => depth = v, 194 | None => return Ok(i.take_split(x)), 195 | }, 196 | b'(' => { 197 | depth += 1; 198 | }, 199 | _ => (), 200 | } 201 | } 202 | match depth { 203 | 0 => Ok((i, Default::default())), 204 | depth => Err(nom::Err::Incomplete(nom::Needed::new(depth))), 205 | } 206 | } 207 | 208 | #[test] 209 | fn model_hacks() { 210 | let testdata = [ 211 | ( 212 | &[ 213 | &b"type(lcd)ABCdcmds()"[..], 214 | &b"type(lcd)modelABCdcmds()"[..], 215 | &b"type(lcd)model(ABCd)cmds()"[..], 216 | ][..], 217 | &[ 218 | Value::String { 219 | tag: "type", 220 | value: b"lcd", 221 | }, 222 | Value::String { 223 | tag: "model", 224 | value: b"ABCd", 225 | }, 226 | Value::String { 227 | tag: "cmds", 228 | value: b"", 229 | }, 230 | ][..], 231 | ), 232 | ( 233 | &[&b"type(lcd)cmds()model(ABCd)"[..]][..], 234 | &[ 235 | Value::String { 236 | tag: "type", 237 | value: b"lcd", 238 | }, 239 | Value::String { 240 | tag: "cmds", 241 | value: b"", 242 | }, 243 | Value::String { 244 | tag: "model", 245 | value: b"ABCd", 246 | }, 247 | ][..], 248 | ), 249 | ]; 250 | 251 | for (testdatas, expected) in testdata { 252 | for testdata in testdatas { 253 | let testdata: Result, _> = ValueParser::new(testdata).collect(); 254 | assert_eq!(testdata.unwrap(), expected); 255 | } 256 | } 257 | } 258 | 259 | #[test] 260 | fn bin_entries() { 261 | let testdata = b"edid bin(3(\xff) ))vdif bin(3 (abc))unknown bin(2(ab))"; 262 | let expected = [ 263 | Value::Binary { 264 | tag: "edid", 265 | data: b"\xff) ", 266 | }, 267 | Value::Binary { 268 | tag: "vdif", 269 | data: b"abc", 270 | }, 271 | Value::Binary { 272 | tag: "unknown", 273 | data: b"ab", 274 | }, 275 | ]; 276 | 277 | let entries: Result, _> = ValueParser::new(testdata).collect(); 278 | assert_eq!(entries.unwrap(), expected); 279 | } 280 | -------------------------------------------------------------------------------- /caps/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![doc(html_root_url = "https://docs.rs/mccs-caps/0.2.0")] 3 | 4 | //! MCCS compliant displays will report their supported capabilities in a string 5 | //! retrieved over DDC/CI. The format of this string is specified in the DDC 6 | //! specification, MCCS, and ACCESS.bus section 7. This crate parses the 7 | //! capability string into structured data. 8 | 9 | pub use self::{ 10 | caps::{Cap, Vcp, VcpName, VcpValue}, 11 | entries::ValueParser, 12 | }; 13 | use { 14 | mccs::{Capabilities, UnknownData, UnknownTag, VcpDescriptor, Version}, 15 | nom::Finish, 16 | std::{fmt, io, str}, 17 | }; 18 | 19 | #[cfg(test)] 20 | mod testdata; 21 | 22 | #[allow(missing_docs)] 23 | mod caps; 24 | #[allow(missing_docs)] 25 | mod entries; 26 | 27 | /// Parses a MCCS capability string. 28 | pub fn parse_capabilities>(capability_string: C) -> io::Result { 29 | let capability_string = capability_string.as_ref(); 30 | let entries = Value::parse_capabilities(capability_string); 31 | 32 | // TODO: check for multiple tags of anything only allowed once? 33 | 34 | let mut caps = Capabilities::default(); 35 | let mut vcpnames = Vec::new(); 36 | for cap in Cap::parse_entries(entries) { 37 | match cap? { 38 | Cap::Protocol(protocol) => caps.protocol = Some(protocol.into()), 39 | Cap::Type(ty) => caps.ty = Some(ty.into()), 40 | Cap::Model(model) => caps.model = Some(model.into()), 41 | Cap::Commands(ref cmds) => caps.commands = cmds.clone(), 42 | Cap::Whql(whql) => caps.ms_whql = Some(whql), 43 | Cap::MccsVersion(major, minor) => caps.mccs_version = Some(Version::new(major, minor)), 44 | Cap::Vcp(ref vcp) => 45 | for Vcp { 46 | feature: code, 47 | ref values, 48 | } in vcp 49 | { 50 | caps.vcp_features 51 | .entry(*code) 52 | .or_insert_with(|| VcpDescriptor::default()) 53 | .values 54 | .extend(values.iter().flat_map(|i| i).map(|v| (v.value, None))) 55 | }, 56 | Cap::VcpNames(v) => vcpnames.extend(v), // wait until after processing vcp() section 57 | Cap::Unknown(value) => caps.unknown_tags.push(UnknownTag { 58 | name: value.tag().into(), 59 | data: match value { 60 | Value::String { value, .. } => match str::from_utf8(value) { 61 | Ok(value) => UnknownData::String(value.into()), 62 | Err(..) => UnknownData::StringBytes(value.into()), 63 | }, 64 | Value::Binary { data, .. } => UnknownData::Binary(data.into()), 65 | }, 66 | }), 67 | Cap::Edid(edid) => caps.edid = Some(edid.into()), 68 | Cap::Vdif(vdif) => caps.vdif.push(vdif.into()), 69 | } 70 | } 71 | 72 | for VcpName { 73 | feature: code, 74 | name, 75 | value_names, 76 | } in vcpnames 77 | { 78 | if let Some(vcp) = caps.vcp_features.get_mut(&code) { 79 | if let Some(name) = name { 80 | vcp.name = Some(name.into()) 81 | } 82 | 83 | if let Some(value_names) = value_names { 84 | for ((_, dest), name) in vcp.values.iter_mut().zip(value_names) { 85 | *dest = Some(name.into()) 86 | } 87 | } 88 | } else { 89 | // TODO: should this be an error if it wasn't specified in vcp()? 90 | } 91 | } 92 | 93 | Ok(caps) 94 | } 95 | 96 | /// An entry from a capability string 97 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 98 | pub enum Value<'i> { 99 | /// A normal string 100 | String { 101 | /// The value name 102 | tag: &'i str, 103 | /// String contents 104 | value: &'i [u8], 105 | }, 106 | /// Raw binary data 107 | Binary { 108 | /// The value name 109 | tag: &'i str, 110 | /// Data contents 111 | data: &'i [u8], 112 | }, 113 | } 114 | 115 | impl<'i> Value<'i> { 116 | /// Create a new iterator over the values in a capability string 117 | pub fn parse_capabilities(capability_string: &'i [u8]) -> ValueParser<'i> { 118 | ValueParser::new(capability_string) 119 | } 120 | 121 | /// Parse a single capability string entry 122 | pub fn parse(data: &'i str) -> io::Result { 123 | Self::parse_bytes(data.as_bytes()) 124 | } 125 | 126 | /// Parse a single capability string entry 127 | pub fn parse_bytes(data: &'i [u8]) -> io::Result { 128 | Self::parse_nom(data, None).finish().map(|(_, v)| v).map_err(map_err) 129 | } 130 | 131 | /// The value name 132 | pub fn tag(&self) -> &'i str { 133 | match *self { 134 | Value::String { tag, .. } => tag, 135 | Value::Binary { tag, .. } => tag, 136 | } 137 | } 138 | } 139 | 140 | impl From> for UnknownTag { 141 | fn from(v: Value) -> Self { 142 | UnknownTag { 143 | name: v.tag().into(), 144 | data: match v { 145 | Value::Binary { data, .. } => UnknownData::Binary(data.into()), 146 | Value::String { value, .. } => match str::from_utf8(value) { 147 | Ok(value) => UnknownData::String(value.into()), 148 | Err(_) => UnknownData::StringBytes(value.into()), 149 | }, 150 | }, 151 | } 152 | } 153 | } 154 | 155 | impl<'a> From<&'a UnknownTag> for Value<'a> { 156 | fn from(v: &'a UnknownTag) -> Self { 157 | let tag = &v.name; 158 | match &v.data { 159 | UnknownData::Binary(data) => Value::Binary { tag, data }, 160 | UnknownData::StringBytes(value) => Value::String { tag, value }, 161 | UnknownData::String(value) => Value::String { 162 | tag, 163 | value: value.as_bytes(), 164 | }, 165 | } 166 | } 167 | } 168 | 169 | impl<'i> fmt::Display for Value<'i> { 170 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 171 | match self { 172 | Value::String { tag, value } => write!(f, "{tag}({})", value.escape_ascii()), 173 | Value::Binary { tag, data } => write!(f, "{tag} bin({}({}))", data.len(), data.escape_ascii()), 174 | } 175 | } 176 | } 177 | 178 | pub(crate) type OResult<'i, O> = Result>; 179 | pub(crate) type OResultI<'i, O> = Result>>; 180 | 181 | pub(crate) fn map_err(e: nom::error::Error<&[u8]>) -> io::Error { 182 | use nom::error::{Error, ErrorKind}; 183 | 184 | io::Error::new( 185 | match e.code { 186 | ErrorKind::Eof | ErrorKind::Complete => io::ErrorKind::UnexpectedEof, 187 | _ => io::ErrorKind::InvalidData, 188 | }, 189 | Error { 190 | input: e.input.escape_ascii().to_string(), 191 | code: e.code, 192 | }, 193 | ) 194 | } 195 | 196 | pub(crate) fn trim_spaces(parser: P) -> impl FnMut(I) -> nom::IResult 197 | where 198 | P: nom::Parser, 199 | E: nom::error::ParseError, 200 | I: Clone + nom::InputTakeAtPosition, 201 | ::Item: nom::AsChar + Clone, 202 | { 203 | use nom::{character::complete::space0, sequence::delimited}; 204 | 205 | delimited(space0, parser, space0) 206 | } 207 | 208 | pub(crate) fn bracketed(parser: P) -> impl FnMut(I) -> nom::IResult 209 | where 210 | P: nom::Parser, 211 | E: nom::error::ParseError, 212 | I: Clone + nom::Slice> + nom::InputIter, 213 | ::Item: nom::AsChar, 214 | { 215 | use nom::{character::complete::char, sequence::delimited}; 216 | 217 | delimited(char('('), parser, char(')')) 218 | } 219 | 220 | #[test] 221 | fn samples_entries() { 222 | for sample in testdata::test_data() { 223 | println!("Parsing caps: {}", String::from_utf8_lossy(sample)); 224 | for cap in Value::parse_capabilities(sample).nom_iter() { 225 | println!("entry: {:?}", cap.unwrap()); 226 | } 227 | } 228 | } 229 | 230 | #[test] 231 | fn samples_caps() { 232 | for sample in testdata::test_data() { 233 | println!("Parsing caps: {}", String::from_utf8_lossy(sample)); 234 | let ent = Value::parse_capabilities(sample); 235 | for (cap, end) in Cap::parse_entries(ent.clone()).zip(ent) { 236 | println!("{}", end.unwrap()); 237 | println!("{:?}", cap.unwrap()); 238 | } 239 | } 240 | } 241 | 242 | #[test] 243 | fn samples_high() { 244 | for sample in testdata::test_data() { 245 | let caps = parse_capabilities(sample).expect("Failed to parse capabilities"); 246 | println!("Caps: {:#?}", caps); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /caps/src/testdata.rs: -------------------------------------------------------------------------------- 1 | pub fn test_data() -> Vec<&'static [u8]> { 2 | [ 3 | // monitors I have lying around 4 | &b"(prot(monitor)type(lcd)27UD58cmds(01 02 03 0C E3 F3)vcp(02 04 05 08 10 12 14(05 08 0B ) 16 18 1A 52 60( 11 12 0F 10) AC AE B2 B6 C0 C6 C8 C9 D6(01 04) DF 62 8D F4 F5(01 02) F6(00 01 02) 4D 4E 4F 15(01 06 11 13 14 28 29 32 48) F7(00 01 02 03) F8(00 01) F9 E4 E5 E6 E7 E8 E9 EA EB EF FD(00 01) FE(00 01 02) FF)mccs_ver(2.1)mswhql(1))"[..], 5 | &b"(prot(monitor)type(LCD)model(ACER)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 08 0B 10 12 14(05 08 0B) 16 18 1A 52 60(01 03 11) 6C 6E 70 AC AE B2 B6 C6 C8 C9 CC(01 02 03 04 05 06 08 09 0A 0C 0D 14 16 1E) D6(01 05) DF)mswhql(1)asset_eep(40)mccs_ver(2.0))\0"[..], 6 | &b"(prot(monitor)type(lcd)model(S2721QS)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 08 10 12 14(05 08 0B 0C) 16 18 1A 52 60( 0F 11 12) 62 AC AE B2 B6 C6 C8 C9 CC(02 03 04 06 09 0A 0D 0E) D6(01 04 05) DC(00 03 ) DF E0 E1 E2(00 1D 02 20 21 22 0E 12 14 23 24 27 )E3 E5 E8 E9(00 01 02 21 22 24) EA F0(00 0C 0F 10 11 31 32 34 35) F1 F2 FD)mccs_ver(2.1)mswhql(1))"[..], 7 | &b"(prot(monitor)type(lcd)EVEcmds(01 02 03 07 0C E3 F3)vcp(02 04 05 08 10 12 14(05 08 0B 0C) 16 18 1A 52 60( 11 12 0F 10) AA(01 02) AC AE B2 B6 C6 C8 C9 D6(01 04 05) DC(00 02 03 05 ) DF FD E0 E1 E2)mccs_ver(2.1)mswhql(1))"[..], 8 | // example from ddcutil 9 | &b"(prot(monitor)type(LED)model(25UM65)cmds(01 02 03 0C E3 F3)vcp(0203(10 00)0405080B0C101214(05 07 08 0B) 16181A5260(03 04)6C6E7087ACAEB6C0C6C8C9D6(01 04)DFE4E5E6E7E8E9EAEBED(00 10 20 40)EE(00 01)FE(01 02 03)FF)mswhql(1)mccs_ver(2.1))"[..], 10 | // example from MCCS spec v2.2a 11 | &b"Prot(display) type(lcd) model(xxxxx) cmds() vcp(02 03 10 12 C8 DC(00 01 02 03 07) DF)mccs_ver(2.2) window1(type (PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10))vcpname(10(Brightness))"[..], 12 | // example from access bus section 7 13 | &br"vcpname(14((9300 6500 5500))44(Rotate)80(Do\x20this(On Off))82(Fixit))"[..], 14 | // above example with matching vcp() section 15 | &br"vcp(14(010203)448082)vcpname(14((9300 6500 5500))44(Rotate)80(Do\x20this(On Off))82(Fixit))"[..], 16 | // tagged length with bracket and invalid utf8 seems like a worst case scenario here: 17 | &b"edid bin(3(\xff) ))vdif bin(3 (abc))unknown bin(2(ab))"[..], 18 | // samples gathered from various online sources 19 | &b"(prot(monitor)type(lcd)model(p2317h)cmds(01 02 03 07 0c e3 f3)vcp(02 04 05 08 10 12 14(05 08 0b 0c) 16 18 1a 52 60(01 0f 11) aa(01 02) ac ae b2 b6 c6 c8 c9 d6(01 04 05) dc(00 02 03 05) df e0 e1 e2(00 1d 01 02 04 0e 12 14) f0(0c) f1 f2 fd)mswhql(1)asset_eep(40)mccs_ver(2.1))"[..], 20 | &b"(prot(monitor)type(lcd)model(U3011)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 06 08 10 12 14(01 05 08 0B 0C) 16 18 1A 52 60(01 03 04 0C 0F 11 12) AC AE B2 B6 C6 C8 C9 D6(01 04 05) DC(00 02 03 04 05) DF FD)mccs_ver(2.1)mswhql(1))"[..], 21 | &b"(prot(monitor)type(lcd)model(W2413)cmds(01 02 03 07 0C 4E F3 E3)vcp(02 04 05 08 0B 0C 10 12 14(05 06 08 0B) 16 18 1A 52 AC AE B2 B6 C0 C6 C8 C9 CC(02 03 04 05 06 09 0A 0D 12 14 1E) D6(01 05) DF 60(01 03 11) 62 8D)mswhql(1)mccs_ver(2.0)asset_eep(32)mpu_ver(01))"[..], 22 | &b"(prot(monitor)type(LCD)model(Plum)mccs_ver(2.0)vcp(04 05 06 08 0E 10 12 14(02 03 0A 0B) 16 18 1A 1E 20 30 3E 60(01 03 05) 87 B0(01 02) B6 C6 C8 C9 D6(01 04) DC(01 02 03 04 05 06 F0 F2 F9 FA FB) DB(00 04 FE) DF E8(00 07 09 0A FE) E9 EA(00 01 02 03 04) EC(00 01 02 03 04 05) F0(00 01 02 03) F2 F6 F7(00 01 02 03) )mswhql(1))"[..], 23 | &b"(prot(monitor)type(LCD)model(AOC)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 06 08 0B 0C 10 12 14(01 02 04 05 08) 16 18 1A 52 60(01 03 11) 87 AC AE B2 B6 C6 C8 CA CC(01 02 03 04 06 0A 0D) D6(01 04) DF FD FF)mswhql(1)asset_eep(40)mccs_ver(2.2))"[..], 24 | // start googling "prot(monitor)" and see what you get... 25 | &b"(prot(monitor)type(lcd)model(XV273K)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 08 0B 0C 10 12 14(05 06 08 0B 0C) 16 18 1A 52 54(00 01) 59 5A 5B 5C 5D 5E 60(03 11 0F 10) 62 9B 9C 9D 9E 9F A0 AC AE B6 C0 C6 C8 C9 CC(01 02 03 04 05 06 08 09 0A 0C 0D 0E 14 16 1E)D6(01 04 05) DF E3 E5 E6 E7(00 01 02) E8(00 01 02) )mswhql(1)asset_eep(32)mccs_ver(2.1))"[..], 26 | &b"(prot(monitor)type(lcd)model(s3219d)cmds(01 02 03 07 0c e3 f3)vcp(02 04 05 08 10 12 14(05 08 0b 0c) 16 18 1a 52 60(0f 11 12 ) 62 ac ae b2 b6 c6 c8 c9 cc(02 0a 03 04 08 09 0d 06 ) d6(01 04 05) dc(00 03 05 ) df e0 e1 e2(00 1d 02 04 0e 12 14 ) f0(0c ) f1 f2 fd)mswhql(1)asset_eep(40)mccs_ver(2.1))"[..], 27 | &b"(prot(monitor)type(lcd)model(se2722hx)cmds(01 02 03 07 0c e3 f3)vcp(02 04 05 08 10 12 14(05 08 0b 0c) 16 18 1a 52 60(01 11 ) ac ae b2 b6 c6 c8 c9 ca cc(02 0a 03 04 08 09 0d 06 ) d6(01 04 05) dc(00 03 05 ) df e0 e1 e2(00 1d 02 22 20 21 0e 12 14 ) f0(0c 0f 10 11 ) f1 f2 fd)mswhql(1)asset_eep(40)mccs_ver(2.1))"[..], 28 | &b"(prot(monitor)type(lcd)model(HP X24c)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 08 0B 0C 10 12 14(02 03 04 05 08 09 0B 0C 0D ) 16 18 1A 52 60(0F 11 ) 6C 6E 70 86(01 02 05) 87(01 02 03 04 05 06 07) AC AE B2 B6 C0 C6 C8 C9 CA(01 02) CC(01 02 03 04 05 06 08 0A 0D 14) D6(01 02 03 04 05) DA(00 02 ) DC (00 (00 10 13 14 15 17 1A 1D 1E ) 01 02 03) E0(02 (00 01 02) ) E1(02 (00 01) 03 (00 01 02 03 04 05 06 07) 04 (00 01) 0F (00 01) 10 (00 01) 27 (02 03 04 05 07 ) ) DF E6(00 01) E7(00 01) E8(00 ( 01 02 03 ) 01 02 03 04 05 06 80 81 82 83 84 85 86 ) E9(00 01) EA(00 01) EB(00 (00 01 02 03 04 ) ) DE(00 (00 01 06 07 ) ) ED(00 01) EE(01 02 03) EE(01 02 03) F4(00 (00 01 02 03) 01 (00 01 02 03 04 05 06 07) 02 (00 01 02 03) 0A (00 01 05) 0B (00 01 02 03 04 05 06 07) 11 (00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F) ) FF)mswhql(1)asset_eep(40)mccs_ver(2.2))"[..], 29 | &b"(prot(monitor)type(LCD)model(RTK)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 06 08 0B 0C 10 12 14(01 02 04 05 06 08 0B) 16 18 1A 52 60(01 03 04 0F 10 11 12) 87 AC AE B2 B6 C6 C8 CA CC(01 02 03 04 06 0A 0D) D6(01 04 05) DF FD FF)mswhql(1)asset_eep(40)mccs_ver(2.2))"[..], 30 | &b"(prot(monitor)type(LCD)model(P2414H)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 08 10 12 14(05 08 0B 0C) 16 18 1A 52 60(01 03 0F) AA AC AE B2 B6 C6 C8 C9 D6(01 04 05) DC(00 02 03 05) DF E0 E1 E2(00 01 02 04 06 0E 12 14) F0(00 01) F1(01) F2 FD)mswhql(1)asset_eep(40)mccs_ver(2.1))"[..], 31 | &b"(vcp(02 04 05 08 10 12 14(01 05 06 08 0B) 16 18 1A 60(01 03)6C 6E 70 C8 9B 9D 9F 9C A0 9EB0 B6 DF)prot(monitor)type(LCD)cmds(01 02 03 07 0C F3)mccs_ver(2.1)asset_eep(64)mpu_ver(V2.00)model(SA240Y bid)mswhql(1))"[..], 32 | &b"prot(monitor)type(LCD)model(VX4380)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 08 0B 0C 10 12 14(01 08 06 05 04 0B) 16 18 1A 52 60(0F 10 11 12) 62 87 8D(01 02) A5 AC AE B2 B6 C6 C8 CA CC(01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 12 14 16 17 1A 1E 24) D6(01 04 05) DC(00 01 02 03 05 08 1F) DF E0(00 01 02 03 14) EC(01 02 03) F6 F7(42 FF) FA(00 01 02) FB FC FD FE(00 01 02 04) FF)mswhql(1)asset_eep(40)mccs_ver(2.2)"[..], 33 | &b"(prot(monitor)type(LCD)model(XB2779QS)cmds(01 03 0C F3)vcp(02 04 05 06 08 0B 0C 10 12 14(02 05 08 0B) 16 18 1A 52 60(01 03 0F 11) B6 DF)mswhql(1)mccs_ver(2.1))"[..], 34 | &b"(prot(monitor)type(lcd)34uc88cmds(01 02 03 0c e3 f3)vcp(02 04 05 08 10 12 14(05 08 0b ) 16 18 1a 52 60( 11 12 0f 10) ac ae b2 b6 c0 c6 c8 c9 d6(01 04) df 62 8d f4 f5(00 01 02 03 04) f6(00 01 02) 4d 4e 4f 15(01 07 08 09 10 11 13 14 28 29 32 48) f7(00 01 02 03) f8(00 01) f9 fd(00 01) fe(00 01 02) ff)mccs_ver(2.1)mswhql(1))"[..], 35 | &b"((prot(monitor)type(lcd)modelVG278Hcmds(01 02 03 07 0C F3)vcp(02 04 05 06 08 0B 0C 10 12 14(01 04 05 08 0B) 16 18 1A 60(01 03 04) 62 6C 6E 70 A8 AC AE B6 C6 C8 C9 D6(01 04) DF FE)mccs_ver(2.1)asset_eep(32)mpu(001)mswhql(1)))"[..], 36 | &b"(prot(monitor)type(lcd)model(xl2411t)cmds(01 02 03 07 0c e3 f3)vcp(02 04 05 08 0b 0c 10 12 14(04 05 08 0b) 16 18 1a 52 60(01 03 11) ac ae b2 b6 c0 c6 c8 c9 ca(01 02) cc(01 02 03 04 05 06 08 09 0a 0b 0d 12 14 1a 1e 1f 20) d6(01 05) df)mswhql(1)mccs_ver(2.0))"[..], 37 | &b"prot(monitor)type(lcd)model(SDM-S205)cmds(0102030CE3F3)vcp(040E10121314(0508010B)16181A1E20303E6C6E7072(0A78FA5064788CA0)B6C0C9DC(00080904)DF)mccs_ver(0201)"[..], 38 | &b"((prot(monitor)type(LCD)model(T)mccs_ver(2.0)vcp(04 05 08 10 12 14(02 03 0A 0B) 16 18 1A 60(01 03) 87 B0(01 02) B6 C6 C8 C9 D6(01 04) DC(01 02 03 04 05 06 F0 F2) DB(00 04 FE) DF E8(00 01 06 07 FE) E9 EA(00 01 02 03 04) F0(00 01 02 03) F2 F3(00 01 02) F6))mswhql(1))"[..], // NOTE: this one's a bit too messed up... 39 | &b"(prot(monitor)type(LCD)model(McKinley)mccs_ver(2.0)vcp(04 05 06 08 0E 10 12 14(02 03 0A 0B) 16 18 1A 1E 20 30 3E 87 B0(01 02) B6 C6 C8 C9 D6(01 04) DC(01 02 03 04 05 06 F0 F2) DB(00 04 FE) DF E8(00 01 06 07 FE) EA(00 01 02 03 04) F0(00 01 02 03) F2 F6 )mswhql(1))"[..], 40 | ].iter().cloned().collect() 41 | } 42 | -------------------------------------------------------------------------------- /ci.nix: -------------------------------------------------------------------------------- 1 | { config, channels, pkgs, lib, ... }: with pkgs; with lib; let 2 | inherit (import ./. { inherit pkgs; }) checks; 3 | in { 4 | name = "mccs"; 5 | ci.gh-actions.enable = true; 6 | cache.cachix = { 7 | ci.signingKey = ""; 8 | arc.enable = true; 9 | }; 10 | channels = { 11 | nixpkgs = "22.11"; 12 | }; 13 | tasks = { 14 | build.inputs = singleton checks.test; 15 | versions.inputs = singleton checks.versions; 16 | fmt.inputs = singleton checks.rustfmt; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /db/.gitignore: -------------------------------------------------------------------------------- 1 | /Cargo.lock 2 | /target/ 3 | -------------------------------------------------------------------------------- /db/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mccs-db" 3 | version = "0.2.0" # keep in sync with html_root_url 4 | authors = ["arcnmx"] 5 | edition = "2021" 6 | 7 | description = "MCCS specification VCP database" 8 | keywords = ["ddc", "mccs", "vcp", "vesa"] 9 | categories = ["hardware-support"] 10 | 11 | documentation = "https://docs.rs/mccs-db" 12 | repository = "https://github.com/arcnmx/mccs-rs" 13 | readme = "README.md" 14 | license = "MIT" 15 | 16 | include = [ 17 | "/src/**/*.rs", 18 | "/data/**", 19 | "/README*", 20 | "/COPYING*", 21 | ] 22 | 23 | [badges] 24 | maintenance = { status = "passively-maintained" } 25 | 26 | [dependencies] 27 | mccs = { version = "0.2", path = "../" } 28 | serde = { version = "1", features = ["derive"] } 29 | serde_yaml = "0.9" 30 | nom = "7" 31 | 32 | [dev-dependencies] 33 | mccs-caps = { version = "0.2", path = "../caps" } 34 | -------------------------------------------------------------------------------- /db/README.md: -------------------------------------------------------------------------------- 1 | # MCCS Database 2 | 3 | [![release-badge][]][cargo] [![docs-badge][]][docs] [![license-badge][]][license] 4 | 5 | `mccs-db` contains the human-readable descriptions of VCP features from the 6 | [MCCS](https://en.wikipedia.org/wiki/Monitor_Control_Command_Set) spec. 7 | 8 | ## [Documentation][docs] 9 | 10 | See the [documentation][docs] for up to date information. 11 | 12 | [release-badge]: https://img.shields.io/crates/v/mccs-db.svg?style=flat-square 13 | [cargo]: https://crates.io/crates/mccs-db 14 | [docs-badge]: https://img.shields.io/badge/API-docs-blue.svg?style=flat-square 15 | [docs]: http://docs.rs/mccs-db/ 16 | [license-badge]: https://img.shields.io/badge/license-MIT-ff69b4.svg?style=flat-square 17 | [license]: https://github.com/arcnmx/mccs-rs/blob/main/COPYING 18 | -------------------------------------------------------------------------------- /db/data/mccs.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - id: preset 3 | name: Preset Operations 4 | - id: display 5 | name: Display Control 6 | - id: misc 7 | name: Miscellaneous Functions 8 | vcp_features: 9 | - code: 0x00 10 | version: ">=2.2 && <3.0" 11 | group: preset 12 | name: Code Page 13 | desc: Code Page ID number. 14 | type: table 15 | interpretation: codepage 16 | mandatory: true 17 | access: rw 18 | desc_long: >- 19 | VCP Code 0x00 has been undefined and must be ignored, in all 20 | MCCS versions prior to version 2.2 including version 3.0! 21 | Starting with this revision VCP 0x00 shall be set to 0x00 until 22 | otherwise defined in a future revision: 23 | 24 | Code Pages 0x01 thru 0xDF are reserved and values in this range 25 | shall be considered invalid. 26 | 27 | Code Pages 0xE0 thru 0xFF may be used for Factory code 28 | definitions and values in this range may be supported by factory 29 | applications. 30 | 31 | On power up or display reset, the value of VCP 0x00 shall be set to 0x00. 32 | - code: 0x04 33 | version: ">=2.0" 34 | group: preset 35 | name: Restore Factory Defaults 36 | desc: >- 37 | Restore all factory presets including luminance / contrast, 38 | geometry, color and TV defaults. 39 | type: nc 40 | interpretation: nonzerowrite 41 | access: w 42 | desc_long: >- 43 | Any non-zero value causes defaults to be restored. 44 | 45 | A value of zero must be ignored 46 | - code: 0x05 47 | version: ">=2.0" 48 | group: preset 49 | name: Restore Factory Luminance / Contrast Defaults 50 | desc: >- 51 | Restores factory defaults for luminance and contrast 52 | adjustments. 53 | type: nc 54 | interpretation: nonzerowrite 55 | access: w 56 | desc_long: >- 57 | Any non-zero value causes defaults to be restored. 58 | 59 | A value of zero must be ignored. 60 | - code: 0x06 61 | version: ">=2.0" 62 | group: preset 63 | name: Restore Factory Geometry Defaults 64 | desc: >- 65 | Restore factory defaults for geometry adjustments. 66 | type: nc 67 | interpretation: nonzerowrite 68 | access: w 69 | desc_long: >- 70 | Any non-zero value causes defaults to be restored. 71 | 72 | A value of zero must be ignored. 73 | - code: 0x08 74 | version: ">=2.0" 75 | group: preset 76 | name: Restore Factory Color Defaults 77 | desc: >- 78 | Restore factory defaults for color settings. 79 | type: nc 80 | interpretation: nonzerowrite 81 | access: w 82 | desc_long: >- 83 | Any non-zero value causes defaults to be restored. 84 | 85 | A value of zero must be ignored. 86 | - code: 0x0a 87 | version: ">=2.0" 88 | group: preset 89 | name: Restore Factory TV Defaults 90 | desc: >- 91 | Restore factory defaults for TV functions. 92 | type: nc 93 | interpretation: nonzerowrite 94 | access: w 95 | desc_long: >- 96 | Any non-zero value causes defaults to be restored. 97 | 98 | A value of zero must be ignored. 99 | - code: 0xb0 100 | version: ">=2.0" 101 | group: preset 102 | name: Settings 103 | desc: >- 104 | Store/Restore the user saved values for current mode. 105 | type: nc 106 | interpretation: 107 | - value: 0x01 108 | name: Store 109 | desc: Store current settings in the monitor. 110 | - value: 0x02 111 | name: Restore 112 | desc: Restore factory defaults for current mode. 113 | desc_long: >- 114 | If no factory defaults exist, then restore 115 | user values for current mode. 116 | - value: ">=0x03" 117 | name: Reserved 118 | desc: Reserved and must be ignored. 119 | access: w 120 | - code: 0xdf 121 | version: ">=2.0" 122 | group: display 123 | name: VCP Version 124 | desc: Defines the version number of the MCCS standard recognized by the display. 125 | type: nc 126 | interpretation: vcpversion 127 | access: r 128 | mandatory: true 129 | desc_long: >- 130 | SH byte: defines the MCCS version number 131 | 132 | SL byte: defines the MCCS revision number 133 | 134 | e.g. 0x02 0x02 defines a MCCS level of 2.2 135 | - code: 0x60 136 | version: ">=2.0 && <3.0" 137 | group: misc 138 | name: Input Select 139 | desc: Allows the host to set one and only one input as "the source" and identify the current input setting. 140 | type: nc 141 | interpretation: 142 | - value: 0x01 143 | name: Analog 1 144 | desc: Analog video (R/G/B) 1 145 | - value: 0x02 146 | name: Analog 2 147 | desc: Analog video (R/G/B) 2 148 | - value: 0x03 149 | name: DVI 1 150 | desc: Digital video (TMDS) 1 151 | - value: 0x04 152 | name: DVI 2 153 | desc: Digital video (TMDS) 2 154 | - value: 0x05 155 | name: Composite 1 156 | desc: Composite video 1 157 | - value: 0x06 158 | name: Composite 2 159 | desc: Composite video 2 160 | - value: 0x07 161 | name: S-video 1 162 | - value: 0x08 163 | name: S-video 2 164 | - value: 0x09 165 | name: Tuner 1 166 | - value: 0x0A 167 | name: Tuner 2 168 | - value: 0x0B 169 | name: Tuner 3 170 | - value: 0x0C 171 | name: Component 1 172 | desc: Component video (YPbPr / YCbCr) 1 173 | - value: 0x0D 174 | name: Component 2 175 | desc: Component video (YPbPr / YCbCr) 2 176 | - value: 0x0E 177 | name: Component 3 178 | desc: Component video (YPbPr / YCbCr) 3 179 | - value: 0x0F 180 | name: DisplayPort 1 181 | - value: 0x10 182 | name: DisplayPort 2 183 | - value: 0x11 184 | name: HDMI 1 185 | desc: Digital Video (TMDS) 3 186 | - value: 0x12 187 | name: HDMI 2 188 | desc: Digital Video (TMDS) 4 189 | - value: 0x19 190 | name: USB-C 1 191 | desc: USB-C / Thunderbolt 1 192 | - value: 0x1B 193 | name: USB-C 2 194 | desc: USB-C / Thunderbolt 2 195 | - value: "(>=0x13 && <=0x18) || =0x1A || >=0x1C" 196 | name: Reserved 197 | desc: Reserved and are un-assigned 198 | access: rw 199 | - code: 0x60 200 | version: ">=3.0" 201 | group: misc 202 | name: Input Select 203 | type: table 204 | access: rw 205 | -------------------------------------------------------------------------------- /db/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![doc(html_root_url = "https://docs.rs/mccs-db/0.2.0")] 3 | 4 | //! Monitor Command Control Set VCP feature code meanings and data 5 | //! interpretation. 6 | //! 7 | //! # Example 8 | //! 9 | //! ``` 10 | //! use mccs_db::Database; 11 | //! 12 | //! # fn read_display_capability_string() -> &'static str { 13 | //! # "(prot(monitor)type(lcd)27UD58cmds(01 02 03 0C E3 F3)vcp(02 04 05 08 10 12 14(05 08 0B ) 16 18 1A 52 60( 11 12 0F 10) AC AE B2 B6 C0 C6 C8 C9 D6(01 04) DF 62 8D F4 F5(01 02) F6(00 01 02) 4D 4E 4F 15(01 06 11 13 14 28 29 32 48) F7(00 01 02 03) F8(00 01) F9 E4 E5 E6 E7 E8 E9 EA EB EF FD(00 01) FE(00 01 02) FF)mccs_ver(2.1)mswhql(1))" 14 | //! # } 15 | //! # fn main() { 16 | //! // Read the capabilities from an external source, such as a monitor over DDC. 17 | //! let caps = mccs_caps::parse_capabilities(read_display_capability_string()).unwrap(); 18 | //! 19 | //! // Load the MCCS version spec and filter by the monitor's capabilities 20 | //! let mut db = Database::from_version(caps.mccs_version.as_ref().unwrap()); 21 | //! db.apply_capabilities(&caps); 22 | //! 23 | //! println!("Display Capabilities: {:#?}", db); 24 | //! # } 25 | //! ``` 26 | 27 | use { 28 | mccs::{Capabilities, FeatureCode, Value, ValueNames, Version}, 29 | serde::{Deserialize, Serialize}, 30 | std::{collections::BTreeMap, io, mem}, 31 | }; 32 | 33 | #[cfg(test)] 34 | #[path = "../../caps/src/testdata.rs"] 35 | mod testdata; 36 | 37 | #[rustfmt::skip::macros(named)] 38 | mod version_req; 39 | 40 | /// Describes how to interpret a table's raw value. 41 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 42 | pub enum TableInterpretation { 43 | /// Generic unparsed data. 44 | Generic, 45 | /// First byte is the code page where `0x00` is the default. 46 | /// 47 | /// The range of `0xe0` to `0xff` is defined for factory use. All other 48 | /// values are reserved. The size of the table is unclear from the spec, 49 | /// maybe 4 or maybe 1? 50 | CodePage, 51 | } 52 | 53 | impl Default for TableInterpretation { 54 | fn default() -> Self { 55 | TableInterpretation::Generic 56 | } 57 | } 58 | 59 | impl TableInterpretation { 60 | /// Formats a table for user display. 61 | /// 62 | /// This can fail if the data is not in the expected format or has an 63 | /// invalid length. 64 | pub fn format(&self, table: &[u8]) -> Result { 65 | Ok(match *self { 66 | TableInterpretation::Generic => format!("{:?}", table), 67 | TableInterpretation::CodePage => 68 | if let Some(v) = table.get(0) { 69 | format!("{v}") 70 | } else { 71 | return Err(()) 72 | }, 73 | }) 74 | } 75 | } 76 | 77 | /// Describes how to interpret a value's raw value. 78 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 79 | pub enum ValueInterpretation { 80 | /// Generic unparsed data. 81 | Continuous, 82 | /// Generic unparsed data. 83 | NonContinuous, 84 | /// Must be set to a non-zero value in order to run the operation. 85 | NonZeroWrite, 86 | /// MCCS version is returned in `mh` (major version) and `ml` (minor/revision). 87 | VcpVersion, 88 | } 89 | 90 | impl ValueInterpretation { 91 | /// Formats a value for user display. 92 | pub fn format(&self, value: &Value) -> String { 93 | match *self { 94 | ValueInterpretation::Continuous => format!("{} / {}", value.value(), value.maximum()), 95 | ValueInterpretation::NonContinuous => { 96 | let v16 = match value.value() { 97 | // Some displays set the high byte to a duplicate of of low byte, 98 | // so assume this is a u8 value if it is out of the expected range 99 | v16 if v16 > value.maximum() => v16 & 0x00ff, 100 | v16 => v16, 101 | }; 102 | format!("{v16}") 103 | }, 104 | ValueInterpretation::NonZeroWrite => if value.sl == 0 { "unset" } else { "set" }.into(), 105 | ValueInterpretation::VcpVersion => format!("{}", Version::new(value.sh, value.sl)), 106 | } 107 | } 108 | } 109 | 110 | /// Describes the type of a VCP value and how to interpret it. 111 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 112 | pub enum ValueType { 113 | /// The type of the data is not known 114 | Unknown, 115 | /// The data is a continuous value. 116 | Continuous { 117 | /// Describes how to interpret the continuous value. 118 | interpretation: ValueInterpretation, 119 | }, 120 | /// The data is a non-continuous value. 121 | NonContinuous { 122 | /// The values allowed or supported to be set, as well as their 123 | /// user-facing names. 124 | values: ValueNames, 125 | /// Describes how to interpret the non-continuous value. 126 | interpretation: ValueInterpretation, 127 | }, 128 | /// The data is a table (byte array) 129 | Table { 130 | /// Describes how to interpret the table. 131 | interpretation: TableInterpretation, 132 | }, 133 | } 134 | 135 | impl Default for ValueType { 136 | fn default() -> Self { 137 | ValueType::Unknown 138 | } 139 | } 140 | 141 | /// The operations allowed on a given VCP feature code. 142 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 143 | pub enum Access { 144 | // TODO: bitflags? 145 | /// The value can only be read from. 146 | ReadOnly, 147 | /// The value can only be written to. 148 | WriteOnly, 149 | /// The value is both readwritable. 150 | ReadWrite, 151 | } 152 | 153 | impl Default for Access { 154 | fn default() -> Self { 155 | Access::ReadWrite 156 | } 157 | } 158 | 159 | /// Describes a VCP feature code's functionality and value format. 160 | #[derive(Debug, Default, Clone)] 161 | pub struct Descriptor { 162 | /// The name of the feature. 163 | pub name: Option, 164 | /// A detailed description of the feature. 165 | pub description: Option, 166 | /// The MCCS grouping this feature belongs to. 167 | pub group: Option, 168 | /// The VCP code of the feature. 169 | pub code: FeatureCode, 170 | /// The data type of the feature. 171 | pub ty: ValueType, 172 | /// Whether the feature can be set, read, or both. 173 | pub access: Access, 174 | /// Whether the feature is required to be supported by the display for MCCS 175 | /// specification compliance. 176 | pub mandatory: bool, 177 | /// Any other feature codes that this "interacts" with. 178 | /// 179 | /// Changing this feature's value may also affect the value of these other 180 | /// listed features. 181 | pub interacts_with: Vec, 182 | } 183 | 184 | /// Describes all the VCP feature codes supported by an MCCS specification or 185 | /// display. 186 | #[derive(Debug, Clone, Default)] 187 | pub struct Database { 188 | entries: BTreeMap, 189 | } 190 | 191 | impl Database { 192 | fn apply_database(&mut self, db: DatabaseFile, mccs_version: &Version) -> io::Result<()> { 193 | for code in db.vcp_features { 194 | if !code.version.matches(mccs_version) { 195 | continue 196 | } 197 | 198 | let entry = self.entries.entry(code.code).or_insert_with(|| Descriptor::default()); 199 | 200 | entry.code = code.code; 201 | if let Some(name) = code.name { 202 | entry.name = Some(name); 203 | } 204 | if let Some(desc) = code.desc { 205 | entry.description = Some(desc); 206 | } 207 | if let Some(group) = code.group { 208 | entry.group = db.groups.iter().find(|g| g.id == group).map(|g| g.name.clone()); 209 | } 210 | if let Some(ty) = code.ty { 211 | entry.ty = match (ty, code.interpretation) { 212 | (DatabaseType::Table, None) => ValueType::Table { 213 | interpretation: TableInterpretation::Generic, 214 | }, 215 | (DatabaseType::Table, Some(DatabaseInterpretation::Values(..))) => 216 | return Err(io::Error::new( 217 | io::ErrorKind::InvalidData, 218 | "table type cannot have value names", 219 | )), 220 | (DatabaseType::Table, Some(DatabaseInterpretation::Id(id))) => ValueType::Table { 221 | interpretation: match id { 222 | DatabaseInterpretationId::CodePage => TableInterpretation::CodePage, 223 | id => 224 | return Err(io::Error::new( 225 | io::ErrorKind::InvalidData, 226 | format!("invalid interpretation {:?} for table", id), 227 | )), 228 | }, 229 | }, 230 | (DatabaseType::Continuous, ..) => ValueType::Continuous { 231 | interpretation: ValueInterpretation::Continuous, 232 | }, 233 | (DatabaseType::NonContinuous, None) => ValueType::NonContinuous { 234 | values: Default::default(), 235 | interpretation: ValueInterpretation::NonContinuous, 236 | }, 237 | (DatabaseType::NonContinuous, Some(DatabaseInterpretation::Values(values))) => 238 | ValueType::NonContinuous { 239 | values: values 240 | .into_iter() 241 | .flat_map(|v| { 242 | let mut name = Some(v.name); 243 | let dbv = v.value; 244 | let (opti, range) = match dbv { 245 | DatabaseValue::Value(value) => (Some((value, name.take())), None), 246 | DatabaseValue::Range(version_req::Req::Eq(value)) => 247 | (Some((value, name.take())), None), 248 | DatabaseValue::Range(range) => (None, Some(range)), 249 | }; 250 | opti.into_iter().chain( 251 | range 252 | .map(move |range| { 253 | (0..=0xff) 254 | .filter(move |value| range.matches(value)) 255 | .map(move |value| (value, name.clone())) 256 | }) 257 | .into_iter() 258 | .flat_map(|i| i), 259 | ) 260 | }) 261 | .collect(), 262 | interpretation: ValueInterpretation::NonContinuous, 263 | }, 264 | (DatabaseType::NonContinuous, Some(DatabaseInterpretation::Id(id))) => ValueType::NonContinuous { 265 | values: Default::default(), 266 | interpretation: match id { 267 | DatabaseInterpretationId::NonZeroWrite => ValueInterpretation::NonZeroWrite, 268 | DatabaseInterpretationId::VcpVersion => ValueInterpretation::VcpVersion, 269 | id => 270 | return Err(io::Error::new( 271 | io::ErrorKind::InvalidData, 272 | format!("invalid interpretation {:?} for nc", id), 273 | )), 274 | }, 275 | }, 276 | } 277 | } 278 | entry.mandatory |= code.mandatory; 279 | if let Some(access) = code.access { 280 | entry.access = match access { 281 | DatabaseReadWrite::ReadOnly => Access::ReadOnly, 282 | DatabaseReadWrite::WriteOnly => Access::WriteOnly, 283 | DatabaseReadWrite::ReadWrite => Access::ReadWrite, 284 | }; 285 | } 286 | entry.interacts_with.extend(code.interacts_with); 287 | } 288 | 289 | Ok(()) 290 | } 291 | 292 | fn mccs_database() -> DatabaseFile { 293 | let data = include_bytes!("../data/mccs.yml"); 294 | serde_yaml::from_slice(data).unwrap() 295 | } 296 | 297 | /// Create a new database from a specified MCCS specification version. 298 | pub fn from_version(mccs_version: &Version) -> Self { 299 | let mut s = Self::default(); 300 | s.apply_database(Self::mccs_database(), mccs_version).unwrap(); 301 | s 302 | } 303 | 304 | /// Create a new database from a specified database description YAML file. 305 | /// 306 | /// This format is not (yet) documented, but an example exists that 307 | /// [describes the MCCS spec](https://github.com/arcnmx/mccs-rs/blob/master/db/data/mccs.yml). 308 | pub fn from_database(database_yaml: R, mccs_version: &Version) -> io::Result { 309 | let db = serde_yaml::from_reader(database_yaml).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; 310 | 311 | let mut s = Self::default(); 312 | s.apply_database(db, mccs_version)?; 313 | Ok(s) 314 | } 315 | 316 | /// Filter out any feature codes or values that are not supported by the 317 | /// specified display. 318 | pub fn apply_capabilities(&mut self, caps: &Capabilities) { 319 | let mut entries = mem::replace(&mut self.entries, Default::default()); 320 | self.entries.extend( 321 | caps.vcp_features 322 | .iter() 323 | .map(|(code, desc)| match (entries.remove(code), *code, desc) { 324 | (Some(mut mccs), code, cap) => { 325 | if let Some(ref name) = cap.name { 326 | mccs.name = Some(name.clone()); 327 | } 328 | 329 | if let ValueType::NonContinuous { ref mut values, .. } = mccs.ty { 330 | let mut full = mem::replace(values, Default::default()); 331 | values.extend(cap.values.iter().map(|(&value, caps_name)| match full.remove(&value) { 332 | Some(name) => (value, caps_name.clone().or(name)), 333 | None => (value, caps_name.clone()), 334 | })); 335 | } 336 | 337 | (code, mccs) 338 | }, 339 | (None, code, cap) => { 340 | let desc = Descriptor { 341 | name: cap.name.clone(), 342 | description: None, 343 | group: None, 344 | code, 345 | ty: if cap.values.is_empty() { 346 | ValueType::Continuous { 347 | interpretation: ValueInterpretation::Continuous, 348 | } 349 | } else { 350 | ValueType::NonContinuous { 351 | interpretation: ValueInterpretation::NonContinuous, 352 | values: cap.values.clone(), 353 | } 354 | }, 355 | access: Access::ReadWrite, 356 | mandatory: false, 357 | interacts_with: Vec::new(), 358 | }; 359 | 360 | (code, desc) 361 | }, 362 | }), 363 | ); 364 | } 365 | 366 | /// Get the description of a given VCP feature code. 367 | pub fn get(&self, code: FeatureCode) -> Option<&Descriptor> { 368 | self.entries.get(&code) 369 | } 370 | } 371 | 372 | #[derive(Debug, Serialize, Deserialize)] 373 | #[serde(rename_all = "snake_case", deny_unknown_fields)] 374 | struct DatabaseGroup { 375 | id: String, 376 | name: String, 377 | } 378 | 379 | #[derive(Debug, Serialize, Deserialize)] 380 | #[serde(rename_all = "snake_case")] 381 | enum DatabaseType { 382 | Table, 383 | #[serde(rename = "nc")] 384 | NonContinuous, 385 | #[serde(rename = "c")] 386 | Continuous, 387 | } 388 | 389 | #[derive(Debug, Serialize, Deserialize)] 390 | enum DatabaseReadWrite { 391 | #[serde(rename = "r")] 392 | ReadOnly, 393 | #[serde(rename = "w")] 394 | WriteOnly, 395 | #[serde(rename = "rw")] 396 | ReadWrite, 397 | } 398 | 399 | #[derive(Debug, Serialize, Deserialize)] 400 | #[serde(untagged)] 401 | enum DatabaseValue { 402 | Value(u8), 403 | Range(version_req::Req), 404 | } 405 | 406 | #[derive(Debug, Serialize, Deserialize)] 407 | #[serde(rename_all = "snake_case", deny_unknown_fields)] 408 | struct DatabaseValueDesc { 409 | value: DatabaseValue, 410 | name: String, 411 | #[serde(default, skip_serializing_if = "Option::is_none")] 412 | desc: Option, 413 | #[serde(default, skip_serializing_if = "Option::is_none")] 414 | desc_long: Option, 415 | } 416 | #[derive(Debug, Deserialize)] 417 | #[serde(rename_all = "lowercase")] 418 | enum DatabaseInterpretationId { 419 | CodePage, 420 | NonZeroWrite, 421 | VcpVersion, 422 | } 423 | 424 | #[derive(Debug, Deserialize)] 425 | #[serde(untagged)] 426 | enum DatabaseInterpretation { 427 | Id(DatabaseInterpretationId), 428 | Values(Vec), 429 | } 430 | 431 | #[derive(Debug, Deserialize)] 432 | #[serde(rename_all = "snake_case", deny_unknown_fields)] 433 | struct DatabaseFeature { 434 | code: FeatureCode, 435 | #[serde(default)] 436 | version: version_req::VersionReq, 437 | name: Option, 438 | #[serde(default, skip_serializing_if = "Option::is_none")] 439 | desc: Option, 440 | #[serde(default, skip_serializing_if = "Option::is_none")] 441 | group: Option, 442 | #[serde(rename = "type")] 443 | ty: Option, 444 | #[serde(default)] 445 | interpretation: Option, 446 | #[serde(default)] 447 | mandatory: bool, 448 | access: Option, 449 | #[allow(dead_code)] 450 | #[serde(default, skip_serializing_if = "Option::is_none")] 451 | desc_long: Option, 452 | #[serde(default, rename = "interacts")] 453 | interacts_with: Vec, 454 | } 455 | 456 | #[derive(Debug, Deserialize)] 457 | #[serde(rename_all = "snake_case", deny_unknown_fields)] 458 | struct DatabaseFile { 459 | groups: Vec, 460 | vcp_features: Vec, 461 | } 462 | 463 | #[test] 464 | fn load_database() { 465 | for version in &[ 466 | Version::new(2, 0), 467 | Version::new(2, 1), 468 | Version::new(2, 2), 469 | Version::new(3, 0), 470 | ] { 471 | let db = Database::from_version(version); 472 | for sample in testdata::test_data() { 473 | let caps = mccs_caps::parse_capabilities(sample).expect("Failed to parse capabilities"); 474 | let mut db = db.clone(); 475 | db.apply_capabilities(&caps); 476 | println!("Intersected: {:#?}", db); 477 | } 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /db/src/version_req.rs: -------------------------------------------------------------------------------- 1 | use { 2 | mccs::Version, 3 | nom::{ 4 | branch::alt, 5 | bytes::complete::{tag, take_while_m_n}, 6 | character::complete::{char, space0, u8}, 7 | combinator::{map, map_res, opt}, 8 | sequence::{delimited, preceded, separated_pair}, 9 | Finish, IResult, 10 | }, 11 | serde::{ 12 | de::{Deserialize, Deserializer, Error, Unexpected}, 13 | ser::{Serialize, Serializer}, 14 | }, 15 | std::{cmp, fmt}, 16 | }; 17 | 18 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 19 | pub(crate) enum Req { 20 | Bracket(Box>), 21 | And(Box>, Box>), 22 | Or(Box>, Box>), 23 | Eq(V), 24 | Ge(V), 25 | Le(V), 26 | Gt(V), 27 | Lt(V), 28 | } 29 | 30 | impl fmt::Display for Req { 31 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 32 | match self { 33 | Req::Bracket(r) => write!(f, "({r})"), 34 | Req::And(lhs, rhs) => write!(f, "{lhs} && {rhs}"), 35 | Req::Or(lhs, rhs) => write!(f, "{lhs} || {rhs}"), 36 | Req::Eq(v) => write!(f, "={v}"), 37 | Req::Gt(v) => write!(f, ">{v}"), 38 | Req::Ge(v) => write!(f, ">={v}"), 39 | Req::Lt(v) => write!(f, "<{v}"), 40 | Req::Le(v) => write!(f, "<={v}"), 41 | } 42 | } 43 | } 44 | 45 | pub(crate) type VersionReq = Req; 46 | 47 | trait ReqValue: Sized + fmt::Debug + fmt::Display { 48 | type Err: fmt::Debug; 49 | const EXPECTED: &'static str; 50 | 51 | fn parse_req(req: &str) -> Result, Self::Err>; 52 | fn parse_nom(i: &str) -> IResult<&str, Self>; 53 | } 54 | 55 | impl ReqValue for Version { 56 | type Err = nom::error::ErrorKind; 57 | 58 | const EXPECTED: &'static str = "version requirement"; 59 | 60 | fn parse_req(req: &str) -> Result, Self::Err> { 61 | parse_req_expr(req).finish().map(|v| v.1).map_err(|e| e.code) 62 | } 63 | 64 | fn parse_nom(i: &str) -> IResult<&str, Self> { 65 | parse_version(i) 66 | } 67 | } 68 | 69 | impl ReqValue for u8 { 70 | type Err = nom::error::ErrorKind; 71 | 72 | const EXPECTED: &'static str = "version requirement"; 73 | 74 | fn parse_req(req: &str) -> Result, Self::Err> { 75 | parse_req_expr(req).finish().map(|v| v.1).map_err(|e| e.code) 76 | } 77 | 78 | fn parse_nom(i: &str) -> IResult<&str, Self> { 79 | parse_u8(i) 80 | } 81 | } 82 | 83 | impl<'a, V: ReqValue> Deserialize<'a> for Req { 84 | fn deserialize>(d: D) -> Result { 85 | String::deserialize(d).and_then(|v| { 86 | V::parse_req(&v).map_err(|e| D::Error::invalid_value(Unexpected::Other(&format!("{:?}", e)), &V::EXPECTED)) 87 | }) 88 | } 89 | } 90 | 91 | impl Serialize for Req { 92 | fn serialize(&self, s: S) -> Result { 93 | self.to_string().serialize(s) 94 | } 95 | } 96 | 97 | impl Req { 98 | #[cfg(test)] 99 | fn and>>(self, rhs: R) -> Self { 100 | Req::And(self.into(), rhs.into()) 101 | } 102 | 103 | #[cfg(test)] 104 | fn or>>(self, rhs: R) -> Self { 105 | Req::Or(self.into(), rhs.into()) 106 | } 107 | } 108 | 109 | impl Req 110 | where 111 | V: cmp::PartialEq + cmp::PartialOrd, 112 | { 113 | pub fn matches(&self, v: &V) -> bool { 114 | match *self { 115 | Req::Bracket(ref r) => r.matches(v), 116 | Req::And(ref lhs, ref rhs) => lhs.matches(v) && rhs.matches(v), 117 | Req::Or(ref lhs, ref rhs) => lhs.matches(v) || rhs.matches(v), 118 | Req::Eq(ref req) => v == req, 119 | Req::Ge(ref req) => v >= req, 120 | Req::Le(ref req) => v <= req, 121 | Req::Lt(ref req) => v < req, 122 | Req::Gt(ref req) => v > req, 123 | } 124 | } 125 | } 126 | 127 | impl Default for Req { 128 | fn default() -> Self { 129 | Req::Ge(Version::new(0, 0)) 130 | } 131 | } 132 | 133 | fn parse_req_expr(i: &str) -> IResult<&str, Req> { 134 | let (i, lhs) = match opt(delimited(char('('), parse_req_expr, char(')')))(i)? { 135 | (i, Some(req)) => (i, Req::Bracket(Box::new(req))), 136 | (_, None) => parse_req(i)?, 137 | }; 138 | 139 | let (i, _) = space0(i)?; 140 | let tags = alt((tag("&&"), tag("||"))); 141 | match opt(delimited(space0, tags, space0))(i)? { 142 | (i, Some(op)) => { 143 | let (i, rhs) = parse_req_expr(i)?; 144 | Ok((i, match op { 145 | "&&" => Req::And(Box::new(lhs), Box::new(rhs)), 146 | "||" => Req::Or(Box::new(lhs), Box::new(rhs)), 147 | op => unreachable!("{lhs:?} {op} {rhs:?}"), 148 | })) 149 | }, 150 | (i, None) => Ok((i, lhs)), 151 | } 152 | } 153 | 154 | fn parse_req(i: &str) -> IResult<&str, Req> { 155 | let (i, _) = space0(i)?; 156 | let tags = alt((tag("<="), tag("<"), tag(">="), tag(">"), tag("="))); 157 | let op: Option<(_, fn(V) -> Req<_>)> = match opt(tags)(i)? { 158 | (i, Some(op)) => Some((i, match op { 159 | "<=" => Req::Le, 160 | "<" => Req::Lt, 161 | ">=" => Req::Ge, 162 | ">" => Req::Gt, 163 | "=" => Req::Eq, 164 | op => unreachable!("unknown op {op}"), 165 | })), 166 | (_, None) => None, 167 | }; 168 | match op { 169 | Some((i, op)) => map(V::parse_nom, op)(i), 170 | None => map(V::parse_nom, Req::Eq)(i), 171 | } 172 | } 173 | 174 | fn hex_u8(i: &str) -> IResult<&str, u8> { 175 | map_res(take_while_m_n(1, 2, |c: char| c.is_digit(16)), |i| { 176 | u8::from_str_radix(i, 16) 177 | })(i) 178 | } 179 | 180 | fn parse_u8(i: &str) -> IResult<&str, u8> { 181 | trim_spaces(alt((preceded(tag("0x"), hex_u8), u8)))(i) 182 | } 183 | 184 | fn parse_version(i: &str) -> IResult<&str, Version> { 185 | trim_spaces(map(separated_pair(u8, trim_spaces(char('.')), u8), |(major, minor)| { 186 | Version { major, minor } 187 | }))(i) 188 | } 189 | 190 | fn trim_spaces(parser: P) -> impl FnMut(I) -> nom::IResult 191 | where 192 | P: nom::Parser, 193 | E: nom::error::ParseError, 194 | I: Clone + nom::InputTakeAtPosition, 195 | ::Item: nom::AsChar + Clone, 196 | { 197 | delimited(space0, parser, space0) 198 | } 199 | 200 | #[test] 201 | fn version() { 202 | assert_eq!(parse_version("22.01").finish().unwrap().1, Version { 203 | major: 22, 204 | minor: 1 205 | }) 206 | } 207 | 208 | #[test] 209 | fn version_eq() { 210 | assert_eq!( 211 | parse_req_expr("2.0").finish().unwrap().1, 212 | VersionReq::Eq(Version { major: 2, minor: 0 }) 213 | ) 214 | } 215 | 216 | #[test] 217 | fn version_req() { 218 | assert_eq!( 219 | parse_req_expr("<=3.1").finish().unwrap().1, 220 | VersionReq::Le(Version { major: 3, minor: 1 }) 221 | ) 222 | } 223 | 224 | #[test] 225 | fn u8_req() { 226 | assert_eq!(parse_req_expr("<=0x0a").finish().unwrap().1, Req::Le(0xa)) 227 | } 228 | 229 | #[test] 230 | fn u8_expr() { 231 | assert_eq!( 232 | parse_req_expr("0x0a && <0x05").finish().unwrap().1, 233 | Req::Eq(0xa).and(Req::Lt(5)) 234 | ) 235 | } 236 | 237 | #[test] 238 | fn u8_chained_expr() { 239 | assert_eq!( 240 | parse_req_expr("(0x0a && <0x05) || (0x01 && 0x02)").finish().unwrap().1, 241 | Req::Bracket(Req::Eq(0xa).and(Req::Lt(5)).into()).or(Req::Bracket(Req::Eq(1).and(Req::Eq(2)).into())) 242 | ) 243 | } 244 | 245 | #[test] 246 | fn u8_ordered_chain() { 247 | assert_eq!( 248 | parse_req_expr("0x0a || 0x01 || 0x02").finish().unwrap().1, 249 | Req::Eq(0xa).or(Req::Eq(1).or(Req::Eq(2))) 250 | ) 251 | } 252 | 253 | #[test] 254 | fn u8_nested_expr() { 255 | assert_eq!( 256 | parse_req_expr("((0x0a && (0x01 && 0x02)) || <0x03)") 257 | .finish() 258 | .unwrap() 259 | .1, 260 | Req::Bracket( 261 | Req::Bracket(Req::Eq(0xa).and(Req::Bracket(Req::Eq(1).and(Req::Eq(2)).into())).into()) 262 | .or(Req::Lt(3)) 263 | .into() 264 | ) 265 | ) 266 | } 267 | 268 | #[test] 269 | fn u8_input_select_expr() { 270 | assert_eq!( 271 | parse_req_expr("(>=0x13 && <=0x18) || =0x1A || >=0x1C") 272 | .finish() 273 | .unwrap() 274 | .1, 275 | Req::Bracket(Req::Ge(0x13).and(Req::Le(0x18)).into()).or(Req::Eq(0x1a).or(Req::Ge(0x1c))) 276 | ) 277 | } 278 | -------------------------------------------------------------------------------- /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": 1671242652, 26 | "narHash": "sha256-pZYiNhTzCn0/Caw8aQJekJPxMfPf44PSwme+1yJm+5A=", 27 | "owner": "flakelib", 28 | "repo": "fl", 29 | "rev": "96ca3638fb39a0ae880ef442479f26bd82bebe20", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "flakelib", 34 | "repo": "fl", 35 | "type": "github" 36 | } 37 | }, 38 | "nix-std": { 39 | "locked": { 40 | "lastModified": 1671170529, 41 | "narHash": "sha256-015C6x3tZMEd83Vd2rpfLC86PSRJrbUca1U3Rysranw=", 42 | "owner": "chessai", 43 | "repo": "nix-std", 44 | "rev": "3b307d64ef7d7e8769d36b8c8bf33983efd1415a", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "chessai", 49 | "repo": "nix-std", 50 | "type": "github" 51 | } 52 | }, 53 | "nixpkgs": { 54 | "locked": { 55 | "lastModified": 1676373897, 56 | "narHash": "sha256-6NYOSedYdrZvnkjJceGnYOgaP1odl436X8bP1Ougm6c=", 57 | "owner": "NixOS", 58 | "repo": "nixpkgs", 59 | "rev": "89fc13df40f857ebe4033f1670cb7c21ace488e3", 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": 1676480941, 82 | "narHash": "sha256-NXWE1lFmz8GLjqXXk0nUbosTvIEscYzd0Zcex5aQo2A=", 83 | "owner": "arcnmx", 84 | "repo": "nixexprs-rust", 85 | "rev": "1b605223ef162badaf6f40d1cea1addfda6550de", 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": 1671225084, 100 | "narHash": "sha256-EzqxFHRifPyjUXQY4B8GJH75fTmWqFnQdj10Q984bR8=", 101 | "owner": "flakelib", 102 | "repo": "std", 103 | "rev": "8546115941a5498ddb03a239aacdba151d433f09", 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 = "VESA Monitor Control Command Set"; 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 19 | , enableRust ? true, cargo 20 | , rustTools ? [ ] 21 | , generate 22 | }: mkShell { 23 | inherit rustTools; 24 | nativeBuildInputs = nixlib.optional enableRust cargo ++ [ 25 | generate 26 | ]; 27 | }; 28 | stable = { rust'stable, outputs'devShells'plain }: outputs'devShells'plain.override { 29 | inherit (rust'stable) mkShell; 30 | enableRust = false; 31 | }; 32 | dev = { rust'unstable, outputs'devShells'plain }: outputs'devShells'plain.override { 33 | inherit (rust'unstable) mkShell; 34 | enableRust = false; 35 | rustTools = [ "rust-analyzer" ]; 36 | }; 37 | default = { outputs'devShells }: outputs'devShells.plain; 38 | }; 39 | checks = { 40 | rustfmt = { rust'builders, source }: rust'builders.check-rustfmt-unstable { 41 | src = source; 42 | config = ./.rustfmt.toml; 43 | }; 44 | versions = { rust'builders, source }: rust'builders.check-contents { 45 | src = source; 46 | patterns = [ 47 | { path = "src/lib.rs"; docs'rs = { 48 | inherit (self.lib.crate) name version; 49 | }; } 50 | { path = "caps/src/lib.rs"; docs'rs = { 51 | inherit (self.lib.crate.members.caps) name version; 52 | }; } 53 | { path = "db/src/lib.rs"; docs'rs = { 54 | inherit (self.lib.crate.members.db) name version; 55 | }; } 56 | ]; 57 | }; 58 | test = { rustPlatform, source }: rustPlatform.buildRustPackage rec { 59 | pname = self.lib.crate.package.name; 60 | inherit (self.lib.crate) cargoLock version; 61 | src = source; 62 | cargoBuildFlags = [ "--workspace" ]; 63 | cargoTestFlags = cargoBuildFlags; 64 | buildType = "debug"; 65 | meta.name = "cargo test"; 66 | }; 67 | }; 68 | legacyPackages = { callPackageSet }: callPackageSet { 69 | source = { rust'builders }: rust'builders.wrapSource self.lib.crate.src; 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 | lib = with nixlib; { 81 | crate = rust.lib.importCargo { 82 | inherit self; 83 | path = ./Cargo.toml; 84 | inherit (import ./lock.nix) outputHashes; 85 | }; 86 | inherit (self.lib.crate.package) version; 87 | }; 88 | config = rec { 89 | name = "mccs-rs"; 90 | packages.namespace = [ name ]; 91 | }; 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /lock.nix: -------------------------------------------------------------------------------- 1 | { 2 | outputHashes = { }; 3 | } 4 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | self = import ./.; 3 | in self.devShells.default or { } // { 4 | __functor = _: { pkgs ? null, ... }@args: (self args).devShells.default; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![doc(html_root_url = "https://docs.rs/mccs/0.2.0")] 3 | 4 | //! VESA Monitor Command Control Set standardizes the meaning of DDC/CI VCP 5 | //! feature codes, and allows a display to broadcast its capabilities to the 6 | //! host. 7 | 8 | use std::{ 9 | collections::{btree_map, BTreeMap}, 10 | convert::Infallible, 11 | fmt::{self, Display, Formatter}, 12 | str::FromStr, 13 | }; 14 | 15 | /// VCP feature code 16 | pub type FeatureCode = u8; 17 | 18 | /// VCP Value 19 | #[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] 20 | pub struct Value { 21 | /// Specifies the type of the value, continuous or non-continuous. 22 | pub ty: u8, 23 | /// The high byte of the maximum allowed value. 24 | pub mh: u8, 25 | /// The low byte of the maximum allowed value. 26 | pub ml: u8, 27 | /// The high byte of the value. 28 | pub sh: u8, 29 | /// The low byte of the value. 30 | pub sl: u8, 31 | } 32 | 33 | /// VCP feature type. 34 | #[repr(u8)] 35 | pub enum ValueType { 36 | /// Sending a command of this type changes some aspect of the monitor's operation. 37 | SetParameter = 0, 38 | /// Sending a command of this type causes the monitor to initiate a 39 | /// self-timed operation and then revert to its original state. 40 | /// 41 | /// Examples include display tests and degaussing. 42 | Momentary = 1, 43 | } 44 | 45 | impl Value { 46 | /// Create a new `Value` from a scalar value. 47 | /// 48 | /// Other fields are left as default. 49 | pub fn from_value(v: u16) -> Self { 50 | Value { 51 | sh: (v >> 8) as u8, 52 | sl: v as u8, 53 | ..Default::default() 54 | } 55 | } 56 | 57 | /// Combines the value bytes into a single value. 58 | pub fn value(&self) -> u16 { 59 | ((self.sh as u16) << 8) | self.sl as u16 60 | } 61 | 62 | /// Combines the maximum value bytes into a single value. 63 | pub fn maximum(&self) -> u16 { 64 | ((self.mh as u16) << 8) | self.ml as u16 65 | } 66 | 67 | /// VCP feature type, if recognized. 68 | pub fn ty(&self) -> Result { 69 | match self.ty { 70 | 0 => Ok(ValueType::SetParameter), 71 | 1 => Ok(ValueType::Momentary), 72 | ty => Err(ty), 73 | } 74 | } 75 | } 76 | 77 | impl fmt::Debug for Value { 78 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 79 | f.debug_struct("Value") 80 | .field("maximum", &self.maximum()) 81 | .field("value", &self.value()) 82 | .field("ty", &self.ty) 83 | .finish() 84 | } 85 | } 86 | 87 | /// Extended Display Identification Data 88 | pub type EdidData = Vec; 89 | 90 | /// Video Display Information Format 91 | pub type VdifData = Vec; 92 | 93 | /// VCP feature value names 94 | pub type ValueNames = BTreeMap>; 95 | 96 | // TODO: move Capabilities struct to mccs-caps? It doesn't need to be here other 97 | // than to keep mccs-db from depending on it directly. You can't really use one 98 | // without the other though... 99 | /// Parsed display capabilities string. 100 | #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 101 | pub struct Capabilities { 102 | /// The protocol class. 103 | /// 104 | /// It's not very clear what this field is for. 105 | pub protocol: Option, 106 | /// The type of display. 107 | pub ty: Option, 108 | /// The model name/number of the display. 109 | pub model: Option, 110 | /// A list of the supported VCP commands. 111 | pub commands: Vec, 112 | /// A value of `1` seems to indicate that the monitor has passed Microsoft's 113 | /// Windows Hardware Quality Labs testing. 114 | pub ms_whql: Option, 115 | /// Monitor Command Control Set version code. 116 | pub mccs_version: Option, 117 | /// Virtual Control Panel feature code descriptors. 118 | pub vcp_features: BTreeMap, 119 | /// Extended Display Identification Data 120 | /// 121 | /// Note that although the standard defines this field, in practice it 122 | /// is not used and instead the EDID is read from a separate I2C EEPROM on 123 | /// the monitor. 124 | pub edid: Option, 125 | /// Video Display Information Format are optional extension blocks for the 126 | /// EDID. Like the EDID field, this is probably not in use. 127 | pub vdif: Vec, 128 | /// Additional unrecognized data from the capability string. 129 | pub unknown_tags: Vec, 130 | } 131 | 132 | /// Display protocol class 133 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 134 | pub enum Protocol { 135 | /// Standard monitor 136 | Monitor, 137 | /// I have never seen this outside of an MCCS spec example, it may be a typo. 138 | Display, 139 | /// Unrecognized protocol class 140 | Unknown(String), 141 | } 142 | 143 | impl<'a> From<&'a str> for Protocol { 144 | fn from(s: &'a str) -> Self { 145 | match s { 146 | "monitor" => Protocol::Monitor, 147 | "display" => Protocol::Display, 148 | s => Protocol::Unknown(s.into()), 149 | } 150 | } 151 | } 152 | 153 | impl Display for Protocol { 154 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 155 | Display::fmt( 156 | match *self { 157 | Protocol::Monitor => "monitor", 158 | Protocol::Display => "display", 159 | Protocol::Unknown(ref s) => s, 160 | }, 161 | f, 162 | ) 163 | } 164 | } 165 | 166 | impl FromStr for Protocol { 167 | type Err = Infallible; 168 | 169 | fn from_str(s: &str) -> Result { 170 | Ok(s.into()) 171 | } 172 | } 173 | 174 | /// Display type 175 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 176 | pub enum Type { 177 | /// Cathode Ray Tube display 178 | Crt, 179 | /// Liquid Crystal Display 180 | Lcd, 181 | /// Also an LCD, I'm not sure this should exist. 182 | Led, 183 | /// Unrecognized display type 184 | Unknown(String), 185 | } 186 | 187 | impl<'a> From<&'a str> for Type { 188 | fn from(s: &'a str) -> Self { 189 | match s { 190 | s if s.eq_ignore_ascii_case("crt") => Type::Crt, 191 | s if s.eq_ignore_ascii_case("lcd") => Type::Lcd, 192 | s if s.eq_ignore_ascii_case("led") => Type::Led, 193 | s => Type::Unknown(s.into()), 194 | } 195 | } 196 | } 197 | 198 | impl Display for Type { 199 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 200 | Display::fmt( 201 | match *self { 202 | Type::Crt => "crt", 203 | Type::Lcd => "lcd", 204 | Type::Led => "led", 205 | Type::Unknown(ref s) => s, 206 | }, 207 | f, 208 | ) 209 | } 210 | } 211 | 212 | impl FromStr for Type { 213 | type Err = Infallible; 214 | 215 | fn from_str(s: &str) -> Result { 216 | Ok(s.into()) 217 | } 218 | } 219 | 220 | /// Monitor Command Control Set specification version code 221 | #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 222 | pub struct Version { 223 | /// Major version number 224 | pub major: u8, 225 | /// Minor revision version 226 | pub minor: u8, 227 | } 228 | 229 | impl Version { 230 | /// Create a new MCCS version from the specified version and revision. 231 | pub fn new(major: u8, minor: u8) -> Self { 232 | Version { major, minor } 233 | } 234 | } 235 | 236 | impl Display for Version { 237 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 238 | write!(f, "{}.{}", self.major, self.minor) 239 | } 240 | } 241 | 242 | /// Descriptive information about a supported VCP feature code. 243 | #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 244 | pub struct VcpDescriptor { 245 | /// The name of the feature code, if different from the standard MCCS spec. 246 | pub name: Option, 247 | /// Allowed values for this feature, and optionally their names. 248 | /// 249 | /// This is used for non-continuous VCP types. 250 | pub values: ValueNames, 251 | } 252 | 253 | impl VcpDescriptor { 254 | /// The allowed values for this feature code. 255 | pub fn values(&self) -> btree_map::Keys> { 256 | self.values.keys() 257 | } 258 | } 259 | 260 | /// An unrecognized entry in the capability string 261 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 262 | pub struct UnknownTag { 263 | /// The name of the entry 264 | pub name: String, 265 | /// The data contained in the entry, usually an unparsed string. 266 | pub data: UnknownData, 267 | } 268 | 269 | /// Data that can be contained in a capability entry. 270 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 271 | pub enum UnknownData { 272 | /// UTF-8/ASCII data 273 | String(String), 274 | /// Data that is not valid UTF-8 275 | StringBytes(Vec), 276 | /// Length-prefixed binary data 277 | Binary(Vec), 278 | } 279 | --------------------------------------------------------------------------------