input_pipeline/mouse_model_database/
db.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use super::{data, data_import_from_chromiumos};
6use fidl_fuchsia_input_report as fidl_input_report;
7use std::collections::HashMap;
8use std::sync::LazyLock;
9
10/// Mouse have a sensor that tells how far they moved in "counts", depends
11/// on sensor, mouse will report different CPI (counts per inch). Currently,
12/// "standard" mouse is 1000 CPI, and it can up to 8000 CPI. We need this
13/// database to understand how far the mouse moved.
14#[derive(Clone, Debug, PartialEq)]
15pub(crate) struct MouseModel {
16    pub(crate) identifier: &'static str,
17    pub(crate) vendor_id: &'static str,
18    pub(crate) product_id: &'static str,
19    /// Some device has multiple CPI, just give the default CPI.
20    pub(crate) counts_per_mm: u32,
21}
22
23const MM_PER_INCH: f32 = 25.4;
24pub(crate) const DEFAULT_COUNTS_PER_MM: u32 = (1000.0 / MM_PER_INCH) as u32;
25
26impl MouseModel {
27    fn new(
28        vendor_id: &'static str,
29        product_id: &'static str,
30        counts_per_inch: u32,
31        identifier: &'static str,
32    ) -> Self {
33        MouseModel {
34            identifier,
35            vendor_id,
36            product_id,
37            counts_per_mm: ((counts_per_inch as f32) / MM_PER_INCH) as u32,
38        }
39    }
40}
41
42/// contains structures help product id matches in one vendor.
43/// The matching order is:
44/// 1. match exact model first.
45/// 2. match by glob pattern, patterns would not conflict because pattern only
46///    allow 3 hex digits with 1 tailing *, and no duplicated pattern ensured
47///    by `validate_vendor_id_product_id`.
48/// 3. use default model of vendor.
49struct VendorProducts {
50    default_model: Option<MouseModel>,
51    patterns: Vec<(glob::Pattern, MouseModel)>,
52    exact_models: HashMap<String, MouseModel>,
53}
54
55impl VendorProducts {
56    fn new() -> Self {
57        Self { default_model: None, patterns: vec![], exact_models: HashMap::new() }
58    }
59
60    fn add(&mut self, model: MouseModel) {
61        if model.product_id == "*" {
62            // 1 vendor can only have 1 wildcard matching. this panic will
63            // not be happen in production because panic will be caught
64            // in test `validate_vendor_id_product_id`.
65            assert_eq!(self.default_model, None);
66            self.default_model = Some(model);
67        } else if model.product_id.ends_with("*") {
68            // This panic will not be reached in production because all panic
69            // will be caught in test `validate_vendor_id_product_id`.
70            let pattern =
71                glob::Pattern::new(model.product_id).expect("product id is not a valid glob");
72            self.patterns.push((pattern, model));
73        } else {
74            self.exact_models.insert(model.product_id.to_string(), model);
75        }
76    }
77
78    fn get(&self, product_id: u32) -> Option<MouseModel> {
79        let pid = to_hex(product_id);
80        if let Some(m) = self.exact_models.get(&pid) {
81            return Some(m.clone());
82        }
83
84        if let Some((_, m)) = self.patterns.iter().find(|(p, _)| p.matches(&pid)) {
85            return Some(m.clone());
86        }
87
88        self.default_model.clone()
89    }
90}
91
92static DB: LazyLock<HashMap<String, VendorProducts>> = LazyLock::new(|| {
93    let mut models: Vec<MouseModel> = Vec::new();
94    for m in data::MODELS {
95        models.push(MouseModel::new(m.0, m.1, m.2, m.3));
96    }
97    for m in data_import_from_chromiumos::MODELS {
98        models.push(MouseModel::new(m.0, m.1, m.2, m.3));
99    }
100
101    let mut db: HashMap<String, VendorProducts> = HashMap::new();
102
103    for m in models {
104        let vendor_id = m.vendor_id.to_owned();
105        match db.get_mut(&vendor_id) {
106            Some(v) => {
107                v.add(m);
108            }
109            None => {
110                let mut v = VendorProducts::new();
111                v.add(m);
112                db.insert(vendor_id, v);
113            }
114        }
115    }
116
117    db
118});
119
120/// "Standard" mouse CPI and polling rate.
121const DEFAULT_MODEL: MouseModel = MouseModel {
122    identifier: "default mouse",
123    vendor_id: "*",
124    product_id: "*",
125    counts_per_mm: DEFAULT_COUNTS_PER_MM,
126};
127
128pub(crate) fn get_mouse_model(
129    device_info: Option<fidl_input_report::DeviceInformation>,
130) -> MouseModel {
131    match device_info {
132        None => DEFAULT_MODEL.clone(),
133        Some(device_info) => {
134            let vid = to_hex(device_info.vendor_id.unwrap_or_default());
135            match DB.get(&vid) {
136                Some(v) => match v.get(device_info.product_id.unwrap_or_default()) {
137                    Some(m) => m,
138                    None => DEFAULT_MODEL.clone(),
139                },
140                None => DEFAULT_MODEL.clone(),
141            }
142        }
143    }
144}
145
146/// usb vendor_id and product_id are 16 bit int.
147fn to_hex(id: u32) -> String {
148    format!("{:04x}", id)
149}
150
151#[cfg(test)]
152mod test {
153    use super::super::{data, data_import_from_chromiumos};
154    use super::*;
155    use regex::Regex;
156    use std::collections::HashSet;
157    use test_case::test_case;
158
159    #[test_case("*", "*", 1000, "default mouse" =>
160      MouseModel {
161        vendor_id: "*",
162        identifier: "default mouse",
163        product_id: "*",
164        counts_per_mm: DEFAULT_COUNTS_PER_MM,
165      }; "default mouse")]
166    #[test_case("0001", "*", 1000, "any mouse of vendor" =>
167      MouseModel {
168        vendor_id: "0001",
169        identifier: "any mouse of vendor",
170        product_id: "*",
171        counts_per_mm: DEFAULT_COUNTS_PER_MM,
172      }; "any mouse of vendor")]
173    #[test_case("0001", "001*", 1000, "pattern product_id" =>
174      MouseModel {
175        vendor_id: "0001",
176        identifier: "pattern product_id",
177        product_id: "001*",
178        counts_per_mm: DEFAULT_COUNTS_PER_MM,
179      }; "pattern product_id")]
180    #[test_case("0001", "0002", 1000, "exact model" =>
181      MouseModel {
182        vendor_id: "0001",
183        identifier: "exact model",
184        product_id: "0002",
185        counts_per_mm: DEFAULT_COUNTS_PER_MM,
186      }; "exact model")]
187    #[fuchsia::test]
188    fn test_mouse_model_new(
189        vendor_id: &'static str,
190        product_id: &'static str,
191        cpi: u32,
192        identifier: &'static str,
193    ) -> MouseModel {
194        MouseModel::new(vendor_id, product_id, cpi, identifier)
195    }
196
197    #[test_case(0x046d, 0xc24c =>
198      MouseModel::new("046d", "c24c", 4000, "Logitech G400s")
199      ; "Known mouse")]
200    #[test_case(0x046d, 0xc401 =>
201      MouseModel::new("046d", "c40*", 600, "Logitech Trackballs*")
202      ; "pattern match")]
203    #[test_case(0x05ac, 0x0000 =>
204      MouseModel::new("05ac", "*", 373, "Apple mice (other)")
205      ; "any match")]
206    #[test_case(0x046d, 0x0aaf =>
207      MouseModel::new("*", "*", 1000, "default mouse")
208      ; "Unknown device: this is a microphone")]
209    #[fuchsia::test]
210    fn test_get_mouse_model(vendor_id: u32, product_id: u32) -> MouseModel {
211        get_mouse_model(Some(fidl_input_report::DeviceInformation {
212            vendor_id: Some(vendor_id),
213            product_id: Some(product_id),
214            version: Some(0),
215            polling_rate: Some(0),
216            ..Default::default()
217        }))
218    }
219
220    #[fuchsia::test]
221    fn test_get_mouse_model_none() {
222        pretty_assertions::assert_eq!(get_mouse_model(None), DEFAULT_MODEL);
223    }
224
225    #[fuchsia::test]
226    fn no_duplicated_mouse_model() {
227        let mut models: HashSet<(&'static str, &'static str)> = HashSet::new();
228        let mut check_duplication =
229            |filename: &'static str, list: &[(&'static str, &'static str, u32, &'static str)]| {
230                for m in list {
231                    let new_inserted = models.insert((m.0, m.1));
232                    if !new_inserted {
233                        panic!(
234                            "found duplicated mouse model in {}: vendor: {}, product: {}",
235                            filename, m.0, m.1
236                        );
237                    }
238                }
239            };
240
241        check_duplication("data_import_from_chromiumos", &data_import_from_chromiumos::MODELS);
242        check_duplication("data", &data::MODELS);
243    }
244
245    #[test_case(&data_import_from_chromiumos::MODELS; "data_import_from_chromiumos")]
246    #[test_case(&data::MODELS; "data")]
247    #[fuchsia::test]
248    fn validate_vendor_id_product_id(models: &[(&'static str, &'static str, u32, &'static str)]) {
249        let vendor_id_re = Regex::new(r"^[0-9a-f]{4}$").unwrap();
250        let product_id_re = Regex::new(r"^[0-9a-f]{3}[0-9a-f\*]$").unwrap();
251        for m in models {
252            assert!(vendor_id_re.is_match(m.0), "vendor id should be 4 low case hex digit");
253            if m.1 != "*" {
254                assert!(
255                    product_id_re.is_match(m.1),
256                    r#"product id should be "* only" or "3 low case hex digit with ending *" or "4 low case hex digit""#
257                );
258            }
259        }
260    }
261}