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