cadmus_core/device/
mod.rs

1//! Device detection and management.
2
3use crate::device::error::DeviceError;
4use crate::device::metadata::DeviceMetadata;
5use crate::input::TouchProto;
6use lazy_static::lazy_static;
7use once_cell::sync::OnceCell;
8use std::env;
9use std::fmt::Debug;
10
11mod error;
12mod metadata;
13mod model;
14mod types;
15mod usb;
16mod wifi;
17
18pub use model::Model;
19pub use types::{FrontlightKind, Orientation};
20
21pub struct Device {
22    pub model: Model,
23    pub proto: TouchProto,
24    pub dims: (u32, u32),
25    pub dpi: u16,
26    metadata: OnceCell<DeviceMetadata>,
27    wifi_manager: OnceCell<Box<dyn crate::device::wifi::WifiManager>>,
28}
29
30impl Debug for Device {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.debug_struct("Device")
33            .field("model", &self.model)
34            .field("proto", &self.proto)
35            .field("dims", &self.dims)
36            .field("dpi", &self.dpi)
37            .finish()
38    }
39}
40impl Device {
41    /// Creates a new device from product and model number strings.
42    fn new(product: &str, model_number: &str) -> Device {
43        match product {
44            "kraken" => Device {
45                model: Model::Glo,
46                proto: TouchProto::Single,
47                dims: (758, 1024),
48                dpi: 212,
49                metadata: OnceCell::new(),
50                wifi_manager: OnceCell::new(),
51            },
52            "pixie" => Device {
53                model: Model::Mini,
54                proto: TouchProto::Single,
55                dims: (600, 800),
56                dpi: 200,
57                metadata: OnceCell::new(),
58                wifi_manager: OnceCell::new(),
59            },
60            "dragon" => Device {
61                model: Model::AuraHD,
62                proto: TouchProto::Single,
63                dims: (1080, 1440),
64                dpi: 265,
65                metadata: OnceCell::new(),
66                wifi_manager: OnceCell::new(),
67            },
68            "phoenix" => Device {
69                model: Model::Aura,
70                proto: TouchProto::MultiA,
71                dims: (758, 1024),
72                dpi: 212,
73                metadata: OnceCell::new(),
74                wifi_manager: OnceCell::new(),
75            },
76            "dahlia" => Device {
77                model: Model::AuraH2O,
78                proto: TouchProto::MultiA,
79                dims: (1080, 1440),
80                dpi: 265,
81                metadata: OnceCell::new(),
82                wifi_manager: OnceCell::new(),
83            },
84            "alyssum" => Device {
85                model: Model::GloHD,
86                proto: TouchProto::MultiA,
87                dims: (1072, 1448),
88                dpi: 300,
89                metadata: OnceCell::new(),
90                wifi_manager: OnceCell::new(),
91            },
92            "pika" => Device {
93                model: Model::Touch2,
94                proto: TouchProto::MultiA,
95                dims: (600, 800),
96                dpi: 167,
97                metadata: OnceCell::new(),
98                wifi_manager: OnceCell::new(),
99            },
100            "daylight" => Device {
101                model: if model_number == "381" {
102                    Model::AuraONELimEd
103                } else {
104                    Model::AuraONE
105                },
106                proto: TouchProto::MultiA,
107                dims: (1404, 1872),
108                dpi: 300,
109                metadata: OnceCell::new(),
110                wifi_manager: OnceCell::new(),
111            },
112            "star" => Device {
113                model: if model_number == "379" {
114                    Model::AuraEd2V2
115                } else {
116                    Model::AuraEd2V1
117                },
118                proto: TouchProto::MultiA,
119                dims: (758, 1024),
120                dpi: 212,
121                metadata: OnceCell::new(),
122                wifi_manager: OnceCell::new(),
123            },
124            "snow" => Device {
125                model: if model_number == "378" {
126                    Model::AuraH2OEd2V2
127                } else {
128                    Model::AuraH2OEd2V1
129                },
130                proto: TouchProto::MultiB,
131                dims: (1080, 1440),
132                dpi: 265,
133                metadata: OnceCell::new(),
134                wifi_manager: OnceCell::new(),
135            },
136            "nova" => Device {
137                model: Model::ClaraHD,
138                proto: TouchProto::MultiB,
139                dims: (1072, 1448),
140                dpi: 300,
141                metadata: OnceCell::new(),
142                wifi_manager: OnceCell::new(),
143            },
144            "frost" => Device {
145                model: if model_number == "380" {
146                    Model::Forma32GB
147                } else {
148                    Model::Forma
149                },
150                proto: TouchProto::MultiB,
151                dims: (1440, 1920),
152                dpi: 300,
153                metadata: OnceCell::new(),
154                wifi_manager: OnceCell::new(),
155            },
156            "storm" => Device {
157                model: Model::LibraH2O,
158                proto: TouchProto::MultiB,
159                dims: (1264, 1680),
160                dpi: 300,
161                metadata: OnceCell::new(),
162                wifi_manager: OnceCell::new(),
163            },
164            "luna" => Device {
165                model: Model::Nia,
166                proto: TouchProto::MultiA,
167                dims: (758, 1024),
168                dpi: 212,
169                metadata: OnceCell::new(),
170                wifi_manager: OnceCell::new(),
171            },
172            "europa" => Device {
173                model: Model::Elipsa,
174                proto: TouchProto::MultiC,
175                dims: (1404, 1872),
176                dpi: 227,
177                metadata: OnceCell::new(),
178                wifi_manager: OnceCell::new(),
179            },
180            "cadmus" => Device {
181                model: Model::Sage,
182                proto: TouchProto::MultiC,
183                dims: (1440, 1920),
184                dpi: 300,
185                metadata: OnceCell::new(),
186                wifi_manager: OnceCell::new(),
187            },
188            "io" => Device {
189                model: Model::Libra2,
190                proto: TouchProto::MultiC,
191                dims: (1264, 1680),
192                dpi: 300,
193                metadata: OnceCell::new(),
194                wifi_manager: OnceCell::new(),
195            },
196            "goldfinch" => Device {
197                model: Model::Clara2E,
198                proto: TouchProto::MultiB,
199                dims: (1072, 1448),
200                dpi: 300,
201                metadata: OnceCell::new(),
202                wifi_manager: OnceCell::new(),
203            },
204            "condor" => Device {
205                model: Model::Elipsa2E,
206                proto: TouchProto::MultiC,
207                dims: (1404, 1872),
208                dpi: 227,
209                metadata: OnceCell::new(),
210                wifi_manager: OnceCell::new(),
211            },
212            "spaBW" | "spaBWTPV" => Device {
213                model: Model::ClaraBW,
214                proto: TouchProto::MultiB,
215                dims: (1072, 1448),
216                dpi: 300,
217                metadata: OnceCell::new(),
218                wifi_manager: OnceCell::new(),
219            },
220            "spaColour" => Device {
221                model: Model::ClaraColour,
222                proto: TouchProto::MultiB,
223                dims: (1072, 1448),
224                dpi: 300,
225                metadata: OnceCell::new(),
226                wifi_manager: OnceCell::new(),
227            },
228            "monza" => Device {
229                model: Model::LibraColour,
230                proto: TouchProto::MultiB,
231                dims: (1264, 1680),
232                dpi: 300,
233                metadata: OnceCell::new(),
234                wifi_manager: OnceCell::new(),
235            },
236            _ => Device {
237                model: if model_number == "320" {
238                    Model::TouchC
239                } else {
240                    Model::TouchAB
241                },
242                proto: TouchProto::Single,
243                dims: (600, 800),
244                dpi: 167,
245                metadata: OnceCell::new(),
246                wifi_manager: OnceCell::new(),
247            },
248        }
249    }
250
251    /// Gets device metadata (lazy initialization).
252    pub fn metadata(&self) -> Result<&DeviceMetadata, DeviceError> {
253        self.metadata.get_or_try_init(DeviceMetadata::read)
254    }
255
256    /// Creates USB manager for this device.
257    #[cfg(feature = "kobo")]
258    pub fn usb_manager(
259        &self,
260    ) -> Result<Box<dyn crate::device::usb::UsbManager>, crate::device::usb::UsbError> {
261        let metadata = self
262            .metadata()
263            .map_err(|e| crate::device::usb::UsbError::DeviceInfo(e.to_string()))?
264            .clone();
265        crate::device::usb::create_usb_manager(metadata)
266    }
267
268    /// Creates stub USB manager (non-kobo builds).
269    #[cfg(not(feature = "kobo"))]
270    pub fn usb_manager(
271        &self,
272    ) -> Result<Box<dyn crate::device::usb::UsbManager>, crate::device::usb::UsbError> {
273        Ok(Box::new(crate::device::usb::StubUsbManager))
274    }
275
276    /// Returns the WiFi manager for this device.
277    pub fn wifi_manager(
278        &self,
279    ) -> Result<&dyn crate::device::wifi::WifiManager, crate::device::wifi::WifiError> {
280        self.wifi_manager
281            .get_or_try_init(crate::device::wifi::create_wifi_manager)
282            .map(|b| b.as_ref())
283    }
284
285    /// Returns the number of color samples for the device screen.
286    pub fn color_samples(&self) -> usize {
287        match self.model {
288            Model::ClaraColour | Model::LibraColour => 3,
289            _ => 1,
290        }
291    }
292
293    /// Returns the frontlight kind for this device.
294    pub fn frontlight_kind(&self) -> FrontlightKind {
295        match self.model {
296            Model::ClaraHD
297            | Model::Forma
298            | Model::Forma32GB
299            | Model::LibraH2O
300            | Model::Sage
301            | Model::Libra2
302            | Model::Clara2E
303            | Model::Elipsa2E
304            | Model::ClaraBW
305            | Model::ClaraColour
306            | Model::LibraColour => FrontlightKind::Premixed,
307            Model::AuraONE | Model::AuraONELimEd | Model::AuraH2OEd2V1 | Model::AuraH2OEd2V2 => {
308                FrontlightKind::Natural
309            }
310            _ => FrontlightKind::Standard,
311        }
312    }
313
314    /// Returns true if the device has natural light capability.
315    pub fn has_natural_light(&self) -> bool {
316        self.frontlight_kind() != FrontlightKind::Standard
317    }
318
319    /// Returns true if the device has a light sensor.
320    pub fn has_lightsensor(&self) -> bool {
321        matches!(self.model, Model::AuraONE | Model::AuraONELimEd)
322    }
323
324    /// Returns true if the device has a gyroscope.
325    pub fn has_gyroscope(&self) -> bool {
326        matches!(
327            self.model,
328            Model::Forma
329                | Model::Forma32GB
330                | Model::LibraH2O
331                | Model::Elipsa
332                | Model::Sage
333                | Model::Libra2
334                | Model::Elipsa2E
335                | Model::LibraColour
336        )
337    }
338
339    /// Returns true if the device has page turn buttons.
340    pub fn has_page_turn_buttons(&self) -> bool {
341        matches!(
342            self.model,
343            Model::Forma
344                | Model::Forma32GB
345                | Model::LibraH2O
346                | Model::Sage
347                | Model::Libra2
348                | Model::LibraColour
349        )
350    }
351
352    /// Returns true if the device supports a power cover.
353    pub fn has_power_cover(&self) -> bool {
354        matches!(self.model, Model::Sage)
355    }
356
357    /// Returns true if the device has removable storage.
358    pub fn has_removable_storage(&self) -> bool {
359        matches!(
360            self.model,
361            Model::AuraH2O
362                | Model::Aura
363                | Model::AuraHD
364                | Model::Glo
365                | Model::TouchAB
366                | Model::TouchC
367        )
368    }
369
370    /// Returns true if buttons should be inverted for the given rotation.
371    pub fn should_invert_buttons(&self, rotation: i8) -> bool {
372        let sr = self.startup_rotation();
373        let (_, dir) = self.mirroring_scheme();
374
375        rotation == (4 + sr - dir) % 4 || rotation == (4 + sr - 2 * dir) % 4
376    }
377
378    /// Returns the orientation for the given rotation.
379    pub fn orientation(&self, rotation: i8) -> Orientation {
380        if self.should_swap_axes(rotation) {
381            Orientation::Portrait
382        } else {
383            Orientation::Landscape
384        }
385    }
386
387    /// Returns the device mark value.
388    pub fn mark(&self) -> u8 {
389        match self.model {
390            Model::LibraColour => 13,
391            Model::ClaraBW | Model::ClaraColour => 12,
392            Model::Elipsa2E => 11,
393            Model::Clara2E => 10,
394            Model::Libra2 => 9,
395            Model::Sage | Model::Elipsa => 8,
396            Model::Nia
397            | Model::LibraH2O
398            | Model::Forma32GB
399            | Model::Forma
400            | Model::ClaraHD
401            | Model::AuraH2OEd2V2
402            | Model::AuraEd2V2 => 7,
403            Model::AuraH2OEd2V1
404            | Model::AuraEd2V1
405            | Model::AuraONELimEd
406            | Model::AuraONE
407            | Model::Touch2
408            | Model::GloHD => 6,
409            Model::AuraH2O | Model::Aura => 5,
410            Model::AuraHD | Model::Mini | Model::Glo | Model::TouchC => 4,
411            Model::TouchAB => 3,
412        }
413    }
414
415    /// Returns whether axes should be mirrored for the given rotation.
416    pub fn should_mirror_axes(&self, rotation: i8) -> (bool, bool) {
417        let (mxy, dir) = self.mirroring_scheme();
418        let mx = (4 + (mxy + dir)) % 4;
419        let my = (4 + (mxy - dir)) % 4;
420        let mirror_x = mxy == rotation || mx == rotation;
421        let mirror_y = mxy == rotation || my == rotation;
422        (mirror_x, mirror_y)
423    }
424
425    /// Returns the center and direction of the mirroring pattern.
426    pub fn mirroring_scheme(&self) -> (i8, i8) {
427        match self.model {
428            Model::AuraH2OEd2V1 | Model::LibraH2O | Model::Libra2 => (3, 1),
429            Model::Sage => (0, 1),
430            Model::AuraH2OEd2V2 => (0, -1),
431            Model::Forma | Model::Forma32GB => (2, -1),
432            _ => (2, 1),
433        }
434    }
435
436    /// Returns true if axes should be swapped for the given rotation.
437    pub fn should_swap_axes(&self, rotation: i8) -> bool {
438        rotation % 2 == self.swapping_scheme()
439    }
440
441    /// Returns the swapping scheme value.
442    fn swapping_scheme(&self) -> i8 {
443        match self.model {
444            Model::LibraH2O => 0,
445            _ => 1,
446        }
447    }
448
449    /// Returns the startup rotation value.
450    pub fn startup_rotation(&self) -> i8 {
451        match self.model {
452            Model::LibraH2O => 0,
453            Model::AuraH2OEd2V1
454            | Model::Forma
455            | Model::Forma32GB
456            | Model::Sage
457            | Model::Libra2
458            | Model::Elipsa2E
459            | Model::LibraColour => 1,
460            _ => 3,
461        }
462    }
463
464    /// Returns a device independent rotation value.
465    pub fn to_canonical(&self, n: i8) -> i8 {
466        let (_, dir) = self.mirroring_scheme();
467        (4 + dir * (n - self.startup_rotation())) % 4
468    }
469
470    /// Returns a device dependent rotation value from canonical.
471    pub fn from_canonical(&self, n: i8) -> i8 {
472        let (_, dir) = self.mirroring_scheme();
473        (self.startup_rotation() + (4 + dir * n) % 4) % 4
474    }
475
476    /// Returns the transformed rotation value.
477    pub fn transformed_rotation(&self, n: i8) -> i8 {
478        match self.model {
479            Model::AuraHD | Model::AuraH2O => n ^ 2,
480            Model::AuraH2OEd2V2 | Model::Forma | Model::Forma32GB => (4 - n) % 4,
481            _ => n,
482        }
483    }
484
485    /// Returns the transformed gyroscope rotation value.
486    pub fn transformed_gyroscope_rotation(&self, n: i8) -> i8 {
487        match self.model {
488            Model::LibraH2O => n ^ 1,
489            Model::Libra2 | Model::Sage | Model::Elipsa2E | Model::LibraColour => (6 - n) % 4,
490            Model::Elipsa => (4 - n) % 4,
491            _ => n,
492        }
493    }
494}
495
496lazy_static! {
497    // TODO(OGKevin): we shan't rely on these env variables to construct the device, and instead
498    //                do discovery here instead of in the bash script.
499    /// Global singleton for the current device.
500    pub static ref CURRENT_DEVICE: Device = {
501        let product = env::var("PRODUCT").unwrap_or_default();
502        let model_number = env::var("MODEL_NUMBER").unwrap_or_default();
503
504        Device::new(&product, &model_number)
505    };
506}
507
508#[cfg(test)]
509mod tests {
510    use super::Device;
511
512    #[test]
513    fn test_device_canonical_rotation() {
514        let forma = Device::new("frost", "377");
515        let aura_one = Device::new("daylight", "373");
516        for n in 0..4 {
517            assert_eq!(forma.from_canonical(forma.to_canonical(n)), n);
518        }
519        assert_eq!(aura_one.from_canonical(0), aura_one.startup_rotation());
520        assert_eq!(
521            forma.from_canonical(1) - forma.from_canonical(0),
522            aura_one.from_canonical(2) - aura_one.from_canonical(3)
523        );
524    }
525}