mccs_db/
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
27use {
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"]
35mod testdata;
36
37#[rustfmt::skip::macros(named)]
38mod version_req;
39
40/// Describes how to interpret a table's raw value.
41#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub 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
53impl Default for TableInterpretation {
54    fn default() -> Self {
55        TableInterpretation::Generic
56    }
57}
58
59impl 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<String, ()> {
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)]
79pub 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
90impl 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)]
112pub 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
135impl 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)]
143pub 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
153impl 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)]
161pub struct Descriptor {
162    /// The name of the feature.
163    pub name: Option<String>,
164    /// A detailed description of the feature.
165    pub description: Option<String>,
166    /// The MCCS grouping this feature belongs to.
167    pub group: Option<String>,
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<FeatureCode>,
182}
183
184/// Describes all the VCP feature codes supported by an MCCS specification or
185/// display.
186#[derive(Debug, Clone, Default)]
187pub struct Database {
188    entries: BTreeMap<FeatureCode, Descriptor>,
189}
190
191impl 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<R: io::Read>(database_yaml: R, mccs_version: &Version) -> io::Result<Self> {
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)]
374struct DatabaseGroup {
375    id: String,
376    name: String,
377}
378
379#[derive(Debug, Serialize, Deserialize)]
380#[serde(rename_all = "snake_case")]
381enum DatabaseType {
382    Table,
383    #[serde(rename = "nc")]
384    NonContinuous,
385    #[serde(rename = "c")]
386    Continuous,
387}
388
389#[derive(Debug, Serialize, Deserialize)]
390enum 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)]
401enum DatabaseValue {
402    Value(u8),
403    Range(version_req::Req<FeatureCode>),
404}
405
406#[derive(Debug, Serialize, Deserialize)]
407#[serde(rename_all = "snake_case", deny_unknown_fields)]
408struct DatabaseValueDesc {
409    value: DatabaseValue,
410    name: String,
411    #[serde(default, skip_serializing_if = "Option::is_none")]
412    desc: Option<String>,
413    #[serde(default, skip_serializing_if = "Option::is_none")]
414    desc_long: Option<String>,
415}
416#[derive(Debug, Deserialize)]
417#[serde(rename_all = "lowercase")]
418enum DatabaseInterpretationId {
419    CodePage,
420    NonZeroWrite,
421    VcpVersion,
422}
423
424#[derive(Debug, Deserialize)]
425#[serde(untagged)]
426enum DatabaseInterpretation {
427    Id(DatabaseInterpretationId),
428    Values(Vec<DatabaseValueDesc>),
429}
430
431#[derive(Debug, Deserialize)]
432#[serde(rename_all = "snake_case", deny_unknown_fields)]
433struct DatabaseFeature {
434    code: FeatureCode,
435    #[serde(default)]
436    version: version_req::VersionReq,
437    name: Option<String>,
438    #[serde(default, skip_serializing_if = "Option::is_none")]
439    desc: Option<String>,
440    #[serde(default, skip_serializing_if = "Option::is_none")]
441    group: Option<String>,
442    #[serde(rename = "type")]
443    ty: Option<DatabaseType>,
444    #[serde(default)]
445    interpretation: Option<DatabaseInterpretation>,
446    #[serde(default)]
447    mandatory: bool,
448    access: Option<DatabaseReadWrite>,
449    #[serde(default, skip_serializing_if = "Option::is_none")]
450    desc_long: Option<String>,
451    #[serde(default, rename = "interacts")]
452    interacts_with: Vec<FeatureCode>,
453}
454
455#[derive(Debug, Deserialize)]
456#[serde(rename_all = "snake_case", deny_unknown_fields)]
457struct DatabaseFile {
458    groups: Vec<DatabaseGroup>,
459    vcp_features: Vec<DatabaseFeature>,
460}
461
462#[test]
463fn load_database() {
464    for version in &[
465        Version::new(2, 0),
466        Version::new(2, 1),
467        Version::new(2, 2),
468        Version::new(3, 0),
469    ] {
470        let db = Database::from_version(version);
471        for sample in testdata::test_data() {
472            let caps = mccs_caps::parse_capabilities(sample).expect("Failed to parse capabilities");
473            let mut db = db.clone();
474            db.apply_capabilities(&caps);
475            println!("Intersected: {:#?}", db);
476        }
477    }
478}