1use 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;
10use std::path::{Path, PathBuf};
11
12mod error;
13mod metadata;
14mod model;
15mod types;
16mod usb;
17mod wifi;
18
19pub use model::Model;
20pub use types::{FrontlightKind, Orientation};
21
22pub struct Device {
23 pub model: Model,
24 pub proto: TouchProto,
25 pub dims: (u32, u32),
26 pub dpi: u16,
27 metadata: OnceCell<DeviceMetadata>,
28 wifi_manager: OnceCell<Box<dyn crate::device::wifi::WifiManager>>,
29}
30
31impl Debug for Device {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.debug_struct("Device")
34 .field("model", &self.model)
35 .field("proto", &self.proto)
36 .field("dims", &self.dims)
37 .field("dpi", &self.dpi)
38 .finish()
39 }
40}
41impl Device {
42 fn new(product: &str, model_number: &str) -> Device {
44 match product {
45 "kraken" => Device {
46 model: Model::Glo,
47 proto: TouchProto::Single,
48 dims: (758, 1024),
49 dpi: 212,
50 metadata: OnceCell::new(),
51 wifi_manager: OnceCell::new(),
52 },
53 "pixie" => Device {
54 model: Model::Mini,
55 proto: TouchProto::Single,
56 dims: (600, 800),
57 dpi: 200,
58 metadata: OnceCell::new(),
59 wifi_manager: OnceCell::new(),
60 },
61 "dragon" => Device {
62 model: Model::AuraHD,
63 proto: TouchProto::Single,
64 dims: (1080, 1440),
65 dpi: 265,
66 metadata: OnceCell::new(),
67 wifi_manager: OnceCell::new(),
68 },
69 "phoenix" => Device {
70 model: Model::Aura,
71 proto: TouchProto::MultiA,
72 dims: (758, 1024),
73 dpi: 212,
74 metadata: OnceCell::new(),
75 wifi_manager: OnceCell::new(),
76 },
77 "dahlia" => Device {
78 model: Model::AuraH2O,
79 proto: TouchProto::MultiA,
80 dims: (1080, 1440),
81 dpi: 265,
82 metadata: OnceCell::new(),
83 wifi_manager: OnceCell::new(),
84 },
85 "alyssum" => Device {
86 model: Model::GloHD,
87 proto: TouchProto::MultiA,
88 dims: (1072, 1448),
89 dpi: 300,
90 metadata: OnceCell::new(),
91 wifi_manager: OnceCell::new(),
92 },
93 "pika" => Device {
94 model: Model::Touch2,
95 proto: TouchProto::MultiA,
96 dims: (600, 800),
97 dpi: 167,
98 metadata: OnceCell::new(),
99 wifi_manager: OnceCell::new(),
100 },
101 "daylight" => Device {
102 model: if model_number == "381" {
103 Model::AuraONELimEd
104 } else {
105 Model::AuraONE
106 },
107 proto: TouchProto::MultiA,
108 dims: (1404, 1872),
109 dpi: 300,
110 metadata: OnceCell::new(),
111 wifi_manager: OnceCell::new(),
112 },
113 "star" => Device {
114 model: if model_number == "379" {
115 Model::AuraEd2V2
116 } else {
117 Model::AuraEd2V1
118 },
119 proto: TouchProto::MultiA,
120 dims: (758, 1024),
121 dpi: 212,
122 metadata: OnceCell::new(),
123 wifi_manager: OnceCell::new(),
124 },
125 "snow" => Device {
126 model: if model_number == "378" {
127 Model::AuraH2OEd2V2
128 } else {
129 Model::AuraH2OEd2V1
130 },
131 proto: TouchProto::MultiB,
132 dims: (1080, 1440),
133 dpi: 265,
134 metadata: OnceCell::new(),
135 wifi_manager: OnceCell::new(),
136 },
137 "nova" => Device {
138 model: Model::ClaraHD,
139 proto: TouchProto::MultiB,
140 dims: (1072, 1448),
141 dpi: 300,
142 metadata: OnceCell::new(),
143 wifi_manager: OnceCell::new(),
144 },
145 "frost" => Device {
146 model: if model_number == "380" {
147 Model::Forma32GB
148 } else {
149 Model::Forma
150 },
151 proto: TouchProto::MultiB,
152 dims: (1440, 1920),
153 dpi: 300,
154 metadata: OnceCell::new(),
155 wifi_manager: OnceCell::new(),
156 },
157 "storm" => Device {
158 model: Model::LibraH2O,
159 proto: TouchProto::MultiB,
160 dims: (1264, 1680),
161 dpi: 300,
162 metadata: OnceCell::new(),
163 wifi_manager: OnceCell::new(),
164 },
165 "luna" => Device {
166 model: Model::Nia,
167 proto: TouchProto::MultiA,
168 dims: (758, 1024),
169 dpi: 212,
170 metadata: OnceCell::new(),
171 wifi_manager: OnceCell::new(),
172 },
173 "europa" => Device {
174 model: Model::Elipsa,
175 proto: TouchProto::MultiC,
176 dims: (1404, 1872),
177 dpi: 227,
178 metadata: OnceCell::new(),
179 wifi_manager: OnceCell::new(),
180 },
181 "cadmus" => Device {
182 model: Model::Sage,
183 proto: TouchProto::MultiC,
184 dims: (1440, 1920),
185 dpi: 300,
186 metadata: OnceCell::new(),
187 wifi_manager: OnceCell::new(),
188 },
189 "io" => Device {
190 model: Model::Libra2,
191 proto: TouchProto::MultiC,
192 dims: (1264, 1680),
193 dpi: 300,
194 metadata: OnceCell::new(),
195 wifi_manager: OnceCell::new(),
196 },
197 "goldfinch" => Device {
198 model: Model::Clara2E,
199 proto: TouchProto::MultiB,
200 dims: (1072, 1448),
201 dpi: 300,
202 metadata: OnceCell::new(),
203 wifi_manager: OnceCell::new(),
204 },
205 "condor" => Device {
206 model: Model::Elipsa2E,
207 proto: TouchProto::MultiC,
208 dims: (1404, 1872),
209 dpi: 227,
210 metadata: OnceCell::new(),
211 wifi_manager: OnceCell::new(),
212 },
213 "spaBW" | "spaBWTPV" => Device {
214 model: Model::ClaraBW,
215 proto: TouchProto::MultiB,
216 dims: (1072, 1448),
217 dpi: 300,
218 metadata: OnceCell::new(),
219 wifi_manager: OnceCell::new(),
220 },
221 "spaColour" => Device {
222 model: Model::ClaraColour,
223 proto: TouchProto::MultiB,
224 dims: (1072, 1448),
225 dpi: 300,
226 metadata: OnceCell::new(),
227 wifi_manager: OnceCell::new(),
228 },
229 "monza" => Device {
230 model: Model::LibraColour,
231 proto: TouchProto::MultiB,
232 dims: (1264, 1680),
233 dpi: 300,
234 metadata: OnceCell::new(),
235 wifi_manager: OnceCell::new(),
236 },
237 _ => Device {
238 model: if model_number == "320" {
239 Model::TouchC
240 } else {
241 Model::TouchAB
242 },
243 proto: TouchProto::Single,
244 dims: (600, 800),
245 dpi: 167,
246 metadata: OnceCell::new(),
247 wifi_manager: OnceCell::new(),
248 },
249 }
250 }
251
252 pub fn metadata(&self) -> Result<&DeviceMetadata, DeviceError> {
254 self.metadata.get_or_try_init(DeviceMetadata::read)
255 }
256
257 #[cfg(feature = "kobo")]
259 pub fn usb_manager(
260 &self,
261 ) -> Result<Box<dyn crate::device::usb::UsbManager>, crate::device::usb::UsbError> {
262 let metadata = self
263 .metadata()
264 .map_err(|e| crate::device::usb::UsbError::DeviceInfo(e.to_string()))?
265 .clone();
266 crate::device::usb::create_usb_manager(metadata)
267 }
268
269 #[cfg(not(feature = "kobo"))]
271 pub fn usb_manager(
272 &self,
273 ) -> Result<Box<dyn crate::device::usb::UsbManager>, crate::device::usb::UsbError> {
274 Ok(Box::new(crate::device::usb::StubUsbManager))
275 }
276
277 pub fn wifi_manager(
279 &self,
280 ) -> Result<&dyn crate::device::wifi::WifiManager, crate::device::wifi::WifiError> {
281 self.wifi_manager
282 .get_or_try_init(crate::device::wifi::create_wifi_manager)
283 .map(|b| b.as_ref())
284 }
285
286 pub fn install_subdir(&self) -> &'static str {
292 #[cfg(not(feature = "test"))]
293 return ".adds/cadmus";
294
295 #[cfg(feature = "test")]
296 return ".adds/cadmus-tst";
297 }
298
299 pub fn install_dir(&self) -> PathBuf {
310 #[cfg(test)]
311 return std::env::temp_dir()
312 .join("test-kobo-installation")
313 .join(self.install_subdir());
314
315 #[cfg(all(feature = "emulator", not(test)))]
316 return PathBuf::from("/tmp").join(self.install_subdir());
317
318 #[cfg(all(not(feature = "emulator"), not(test)))]
319 return PathBuf::from(crate::settings::INTERNAL_CARD_ROOT).join(self.install_subdir());
320 }
321
322 pub fn install_path(&self, relative_path: impl AsRef<Path>) -> PathBuf {
327 self.install_dir().join(relative_path)
328 }
329
330 pub fn tmp_dir(&self) -> PathBuf {
335 self.install_path("tmp")
336 }
337
338 pub fn clean_tmp_dir(&self) {
346 let dir = self.tmp_dir();
347 if let Err(e) = std::fs::remove_dir_all(&dir) {
348 if e.kind() != std::io::ErrorKind::NotFound {
349 tracing::warn!(path = ?dir, error = %e, "Failed to clean tmp dir");
350 }
351 }
352 if let Err(e) = std::fs::create_dir_all(&dir) {
353 tracing::warn!(path = ?dir, error = %e, "Failed to create tmp dir");
354 }
355 }
356
357 pub fn color_samples(&self) -> usize {
359 match self.model {
360 Model::ClaraColour | Model::LibraColour => 3,
361 _ => 1,
362 }
363 }
364
365 pub fn frontlight_kind(&self) -> FrontlightKind {
367 match self.model {
368 Model::ClaraHD
369 | Model::Forma
370 | Model::Forma32GB
371 | Model::LibraH2O
372 | Model::Sage
373 | Model::Libra2
374 | Model::Clara2E
375 | Model::Elipsa2E
376 | Model::ClaraBW
377 | Model::ClaraColour
378 | Model::LibraColour => FrontlightKind::Premixed,
379 Model::AuraONE | Model::AuraONELimEd | Model::AuraH2OEd2V1 | Model::AuraH2OEd2V2 => {
380 FrontlightKind::Natural
381 }
382 _ => FrontlightKind::Standard,
383 }
384 }
385
386 pub fn has_natural_light(&self) -> bool {
388 self.frontlight_kind() != FrontlightKind::Standard
389 }
390
391 pub fn has_lightsensor(&self) -> bool {
393 matches!(self.model, Model::AuraONE | Model::AuraONELimEd)
394 }
395
396 pub fn has_gyroscope(&self) -> bool {
398 matches!(
399 self.model,
400 Model::Forma
401 | Model::Forma32GB
402 | Model::LibraH2O
403 | Model::Elipsa
404 | Model::Sage
405 | Model::Libra2
406 | Model::Elipsa2E
407 | Model::LibraColour
408 )
409 }
410
411 pub fn has_page_turn_buttons(&self) -> bool {
413 matches!(
414 self.model,
415 Model::Forma
416 | Model::Forma32GB
417 | Model::LibraH2O
418 | Model::Sage
419 | Model::Libra2
420 | Model::LibraColour
421 )
422 }
423
424 pub fn has_power_cover(&self) -> bool {
426 matches!(self.model, Model::Sage)
427 }
428
429 pub fn has_removable_storage(&self) -> bool {
431 matches!(
432 self.model,
433 Model::AuraH2O
434 | Model::Aura
435 | Model::AuraHD
436 | Model::Glo
437 | Model::TouchAB
438 | Model::TouchC
439 )
440 }
441
442 pub fn should_invert_buttons(&self, rotation: i8) -> bool {
444 let sr = self.startup_rotation();
445 let (_, dir) = self.mirroring_scheme();
446
447 rotation == (4 + sr - dir) % 4 || rotation == (4 + sr - 2 * dir) % 4
448 }
449
450 pub fn orientation(&self, rotation: i8) -> Orientation {
452 if self.should_swap_axes(rotation) {
453 Orientation::Portrait
454 } else {
455 Orientation::Landscape
456 }
457 }
458
459 pub fn mark(&self) -> u8 {
461 match self.model {
462 Model::LibraColour => 13,
463 Model::ClaraBW | Model::ClaraColour => 12,
464 Model::Elipsa2E => 11,
465 Model::Clara2E => 10,
466 Model::Libra2 => 9,
467 Model::Sage | Model::Elipsa => 8,
468 Model::Nia
469 | Model::LibraH2O
470 | Model::Forma32GB
471 | Model::Forma
472 | Model::ClaraHD
473 | Model::AuraH2OEd2V2
474 | Model::AuraEd2V2 => 7,
475 Model::AuraH2OEd2V1
476 | Model::AuraEd2V1
477 | Model::AuraONELimEd
478 | Model::AuraONE
479 | Model::Touch2
480 | Model::GloHD => 6,
481 Model::AuraH2O | Model::Aura => 5,
482 Model::AuraHD | Model::Mini | Model::Glo | Model::TouchC => 4,
483 Model::TouchAB => 3,
484 }
485 }
486
487 pub fn should_mirror_axes(&self, rotation: i8) -> (bool, bool) {
489 let (mxy, dir) = self.mirroring_scheme();
490 let mx = (4 + (mxy + dir)) % 4;
491 let my = (4 + (mxy - dir)) % 4;
492 let mirror_x = mxy == rotation || mx == rotation;
493 let mirror_y = mxy == rotation || my == rotation;
494 (mirror_x, mirror_y)
495 }
496
497 pub fn mirroring_scheme(&self) -> (i8, i8) {
499 match self.model {
500 Model::AuraH2OEd2V1 | Model::LibraH2O | Model::Libra2 => (3, 1),
501 Model::Sage => (0, 1),
502 Model::AuraH2OEd2V2 => (0, -1),
503 Model::Forma | Model::Forma32GB => (2, -1),
504 _ => (2, 1),
505 }
506 }
507
508 pub fn should_swap_axes(&self, rotation: i8) -> bool {
510 rotation % 2 == self.swapping_scheme()
511 }
512
513 fn swapping_scheme(&self) -> i8 {
515 match self.model {
516 Model::LibraH2O => 0,
517 _ => 1,
518 }
519 }
520
521 pub fn startup_rotation(&self) -> i8 {
523 match self.model {
524 Model::LibraH2O => 0,
525 Model::AuraH2OEd2V1
526 | Model::Forma
527 | Model::Forma32GB
528 | Model::Sage
529 | Model::Libra2
530 | Model::Elipsa2E
531 | Model::LibraColour => 1,
532 _ => 3,
533 }
534 }
535
536 pub fn to_canonical(&self, n: i8) -> i8 {
538 let (_, dir) = self.mirroring_scheme();
539 (4 + dir * (n - self.startup_rotation())) % 4
540 }
541
542 pub fn from_canonical(&self, n: i8) -> i8 {
544 let (_, dir) = self.mirroring_scheme();
545 (self.startup_rotation() + (4 + dir * n) % 4) % 4
546 }
547
548 pub fn transformed_rotation(&self, n: i8) -> i8 {
550 match self.model {
551 Model::AuraHD | Model::AuraH2O => n ^ 2,
552 Model::AuraH2OEd2V2 | Model::Forma | Model::Forma32GB => (4 - n) % 4,
553 _ => n,
554 }
555 }
556
557 pub fn transformed_gyroscope_rotation(&self, n: i8) -> i8 {
559 match self.model {
560 Model::LibraH2O => n ^ 1,
561 Model::Libra2 | Model::Sage | Model::Elipsa2E | Model::LibraColour => (6 - n) % 4,
562 Model::Elipsa => (4 - n) % 4,
563 _ => n,
564 }
565 }
566}
567
568lazy_static! {
569 pub static ref CURRENT_DEVICE: Device = {
573 let product = env::var("PRODUCT").unwrap_or_default();
574 let model_number = env::var("MODEL_NUMBER").unwrap_or_default();
575
576 Device::new(&product, &model_number)
577 };
578}
579
580#[cfg(test)]
581mod tests {
582 use super::Device;
583
584 #[test]
585 fn test_device_canonical_rotation() {
586 let forma = Device::new("frost", "377");
587 let aura_one = Device::new("daylight", "373");
588 for n in 0..4 {
589 assert_eq!(forma.from_canonical(forma.to_canonical(n)), n);
590 }
591 assert_eq!(aura_one.from_canonical(0), aura_one.startup_rotation());
592 assert_eq!(
593 forma.from_canonical(1) - forma.from_canonical(0),
594 aura_one.from_canonical(2) - aura_one.from_canonical(3)
595 );
596 }
597}