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;
14pub mod migration;
15mod model;
16mod power;
17mod types;
18mod usb;
19mod wifi;
20
21pub use model::Model;
22pub use types::{FrontlightKind, Orientation};
23
24pub struct Device {
25 pub model: Model,
26 pub proto: TouchProto,
27 pub dims: (u32, u32),
28 pub dpi: u16,
29 metadata: OnceCell<DeviceMetadata>,
30 wifi_manager: OnceCell<Box<dyn crate::device::wifi::WifiManager>>,
31 power_manager: OnceCell<Box<dyn crate::device::power::PowerManager>>,
32}
33
34impl Debug for Device {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("Device")
37 .field("model", &self.model)
38 .field("proto", &self.proto)
39 .field("dims", &self.dims)
40 .field("dpi", &self.dpi)
41 .finish()
42 }
43}
44impl Device {
45 fn new(product: &str, model_number: &str) -> Device {
47 let (model, proto, dims, dpi) = match product {
48 "kraken" => (Model::Glo, TouchProto::Single, (758, 1024), 212),
49 "pixie" => (Model::Mini, TouchProto::Single, (600, 800), 200),
50 "dragon" => (Model::AuraHD, TouchProto::Single, (1080, 1440), 265),
51 "phoenix" => (Model::Aura, TouchProto::MultiA, (758, 1024), 212),
52 "dahlia" => (Model::AuraH2O, TouchProto::MultiA, (1080, 1440), 265),
53 "alyssum" => (Model::GloHD, TouchProto::MultiA, (1072, 1448), 300),
54 "pika" => (Model::Touch2, TouchProto::MultiA, (600, 800), 167),
55 "daylight" => {
56 let model = if model_number == "381" {
57 Model::AuraONELimEd
58 } else {
59 Model::AuraONE
60 };
61 (model, TouchProto::MultiA, (1404, 1872), 300)
62 }
63 "star" => {
64 let model = if model_number == "379" {
65 Model::AuraEd2V2
66 } else {
67 Model::AuraEd2V1
68 };
69 (model, TouchProto::MultiA, (758, 1024), 212)
70 }
71 "snow" => {
72 let model = if model_number == "378" {
73 Model::AuraH2OEd2V2
74 } else {
75 Model::AuraH2OEd2V1
76 };
77 (model, TouchProto::MultiB, (1080, 1440), 265)
78 }
79 "nova" => (Model::ClaraHD, TouchProto::MultiB, (1072, 1448), 300),
80 "frost" => {
81 let model = if model_number == "380" {
82 Model::Forma32GB
83 } else {
84 Model::Forma
85 };
86 (model, TouchProto::MultiB, (1440, 1920), 300)
87 }
88 "storm" => (Model::LibraH2O, TouchProto::MultiB, (1264, 1680), 300),
89 "luna" => (Model::Nia, TouchProto::MultiA, (758, 1024), 212),
90 "europa" => (Model::Elipsa, TouchProto::MultiC, (1404, 1872), 227),
91 "cadmus" => (Model::Sage, TouchProto::MultiC, (1440, 1920), 300),
92 "io" => (Model::Libra2, TouchProto::MultiC, (1264, 1680), 300),
93 "goldfinch" => (Model::Clara2E, TouchProto::MultiB, (1072, 1448), 300),
94 "condor" => (Model::Elipsa2E, TouchProto::MultiC, (1404, 1872), 227),
95 "spaBW" | "spaBWTPV" => (Model::ClaraBW, TouchProto::MultiB, (1072, 1448), 300),
96 "spaColour" => (Model::ClaraColour, TouchProto::MultiB, (1072, 1448), 300),
97 "monza" => (Model::LibraColour, TouchProto::MultiB, (1264, 1680), 300),
98 _ => {
99 let model = if model_number == "320" {
100 Model::TouchC
101 } else {
102 Model::TouchAB
103 };
104 (model, TouchProto::Single, (600, 800), 167)
105 }
106 };
107
108 Device {
109 model,
110 proto,
111 dims,
112 dpi,
113 metadata: OnceCell::new(),
114 wifi_manager: OnceCell::new(),
115 power_manager: OnceCell::new(),
116 }
117 }
118
119 pub fn metadata(&self) -> Result<&DeviceMetadata, DeviceError> {
121 self.metadata.get_or_try_init(DeviceMetadata::read)
122 }
123
124 pub fn usb_manager(
126 &self,
127 ) -> Result<Box<dyn crate::device::usb::UsbManager>, crate::device::usb::UsbError> {
128 cfg_select! {
129 feature = "kobo" => {
130 let metadata = self
131 .metadata()
132 .map_err(|e| crate::device::usb::UsbError::DeviceInfo(e.to_string()))?
133 .clone();
134 crate::device::usb::create_usb_manager(metadata)
135 }
136 _ => {
137 Ok(Box::new(crate::device::usb::StubUsbManager))
138 }
139 }
140 }
141
142 pub fn wifi_manager(
144 &self,
145 ) -> Result<&dyn crate::device::wifi::WifiManager, crate::device::wifi::WifiError> {
146 self.wifi_manager
147 .get_or_try_init(crate::device::wifi::create_wifi_manager)
148 .map(|b| b.as_ref())
149 }
150
151 pub fn power_manager(
153 &self,
154 ) -> Result<&dyn crate::device::power::PowerManager, crate::device::power::PowerError> {
155 self.power_manager
156 .get_or_try_init(|| crate::device::power::create_power_manager(self.model))
157 .map(|b| b.as_ref())
158 }
159
160 pub fn install_subdir(&self) -> &'static str {
166 cfg_select! {
167 feature = "emulator" => {""}
168 feature = "test" => { ".adds/cadmus-tst" }
169 _ => { ".adds/cadmus" }
170 }
171 }
172
173 pub fn install_dir(&self) -> PathBuf {
184 cfg_select! {
185 test => {
186 std::env::temp_dir()
187 .join("test-kobo-installation")
188 .join(self.install_subdir())
189 }
190 feature = "emulator" => {
191 PathBuf::from(".").join(self.install_subdir())
192 }
193 _ => {
194 PathBuf::from(crate::settings::INTERNAL_CARD_ROOT).join(self.install_subdir())
195 }
196 }
197 }
198
199 pub fn install_path(&self, relative_path: impl AsRef<Path>) -> PathBuf {
204 self.install_dir().join(relative_path)
205 }
206
207 pub fn data_subdir(&self) -> &'static str {
212 cfg_select! {
213 feature = "test" => { ".cadmus-tst" }
214 _ => { ".cadmus" }
215 }
216 }
217
218 pub fn data_dir(&self) -> PathBuf {
228 cfg_select! {
229 test => { self.install_dir() }
230 feature = "emulator" => {
231 std::env::current_dir()
232 .map(|cwd| cwd.join(self.install_dir()))
233 .unwrap_or_else(|_| self.install_dir())
234 }
235 _ => {
236 if self.has_removable_storage()
237 && Path::new(crate::settings::EXTERNAL_CARD_ROOT).is_dir()
238 {
239 PathBuf::from(crate::settings::EXTERNAL_CARD_ROOT)
240 .join(self.data_subdir())
241 } else {
242 self.install_dir()
243 }
244 }
245 }
246 }
247
248 pub fn data_path(&self, relative_path: impl AsRef<Path>) -> PathBuf {
254 self.data_dir().join(relative_path)
255 }
256
257 pub fn resolve_db_path(&self) -> PathBuf {
265 let data_path = self.data_path(crate::db::DB_FILENAME);
266 if data_path.exists() {
267 return data_path;
268 }
269
270 let install_path = self.install_path(crate::db::DB_FILENAME);
271 if install_path.exists() {
272 tracing::warn!(
273 path = %install_path.display(),
274 "sqlite db found in install dir, not data dir; \
275 copy it to data dir"
276 );
277 return install_path;
278 }
279
280 data_path
281 }
282
283 pub fn tmp_dir(&self) -> PathBuf {
289 self.data_path("tmp")
290 }
291
292 pub fn clean_tmp_dir(&self) {
300 let dir = self.tmp_dir();
301 if let Err(e) = std::fs::remove_dir_all(&dir) {
302 if e.kind() != std::io::ErrorKind::NotFound {
303 tracing::warn!(path = ?dir, error = %e, "Failed to clean tmp dir");
304 }
305 }
306 if let Err(e) = std::fs::create_dir_all(&dir) {
307 tracing::warn!(path = ?dir, error = %e, "Failed to create tmp dir");
308 }
309 }
310
311 pub fn color_samples(&self) -> usize {
313 match self.model {
314 Model::ClaraColour | Model::LibraColour => 3,
315 _ => 1,
316 }
317 }
318
319 pub fn frontlight_kind(&self) -> FrontlightKind {
321 match self.model {
322 Model::ClaraHD
323 | Model::Forma
324 | Model::Forma32GB
325 | Model::LibraH2O
326 | Model::Sage
327 | Model::Libra2
328 | Model::Clara2E
329 | Model::Elipsa2E
330 | Model::ClaraBW
331 | Model::ClaraColour
332 | Model::LibraColour => FrontlightKind::Premixed,
333 Model::AuraONE | Model::AuraONELimEd | Model::AuraH2OEd2V1 | Model::AuraH2OEd2V2 => {
334 FrontlightKind::Natural
335 }
336 _ => FrontlightKind::Standard,
337 }
338 }
339
340 pub fn has_natural_light(&self) -> bool {
342 self.frontlight_kind() != FrontlightKind::Standard
343 }
344
345 pub fn has_lightsensor(&self) -> bool {
347 matches!(self.model, Model::AuraONE | Model::AuraONELimEd)
348 }
349
350 pub fn has_gyroscope(&self) -> bool {
352 matches!(
353 self.model,
354 Model::Forma
355 | Model::Forma32GB
356 | Model::LibraH2O
357 | Model::Elipsa
358 | Model::Sage
359 | Model::Libra2
360 | Model::Elipsa2E
361 | Model::LibraColour
362 )
363 }
364
365 pub fn has_page_turn_buttons(&self) -> bool {
367 matches!(
368 self.model,
369 Model::Forma
370 | Model::Forma32GB
371 | Model::LibraH2O
372 | Model::Sage
373 | Model::Libra2
374 | Model::LibraColour
375 )
376 }
377
378 pub fn has_power_cover(&self) -> bool {
380 matches!(self.model, Model::Sage)
381 }
382
383 pub fn has_removable_storage(&self) -> bool {
385 matches!(
386 self.model,
387 Model::AuraH2O
388 | Model::Aura
389 | Model::AuraHD
390 | Model::Glo
391 | Model::TouchAB
392 | Model::TouchC
393 )
394 }
395
396 pub fn should_invert_buttons(&self, rotation: i8) -> bool {
398 let sr = self.startup_rotation();
399 let (_, dir) = self.mirroring_scheme();
400
401 rotation == (4 + sr - dir) % 4 || rotation == (4 + sr - 2 * dir) % 4
402 }
403
404 pub fn orientation(&self, rotation: i8) -> Orientation {
406 if self.should_swap_axes(rotation) {
407 Orientation::Portrait
408 } else {
409 Orientation::Landscape
410 }
411 }
412
413 pub fn mark(&self) -> u8 {
415 match self.model {
416 Model::LibraColour => 13,
417 Model::ClaraBW | Model::ClaraColour => 12,
418 Model::Elipsa2E => 11,
419 Model::Clara2E => 10,
420 Model::Libra2 => 9,
421 Model::Sage | Model::Elipsa => 8,
422 Model::Nia
423 | Model::LibraH2O
424 | Model::Forma32GB
425 | Model::Forma
426 | Model::ClaraHD
427 | Model::AuraH2OEd2V2
428 | Model::AuraEd2V2 => 7,
429 Model::AuraH2OEd2V1
430 | Model::AuraEd2V1
431 | Model::AuraONELimEd
432 | Model::AuraONE
433 | Model::Touch2
434 | Model::GloHD => 6,
435 Model::AuraH2O | Model::Aura => 5,
436 Model::AuraHD | Model::Mini | Model::Glo | Model::TouchC => 4,
437 Model::TouchAB => 3,
438 }
439 }
440
441 pub fn should_mirror_axes(&self, rotation: i8) -> (bool, bool) {
443 let (mxy, dir) = self.mirroring_scheme();
444 let mx = (4 + (mxy + dir)) % 4;
445 let my = (4 + (mxy - dir)) % 4;
446 let mirror_x = mxy == rotation || mx == rotation;
447 let mirror_y = mxy == rotation || my == rotation;
448 (mirror_x, mirror_y)
449 }
450
451 pub fn mirroring_scheme(&self) -> (i8, i8) {
453 match self.model {
454 Model::AuraH2OEd2V1 | Model::LibraH2O | Model::Libra2 => (3, 1),
455 Model::Sage => (0, 1),
456 Model::AuraH2OEd2V2 => (0, -1),
457 Model::Forma | Model::Forma32GB => (2, -1),
458 _ => (2, 1),
459 }
460 }
461
462 pub fn should_swap_axes(&self, rotation: i8) -> bool {
464 rotation % 2 == self.swapping_scheme()
465 }
466
467 fn swapping_scheme(&self) -> i8 {
469 match self.model {
470 Model::LibraH2O => 0,
471 _ => 1,
472 }
473 }
474
475 pub fn startup_rotation(&self) -> i8 {
477 match self.model {
478 Model::LibraH2O => 0,
479 Model::AuraH2OEd2V1
480 | Model::Forma
481 | Model::Forma32GB
482 | Model::Sage
483 | Model::Libra2
484 | Model::Elipsa2E
485 | Model::LibraColour => 1,
486 _ => 3,
487 }
488 }
489
490 pub fn to_canonical(&self, n: i8) -> i8 {
492 let (_, dir) = self.mirroring_scheme();
493 (4 + dir * (n - self.startup_rotation())) % 4
494 }
495
496 pub fn from_canonical(&self, n: i8) -> i8 {
498 let (_, dir) = self.mirroring_scheme();
499 (self.startup_rotation() + (4 + dir * n) % 4) % 4
500 }
501
502 pub fn transformed_rotation(&self, n: i8) -> i8 {
504 match self.model {
505 Model::AuraHD | Model::AuraH2O => n ^ 2,
506 Model::AuraH2OEd2V2 | Model::Forma | Model::Forma32GB => (4 - n) % 4,
507 _ => n,
508 }
509 }
510
511 pub fn transformed_gyroscope_rotation(&self, n: i8) -> i8 {
513 match self.model {
514 Model::LibraH2O => n ^ 1,
515 Model::Libra2 | Model::Sage | Model::Elipsa2E | Model::LibraColour => (6 - n) % 4,
516 Model::Elipsa => (4 - n) % 4,
517 _ => n,
518 }
519 }
520}
521
522lazy_static! {
523 pub static ref CURRENT_DEVICE: Device = {
527 let product = env::var("PRODUCT").unwrap_or_default();
528 let model_number = env::var("MODEL_NUMBER").unwrap_or_default();
529
530 Device::new(&product, &model_number)
531 };
532}
533
534#[cfg(test)]
535mod tests {
536 use super::Device;
537
538 #[test]
539 fn test_device_canonical_rotation() {
540 let forma = Device::new("frost", "377");
541 let aura_one = Device::new("daylight", "373");
542 for n in 0..4 {
543 assert_eq!(forma.from_canonical(forma.to_canonical(n)), n);
544 }
545 assert_eq!(aura_one.from_canonical(0), aura_one.startup_rotation());
546 assert_eq!(
547 forma.from_canonical(1) - forma.from_canonical(0),
548 aura_one.from_canonical(2) - aura_one.from_canonical(3)
549 );
550 }
551}