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 lazy_static::lazy_static;
8use std::collections::HashMap;
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
92lazy_static! {
93    static ref DB: HashMap<String, VendorProducts> = {
94        let mut models: Vec<MouseModel> = Vec::new();
95        for m in data::MODELS {
96            models.push(MouseModel::new(m.0, m.1, m.2, m.3));
97        }
98        for m in data_import_from_chromiumos::MODELS {
99            models.push(MouseModel::new(m.0, m.1, m.2, m.3));
100        }
101
102        let mut db: HashMap<String, VendorProducts> = HashMap::new();
103
104        for m in models {
105            let vendor_id = m.vendor_id.to_owned();
106            match db.get_mut(&vendor_id) {
107                Some(v) => {
108                    v.add(m);
109                }
110                None => {
111                    let mut v = VendorProducts::new();
112                    v.add(m);
113                    db.insert(vendor_id, v);
114                }
115            }
116        }
117
118        db
119    };
120}
121
122/// "Standard" mouse CPI and polling rate.
123const DEFAULT_MODEL: MouseModel = MouseModel {
124    identifier: "default mouse",
125    vendor_id: "*",
126    product_id: "*",
127    counts_per_mm: DEFAULT_COUNTS_PER_MM,
128};
129
130pub(crate) fn get_mouse_model(
131    device_info: Option<fidl_input_report::DeviceInformation>,
132) -> MouseModel {
133    match device_info {
134        None => DEFAULT_MODEL.clone(),
135        Some(device_info) => {
136            let vid = to_hex(device_info.vendor_id.unwrap_or_default());
137            match DB.get(&vid) {
138                Some(v) => match v.get(device_info.product_id.unwrap_or_default()) {
139                    Some(m) => m,
140                    None => DEFAULT_MODEL.clone(),
141                },
142                None => DEFAULT_MODEL.clone(),
143            }
144        }
145    }
146}
147
148/// usb vendor_id and product_id are 16 bit int.
149fn to_hex(id: u32) -> String {
150    format!("{:04x}", id)
151}
152
153#[cfg(test)]
154mod test {
155    use super::super::{data, data_import_from_chromiumos};
156    use super::*;
157    use regex::Regex;
158    use std::collections::HashSet;
159    use test_case::test_case;
160
161    #[test_case("*", "*", 1000, "default mouse" =>
162      MouseModel {
163        vendor_id: "*",
164        identifier: "default mouse",
165        product_id: "*",
166        counts_per_mm: DEFAULT_COUNTS_PER_MM,
167      }; "default mouse")]
168    #[test_case("0001", "*", 1000, "any mouse of vendor" =>
169      MouseModel {
170        vendor_id: "0001",
171        identifier: "any mouse of vendor",
172        product_id: "*",
173        counts_per_mm: DEFAULT_COUNTS_PER_MM,
174      }; "any mouse of vendor")]
175    #[test_case("0001", "001*", 1000, "pattern product_id" =>
176      MouseModel {
177        vendor_id: "0001",
178        identifier: "pattern product_id",
179        product_id: "001*",
180        counts_per_mm: DEFAULT_COUNTS_PER_MM,
181      }; "pattern product_id")]
182    #[test_case("0001", "0002", 1000, "exact model" =>
183      MouseModel {
184        vendor_id: "0001",
185        identifier: "exact model",
186        product_id: "0002",
187        counts_per_mm: DEFAULT_COUNTS_PER_MM,
188      }; "exact model")]
189    #[fuchsia::test]
190    fn test_mouse_model_new(
191        vendor_id: &'static str,
192        product_id: &'static str,
193        cpi: u32,
194        identifier: &'static str,
195    ) -> MouseModel {
196        MouseModel::new(vendor_id, product_id, cpi, identifier)
197    }
198
199    #[test_case(0x046d, 0xc24c =>
200      MouseModel::new("046d", "c24c", 4000, "Logitech G400s")
201      ; "Known mouse")]
202    #[test_case(0x046d, 0xc401 =>
203      MouseModel::new("046d", "c40*", 600, "Logitech Trackballs*")
204      ; "pattern match")]
205    #[test_case(0x05ac, 0x0000 =>
206      MouseModel::new("05ac", "*", 373, "Apple mice (other)")
207      ; "any match")]
208    #[test_case(0x046d, 0x0aaf =>
209      MouseModel::new("*", "*", 1000, "default mouse")
210      ; "Unknown device: this is a microphone")]
211    #[fuchsia::test]
212    fn test_get_mouse_model(vendor_id: u32, product_id: u32) -> MouseModel {
213        get_mouse_model(Some(fidl_input_report::DeviceInformation {
214            vendor_id: Some(vendor_id),
215            product_id: Some(product_id),
216            version: Some(0),
217            polling_rate: Some(0),
218            ..Default::default()
219        }))
220    }
221
222    #[fuchsia::test]
223    fn test_get_mouse_model_none() {
224        pretty_assertions::assert_eq!(get_mouse_model(None), DEFAULT_MODEL);
225    }
226
227    #[fuchsia::test]
228    fn no_duplicated_mouse_model() {
229        let mut models: HashSet<(&'static str, &'static str)> = HashSet::new();
230        let mut check_duplication =
231            |filename: &'static str, list: &[(&'static str, &'static str, u32, &'static str)]| {
232                for m in list {
233                    let new_inserted = models.insert((m.0, m.1));
234                    if !new_inserted {
235                        panic!(
236                            "found duplicated mouse model in {}: vendor: {}, product: {}",
237                            filename, m.0, m.1
238                        );
239                    }
240                }
241            };
242
243        check_duplication("data_import_from_chromiumos", &data_import_from_chromiumos::MODELS);
244        check_duplication("data", &data::MODELS);
245    }
246
247    #[test_case(&data_import_from_chromiumos::MODELS; "data_import_from_chromiumos")]
248    #[test_case(&data::MODELS; "data")]
249    #[fuchsia::test]
250    fn validate_vendor_id_product_id(models: &[(&'static str, &'static str, u32, &'static str)]) {
251        let vendor_id_re = Regex::new(r"^[0-9a-f]{4}$").unwrap();
252        let product_id_re = Regex::new(r"^[0-9a-f]{3}[0-9a-f\*]$").unwrap();
253        for m in models {
254            assert!(vendor_id_re.is_match(m.0), "vendor id should be 4 low case hex digit");
255            if m.1 != "*" {
256                assert!(
257                    product_id_re.is_match(m.1),
258                    r#"product id should be "* only" or "3 low case hex digit with ending *" or "4 low case hex digit""#
259                );
260            }
261        }
262    }
263}