diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index 1278c24f..6ce34536 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -290,3 +290,4 @@ STR_NO_FOOTNOTES: "На гэтай старонцы няма зносак" STR_LINK: "[спасылка]" STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: " STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)" +STR_TILT_PAGE_TURN: "Перагортванне нахілам" diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index d4478313..ec377bb9 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -290,3 +290,4 @@ STR_LINK: "[enllaç]" STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla" STR_AUTO_TURN_ENABLED: "Passar automàtic activat: " STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)" +STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació" diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index 9ba06d62..27113b56 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -262,3 +262,4 @@ STR_BOOK_S_STYLE: "Styl knihy" STR_EMBEDDED_STYLE: "Vložený styl" STR_OPDS_SERVER_URL: "URL serveru OPDS" STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky" +STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním" diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index 02ac2864..2b631872 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -290,3 +290,4 @@ STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Tag skærmbillede" STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: " STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)" +STR_TILT_PAGE_TURN: "Vip for at vende side" diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index abe05da9..7c6bdfaf 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -289,4 +289,5 @@ STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina" STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Screenshot maken" STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: " -STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)" \ No newline at end of file +STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)" +STR_TILT_PAGE_TURN: "Kantel om te bladeren" \ No newline at end of file diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 1aba1ea0..ee5dc84d 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -305,6 +305,7 @@ STR_CRASH_TITLE: "System Crash" STR_CRASH_DESCRIPTION: "A detailed report was saved to crash_report.txt. Please include this file in your bug report." STR_CRASH_REASON: "Crash reason:" STR_CRASH_NO_REASON: "(No reason was recorded)" +STR_TILT_PAGE_TURN: "Tilt Page Turn" STR_KB_HINT_MOVE_CURSOR: "Press LEFT or RIGHT to move cursor" STR_KB_HINT_RETURN_CURSOR: "Press LEFT to return to cursor position" STR_KB_HINT_HIDE_PASSWORD: "Hold RIGHT then press [***] to hide password" diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml index 87ff1ee1..71785d43 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -260,3 +260,4 @@ STR_BOOK_S_STYLE: "Kirjan tyyli" STR_EMBEDDED_STYLE: "Upotettu tyyli" STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite" STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus" +STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index d300b444..b4118a1c 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -291,3 +291,4 @@ STR_LINK: "[lien]" STR_SCREENSHOT_BUTTON: "Capture d'écran" STR_AUTO_TURN_ENABLED: "Tourne-page auto : " STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)" +STR_TILT_PAGE_TURN: "Tourner par inclinaison" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 8fb17111..2a5fcf56 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -321,3 +321,4 @@ STR_UPTIME: "Laufzeit" STR_CHARGING: "Lädt" STR_GATHERING_DATA: "Daten werden gesammelt..." STR_READING: "Lese..." +STR_TILT_PAGE_TURN: "Neigung zum Blättern" diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index b23f33e7..0343c197 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -290,3 +290,4 @@ STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Képernyőkép készítése" STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: " STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)" +STR_TILT_PAGE_TURN: "Döntéses lapozás" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 0da638f5..9f452bac 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -290,3 +290,4 @@ STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Screenshot" STR_AUTO_TURN_ENABLED: "Cambio pagina automatico: " STR_AUTO_TURN_PAGES_PER_MIN: "Cambio pagina automatico (pag/min)" +STR_TILT_PAGE_TURN: "Gira pagina con inclinazione" diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index 29fe7b8f..8ff2c99d 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -289,3 +289,4 @@ STR_LINK: "[сілтеме]" STR_SCREENSHOT_BUTTON: "Скриншот түсіру" STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: " STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)" +STR_TILT_PAGE_TURN: "Еңкейту арқылы бет аудару" diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index e72d554b..c004c49e 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -290,3 +290,4 @@ STR_LINK: "[nuoroda]" STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka" STR_AUTO_TURN_ENABLED: "Auto-vertimas: " STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)" +STR_TILT_PAGE_TURN: "Puslapio vertimas pakreipiant" diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index 2b5867ce..8807ee65 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -290,3 +290,4 @@ STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Zrób zrzut ekranu" STR_AUTO_TURN_ENABLED: "Auto-kartkowanie: " STR_AUTO_TURN_PAGES_PER_MIN: "Auto-kartkowanie (str./min)" +STR_TILT_PAGE_TURN: "Kartkowanie przechyłem" diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index 7a0fd8fe..5ff85267 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -262,3 +262,4 @@ STR_BOOK_S_STYLE: "Estilo do livro" STR_EMBEDDED_STYLE: "Estilo embutido" STR_OPDS_SERVER_URL: "URL do servidor OPDS" STR_SCREENSHOT_BUTTON: "Capturar tela" +STR_TILT_PAGE_TURN: "Virar página por inclinação" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index 59b70f41..3d557264 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -290,3 +290,4 @@ STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Captură ecran" STR_AUTO_TURN_ENABLED: "Răsfoire automată: " STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut" +STR_TILT_PAGE_TURN: "Întoarcere pagină prin înclinare" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index fdd032d1..1391cbcc 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -291,6 +291,7 @@ STR_OPDS_SERVER_URL: "URL OPDS сервера" STR_SCREENSHOT_BUTTON: "Сделать снимок экрана" STR_AUTO_TURN_ENABLED: "Автоперелистывание: " STR_AUTO_TURN_PAGES_PER_MIN: "Автоперелистывание (стр./мин)" +STR_TILT_PAGE_TURN: "Перелистывание наклоном" STR_ADD_SERVER: "Добавить сервер" STR_SERVER_NAME: "Имя сервера" STR_NO_SERVERS: "Нет настроенных серверов OPDS" diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml index 6bf3a91a..19dcf784 100644 --- a/lib/I18n/translations/slovenian.yaml +++ b/lib/I18n/translations/slovenian.yaml @@ -290,3 +290,4 @@ STR_LINK: "[povezava]" STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona" STR_AUTO_TURN_ENABLED: "Samodejno obračanje: " STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)" +STR_TILT_PAGE_TURN: "Obračanje s priklonom" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 2a105bf8..c37353f8 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -289,5 +289,6 @@ STR_FOOTNOTES: "Pie de página" STR_NO_FOOTNOTES: "No hay notas al pie de esta página" STR_LINK: "[enlace]" STR_SCREENSHOT_BUTTON: "Tomar captura de pantalla" +STR_TILT_PAGE_TURN: "Pasar página inclinando" STR_AUTO_TURN_ENABLED: "Avance activado: " STR_AUTO_TURN_PAGES_PER_MIN: "Avance auto. (pág./min)" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index bb58cbdf..815d0cc3 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -305,6 +305,7 @@ STR_CRASH_TITLE: "Systemkrasch" STR_CRASH_DESCRIPTION: "En detaljerad rapport sparades till crash_report.txt. Vänligen inkludera den här filen i din felrapport." STR_CRASH_REASON: "Orsak till kraschen:" STR_CRASH_NO_REASON: "(Ingen orsak registrerades)" +STR_TILT_PAGE_TURN: "Luta för att vända sida" STR_KB_HINT_MOVE_CURSOR: "Tryck VÄNSTER eller HÖGER för att flytta markören" STR_KB_HINT_RETURN_CURSOR: "Tryck VÄNSTER för att återgå till markörpositionen" STR_KB_HINT_HIDE_PASSWORD: "Håll HÖGER och tryck sedan på [***] för att dölja lösenordet" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index 3f65b2ec..a517eb27 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -290,3 +290,4 @@ STR_SCREENSHOT_BUTTON: "Ekran görüntüsü al" STR_SELECTED: "Seçili" STR_SHOW: "Göster" STR_TITLE: "Başlık" +STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme" diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index b2cab499..d271ac45 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -291,3 +291,4 @@ STR_LINK: "[посилання]" STR_SCREENSHOT_BUTTON: "Знімок екрана" STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: " STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)" +STR_TILT_PAGE_TURN: "Перегортання нахилом" diff --git a/lib/hal/HalTiltSensor.cpp b/lib/hal/HalTiltSensor.cpp new file mode 100644 index 00000000..b5808cca --- /dev/null +++ b/lib/hal/HalTiltSensor.cpp @@ -0,0 +1,234 @@ +#include "HalTiltSensor.h" + +#include + +HalTiltSensor halTiltSensor; // Singleton instance + +bool HalTiltSensor::writeReg(uint8_t reg, uint8_t val) const { + Wire.beginTransmission(_i2cAddr); + Wire.write(reg); + Wire.write(val); + return Wire.endTransmission() == 0; +} + +bool HalTiltSensor::readReg(uint8_t reg, uint8_t* val) const { + Wire.beginTransmission(_i2cAddr); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) { + return false; + } + Wire.requestFrom(_i2cAddr, (uint8_t)1); + if (Wire.available() < 1) { + return false; + } + *val = Wire.read(); + return true; +} + +bool HalTiltSensor::readGyro(float& gx, float& gy, float& gz) const { + Wire.beginTransmission(_i2cAddr); + Wire.write(REG_GX_L); // Start reading at Gyro X Low + if (Wire.endTransmission(false) != 0) { + return false; + } + + Wire.requestFrom(_i2cAddr, (uint8_t)6); + if (Wire.available() < 6) { + return false; + } + + auto readInt16 = [&]() -> int16_t { + const uint8_t lo = Wire.read(); + const uint8_t hi = Wire.read(); + return static_cast((hi << 8) | lo); + }; + + // If Full Scale is ±512 dps, the scale factor is 32768 / 512 = 64 LSB/dps + constexpr float SCALE = 1.0f / 64.0f; + gx = readInt16() * SCALE; + gy = readInt16() * SCALE; + gz = readInt16() * SCALE; + return true; +} + +void HalTiltSensor::begin() { + if (!gpio.deviceIsX3()) { + _available = false; + return; + } + + // Try primary address, then alternate + uint8_t whoami = 0; + _i2cAddr = I2C_ADDR_QMI8658; + if (!readReg(QMI8658_WHO_AM_I_REG, &whoami) || whoami != QMI8658_WHO_AM_I_VALUE) { + _i2cAddr = I2C_ADDR_QMI8658_ALT; + if (!readReg(QMI8658_WHO_AM_I_REG, &whoami) || whoami != QMI8658_WHO_AM_I_VALUE) { + LOG_ERR("GYR", "QMI8658 IMU not found"); + _available = false; + return; + } + } + + LOG_INF("GYR", "QMI8658 IMU found at 0x%02X", _i2cAddr); + + if (!writeReg(REG_CTRL7, CTRL7_DISABLE_ALL) || !writeReg(REG_CTRL3, CTRL3_FS_512DPS | CTRL3_ODR_28HZ) || + !writeReg(REG_CTRL1, CTRL1_BASE | CTRL1_SENSOR_DISABLE)) { + LOG_ERR("GYR", "QMI8658 register configuration failed"); + _available = false; + return; + } + + _available = true; + _initMs = millis(); + _lastPollMs = millis(); + LOG_INF("GYR", "QMI8658 gyro initialized and put to sleep"); +} + +bool HalTiltSensor::wake() { + if (!_available) { + return false; + } + + // Wait for init to complete before waking + if ((millis() - _initMs) < SLEEP_STABILIZE_MS) { + return false; + } + + if (writeReg(REG_CTRL1, CTRL1_BASE) && writeReg(REG_CTRL7, CTRL7_GYRO_ENABLE)) { + _lastPollMs = millis(); + _lastTiltMs = millis(); + _wakeMs = millis(); + LOG_INF("GYR", "QMI8658 woke up"); + return true; + } else { + LOG_ERR("GYR", "Failed to wake QMI8658"); + return false; + } +} + +bool HalTiltSensor::deepSleep() { + if (!_available) { + return false; + } + + if ((millis() - _wakeMs) < SLEEP_STABILIZE_MS) { + return false; + } + + if (writeReg(REG_CTRL7, CTRL7_DISABLE_ALL) && writeReg(REG_CTRL1, CTRL1_BASE | CTRL1_SENSOR_DISABLE)) { + // Clear any residual state so it doesn't immediately trigger upon waking + clearPendingEvents(); + _inTilt = false; + LOG_INF("GYR", "QMI8658 entered sleep mode"); + return true; + } else { + LOG_ERR("GYR", "Failed to put QMI8658 to sleep"); + return false; + } +} + +void HalTiltSensor::update(const uint8_t mode, const uint8_t orientation, const bool inReader) { + if (!_available) { + return; + } + + // State machine: wake up or sleep based on the enabled flag + if ((mode != CrossPointTiltPageTurn::TILT_OFF) && !_isAwake) { + _isAwake = wake(); + return; + } else if ((mode == CrossPointTiltPageTurn::TILT_OFF) && _isAwake) { + _isAwake = !deepSleep(); + return; + } + + // If disabled, skip the rest of the polling logic and avoid unnecessary I2C traffic in non-reader activities + if ((mode == CrossPointTiltPageTurn::TILT_OFF) || !inReader) { + return; + } + + const unsigned long now = millis(); + // Stabilization: discard readings during gyro startup transient + if ((now - _wakeMs) < WAKE_STABILIZE_MS) { + return; + } + + if ((now - _lastPollMs) < POLL_INTERVAL_MS) { + return; + } + _lastPollMs = now; + + float gx, gy, gz; + if (!readGyro(gx, gy, gz)) { + return; + } + + // Map the gyro axis to left/right tilt based on reader orientation. + // On the X3 PCB: X axis = left/right in portrait, Y axis = left/right in landscape. + float tiltAxis; + switch (orientation) { + case CrossPointOrientation::PORTRAIT: + tiltAxis = mode == CrossPointTiltPageTurn::TILT_INVERTED ? -gx : gx; + break; + case CrossPointOrientation::INVERTED: + tiltAxis = mode == CrossPointTiltPageTurn::TILT_INVERTED ? gx : -gx; + break; + case CrossPointOrientation::LANDSCAPE_CW: + tiltAxis = mode == CrossPointTiltPageTurn::TILT_INVERTED ? gy : -gy; + break; + case CrossPointOrientation::LANDSCAPE_CCW: + tiltAxis = mode == CrossPointTiltPageTurn::TILT_INVERTED ? -gy : gy; + break; + default: + tiltAxis = gx; + break; + } + + if (_inTilt) { + // Wait for device to return to neutral before allowing next trigger + if (fabsf(tiltAxis) < NEUTRAL_RATE_DPS) { + _inTilt = false; + } + } else { + // Check for new tilt gesture (with cooldown) + if ((now - _lastTiltMs) >= COOLDOWN_MS) { + if (tiltAxis > RATE_THRESHOLD_DPS) { + _tiltForwardEvent = true; + _hadActivity = true; + _inTilt = true; + _lastTiltMs = now; + LOG_INF("GYR", "Forward Trigger=(%.1f) dps", tiltAxis); + } else if (tiltAxis < -RATE_THRESHOLD_DPS) { + _tiltBackEvent = true; + _hadActivity = true; + _inTilt = true; + _lastTiltMs = now; + LOG_INF("GYR", "Backward Trigger=(%.1f) dps", tiltAxis); + } + } + } +} + +bool HalTiltSensor::wasTiltedForward() { + const bool val = _tiltForwardEvent; + _tiltForwardEvent = false; + return val; +} + +bool HalTiltSensor::wasTiltedBack() { + const bool val = _tiltBackEvent; + _tiltBackEvent = false; + return val; +} + +bool HalTiltSensor::hadActivity() { + const bool val = _hadActivity; + _hadActivity = false; + return val; +} + +void HalTiltSensor::clearPendingEvents() { + _tiltForwardEvent = false; + _tiltBackEvent = false; + _hadActivity = false; + // Intentionally preserve _inTilt so a held tilt doesn't retrigger on next poll +} diff --git a/lib/hal/HalTiltSensor.h b/lib/hal/HalTiltSensor.h new file mode 100644 index 00000000..ef132183 --- /dev/null +++ b/lib/hal/HalTiltSensor.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include + +#include "HalGPIO.h" + +// TODO: Move enums into new header and share with CrossPointSettings.h +namespace CrossPointOrientation { +enum Value : uint8_t { PORTRAIT = 0, LANDSCAPE_CW = 1, INVERTED = 2, LANDSCAPE_CCW = 3 }; +} + +namespace CrossPointTiltPageTurn { +enum Value : uint8_t { TILT_OFF = 0, TILT_NORMAL = 1, TILT_INVERTED = 2 }; +} + +class HalTiltSensor; +extern HalTiltSensor halTiltSensor; // Singleton + +class HalTiltSensor { + bool _available = false; + uint8_t _i2cAddr = 0; + + // Tilt gesture state machine + bool _tiltForwardEvent = false; // Consumed by wasTiltedForward() + bool _tiltBackEvent = false; // Consumed by wasTiltedBack() + bool _hadActivity = false; // Non-consuming flag for sleep timer + bool _inTilt = false; // Currently tilted past threshold + bool _isAwake = false; // Tracks power state + unsigned long _initMs = 0; // Timestamp of sensor init + unsigned long _lastTiltMs = 0; // Debounce / cooldown + unsigned long _wakeMs = 0; // Timestamp of last wake() for stabilization + + // Tuning constants + static constexpr float RATE_THRESHOLD_DPS = 270.0f; // Deg/sec speed to trigger flick + static constexpr float NEUTRAL_RATE_DPS = 50.0f; // Must stop moving below this rate before next trigger + static constexpr unsigned long COOLDOWN_MS = 600; // Minimum ms between triggers + static constexpr unsigned long POLL_INTERVAL_MS = 50; // 20 Hz polling + static constexpr unsigned long WAKE_STABILIZE_MS = 300; // Ignore readings after wake + static constexpr unsigned long SLEEP_STABILIZE_MS = 15; // Sleep turn on/off delay + + mutable unsigned long _lastPollMs = 0; + + // --- QMI8658 registers --- + static constexpr uint8_t REG_CTRL1 = 0x02; + static constexpr uint8_t REG_CTRL3 = 0x04; + static constexpr uint8_t REG_CTRL7 = 0x08; + static constexpr uint8_t REG_GX_L = 0x3B; + + // --- Register Bit Flags --- + + // REG_CTRL1 (0x02) + static constexpr uint8_t CTRL1_BIG_ENDIAN = (1 << 5); // 0x20: Default state (1 = Big Endian) + static constexpr uint8_t CTRL1_AUTO_INC = (1 << 6); // 0x40: Enable address auto-increment + static constexpr uint8_t CTRL1_SENSOR_DISABLE = (1 << 0); // 0x01: Power down sensor engine + static constexpr uint8_t CTRL1_BASE = CTRL1_AUTO_INC | CTRL1_BIG_ENDIAN; // 0x60 + + // REG_CTRL3 (0x04) - Gyro Config + static constexpr uint8_t CTRL3_FS_512DPS = (0b101 << 4); // Bits 6:4 = 101 + static constexpr uint8_t CTRL3_ODR_28HZ = 0b1000; // Bits 3:0 = 1000 (28.025 Hz) + + // REG_CTRL7 (0x08) - Enable + static constexpr uint8_t CTRL7_DISABLE_ALL = 0x00; + static constexpr uint8_t CTRL7_GYRO_ENABLE = (1 << 1); // Bit 1 = 1 + + bool writeReg(uint8_t reg, uint8_t val) const; + bool readReg(uint8_t reg, uint8_t* val) const; + bool readGyro(float& gx, float& gy, float& gz) const; + + public: + // Call after gpio.begin() and powerManager.begin() (I2C already initialised for X3) + void begin(); + + // Enables the QMI8658 internal sensor engine + bool wake(); + + // Puts the QMI8658 into a low-power standby state + bool deepSleep(); + + // True if the QMI8658 IMU is present on this device + bool isAvailable() const { return _available; } + + // Poll the accelerometer and update tilt gesture state. + void update(const uint8_t mode, const uint8_t orientation, const bool inReader); + + // Returns true once per tilt-forward gesture (next page direction). + // Consumed on read — subsequent calls return false until next gesture. + bool wasTiltedForward(); + + // Returns true once per tilt-back gesture (previous page direction). + // Consumed on read. + bool wasTiltedBack(); + + // Non-consuming: true if any tilt activity occurred since last call. + // Used to reset the auto-sleep inactivity timer. + bool hadActivity(); + + // Discard any pending tilt events (call when leaving reader or disabling tilt). + void clearPendingEvents(); +}; diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index a407e2d2..5d35c9e9 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -137,6 +137,8 @@ class CrossPointSettings { // Image rendering in EPUB reader enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT }; + enum TILT_PAGE_TURN { TILT_OFF = 0, TILT_NORMAL = 1, TILT_NVERTED = 2, TILT_PAGE_TURN_COUNT }; + // Sleep screen settings uint8_t sleepScreen = DARK; // Sleep screen cover mode settings @@ -199,6 +201,8 @@ class CrossPointSettings { uint8_t showHiddenFiles = 0; // Image rendering mode in EPUB reader uint8_t imageRendering = IMAGES_DISPLAY; + // Tilt-based page turning (X3 only — requires QMI8658 IMU) + uint8_t tiltPageTurn = TILT_OFF; ~CrossPointSettings() = default; diff --git a/src/SettingsList.h b/src/SettingsList.h index aaa00f8a..9a9b5b09 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -12,122 +13,137 @@ // Each entry has a key (for JSON API) and category (for grouping). // ACTION-type entries and entries without a key are device-only. inline const std::vector& getSettingsList() { - static const std::vector list = { - // --- Display --- - SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, - {StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT, - StrId::STR_COVER_CUSTOM}, - "sleepScreen", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode, - {StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter, - {StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED}, - "sleepScreenCoverFilter", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage, - {StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage", - StrId::STR_CAT_DISPLAY), - SettingInfo::Enum( - StrId::STR_REFRESH_FREQ, &CrossPointSettings::refreshFrequency, - {StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30}, - "refreshFrequency", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_UI_THEME, &CrossPointSettings::uiTheme, - {StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA, StrId::STR_THEME_LYRA_EXTENDED, - StrId::STR_THEME_ROUNDEDRAFF}, - "uiTheme", StrId::STR_CAT_DISPLAY), - SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix", + static const std::vector list = [] { + std::vector v = { + // --- Display --- + SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, + {StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT, + StrId::STR_COVER_CUSTOM}, + "sleepScreen", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode, + {StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter, + {StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED}, + "sleepScreenCoverFilter", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage, + {StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum( + StrId::STR_REFRESH_FREQ, &CrossPointSettings::refreshFrequency, + {StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30}, + "refreshFrequency", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_UI_THEME, &CrossPointSettings::uiTheme, + {StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA, StrId::STR_THEME_LYRA_EXTENDED, + StrId::STR_THEME_ROUNDEDRAFF}, + "uiTheme", StrId::STR_CAT_DISPLAY), + SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix", + StrId::STR_CAT_DISPLAY), - // --- Reader --- - SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily, - {StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, "fontFamily", - StrId::STR_CAT_READER), - SettingInfo::Enum(StrId::STR_FONT_SIZE, &CrossPointSettings::fontSize, - {StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE}, "fontSize", - StrId::STR_CAT_READER), - SettingInfo::Enum(StrId::STR_LINE_SPACING, &CrossPointSettings::lineSpacing, - {StrId::STR_TIGHT, StrId::STR_NORMAL, StrId::STR_WIDE}, "lineSpacing", StrId::STR_CAT_READER), - SettingInfo::Value(StrId::STR_SCREEN_MARGIN, &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin", - StrId::STR_CAT_READER), - SettingInfo::Enum(StrId::STR_PARA_ALIGNMENT, &CrossPointSettings::paragraphAlignment, - {StrId::STR_JUSTIFY, StrId::STR_ALIGN_LEFT, StrId::STR_CENTER, StrId::STR_ALIGN_RIGHT, - StrId::STR_BOOK_S_STYLE}, - "paragraphAlignment", StrId::STR_CAT_READER), - SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle", + // --- Reader --- + SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily, + {StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, "fontFamily", StrId::STR_CAT_READER), - SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", + SettingInfo::Enum(StrId::STR_FONT_SIZE, &CrossPointSettings::fontSize, + {StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE}, "fontSize", StrId::STR_CAT_READER), - SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation, - {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, - "orientation", StrId::STR_CAT_READER), - SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, "extraParagraphSpacing", - StrId::STR_CAT_READER), - SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing", - StrId::STR_CAT_READER), - SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering, - {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, - "imageRendering", StrId::STR_CAT_READER), - // --- Controls --- - SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout, - {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS), - SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip", - StrId::STR_CAT_CONTROLS), - SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, - {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH}, - "shortPwrBtn", StrId::STR_CAT_CONTROLS), + SettingInfo::Enum(StrId::STR_LINE_SPACING, &CrossPointSettings::lineSpacing, + {StrId::STR_TIGHT, StrId::STR_NORMAL, StrId::STR_WIDE}, "lineSpacing", StrId::STR_CAT_READER), + SettingInfo::Value(StrId::STR_SCREEN_MARGIN, &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin", + StrId::STR_CAT_READER), + SettingInfo::Enum(StrId::STR_PARA_ALIGNMENT, &CrossPointSettings::paragraphAlignment, + {StrId::STR_JUSTIFY, StrId::STR_ALIGN_LEFT, StrId::STR_CENTER, StrId::STR_ALIGN_RIGHT, + StrId::STR_BOOK_S_STYLE}, + "paragraphAlignment", StrId::STR_CAT_READER), + SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle", + StrId::STR_CAT_READER), + SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", + StrId::STR_CAT_READER), + SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation, + {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, + "orientation", StrId::STR_CAT_READER), + SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, + "extraParagraphSpacing", StrId::STR_CAT_READER), + SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing", + StrId::STR_CAT_READER), + SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering, + {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, + "imageRendering", StrId::STR_CAT_READER), + // --- Controls --- + SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout, + {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS), + SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, + "longPressChapterSkip", StrId::STR_CAT_CONTROLS), + SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, + {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH}, + "shortPwrBtn", StrId::STR_CAT_CONTROLS), - // --- System --- - SettingInfo::Enum(StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeout, - {StrId::STR_MIN_1, StrId::STR_MIN_5, StrId::STR_MIN_10, StrId::STR_MIN_15, StrId::STR_MIN_30}, - "sleepTimeout", StrId::STR_CAT_SYSTEM), - SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles", - StrId::STR_CAT_SYSTEM), + // --- System --- + SettingInfo::Enum(StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeout, + {StrId::STR_MIN_1, StrId::STR_MIN_5, StrId::STR_MIN_10, StrId::STR_MIN_15, StrId::STR_MIN_30}, + "sleepTimeout", StrId::STR_CAT_SYSTEM), + SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles", + StrId::STR_CAT_SYSTEM), - // --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- - SettingInfo::DynamicString( - StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); }, - [](const std::string& v) { - KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword()); - KOREADER_STORE.saveToFile(); - }, - "koUsername", StrId::STR_KOREADER_SYNC), - SettingInfo::DynamicString( - StrId::STR_KOREADER_PASSWORD, [] { return KOREADER_STORE.getPassword(); }, - [](const std::string& v) { - KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v); - KOREADER_STORE.saveToFile(); - }, - "koPassword", StrId::STR_KOREADER_SYNC), - SettingInfo::DynamicString( - StrId::STR_SYNC_SERVER_URL, [] { return KOREADER_STORE.getServerUrl(); }, - [](const std::string& v) { - KOREADER_STORE.setServerUrl(v); - KOREADER_STORE.saveToFile(); - }, - "koServerUrl", StrId::STR_KOREADER_SYNC), - SettingInfo::DynamicEnum( - StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY}, - [] { return static_cast(KOREADER_STORE.getMatchMethod()); }, - [](uint8_t v) { - KOREADER_STORE.setMatchMethod(static_cast(v)); - KOREADER_STORE.saveToFile(); - }, - "koMatchMethod", StrId::STR_KOREADER_SYNC), - // --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) --- - SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount, - "statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Toggle(StrId::STR_BOOK_PROGRESS_PERCENTAGE, &CrossPointSettings::statusBarBookProgressPercentage, - "statusBarBookProgressPercentage", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Enum(StrId::STR_PROGRESS_BAR, &CrossPointSettings::statusBarProgressBar, - {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarProgressBar", - StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Enum(StrId::STR_PROGRESS_BAR_THICKNESS, &CrossPointSettings::statusBarProgressBarThickness, - {StrId::STR_PROGRESS_BAR_THIN, StrId::STR_PROGRESS_BAR_MEDIUM, StrId::STR_PROGRESS_BAR_THICK}, - "statusBarProgressBarThickness", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Enum(StrId::STR_TITLE, &CrossPointSettings::statusBarTitle, - {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarTitle", - StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Toggle(StrId::STR_BATTERY, &CrossPointSettings::statusBarBattery, "statusBarBattery", + // --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- + SettingInfo::DynamicString( + StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); }, + [](const std::string& v) { + KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword()); + KOREADER_STORE.saveToFile(); + }, + "koUsername", StrId::STR_KOREADER_SYNC), + SettingInfo::DynamicString( + StrId::STR_KOREADER_PASSWORD, [] { return KOREADER_STORE.getPassword(); }, + [](const std::string& v) { + KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v); + KOREADER_STORE.saveToFile(); + }, + "koPassword", StrId::STR_KOREADER_SYNC), + SettingInfo::DynamicString( + StrId::STR_SYNC_SERVER_URL, [] { return KOREADER_STORE.getServerUrl(); }, + [](const std::string& v) { + KOREADER_STORE.setServerUrl(v); + KOREADER_STORE.saveToFile(); + }, + "koServerUrl", StrId::STR_KOREADER_SYNC), + SettingInfo::DynamicEnum( + StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY}, + [] { return static_cast(KOREADER_STORE.getMatchMethod()); }, + [](uint8_t v) { + KOREADER_STORE.setMatchMethod(static_cast(v)); + KOREADER_STORE.saveToFile(); + }, + "koMatchMethod", StrId::STR_KOREADER_SYNC), + // --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) --- + SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount, + "statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Toggle(StrId::STR_BOOK_PROGRESS_PERCENTAGE, &CrossPointSettings::statusBarBookProgressPercentage, + "statusBarBookProgressPercentage", StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Enum(StrId::STR_PROGRESS_BAR, &CrossPointSettings::statusBarProgressBar, + {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarProgressBar", StrId::STR_CUSTOMISE_STATUS_BAR), - }; + SettingInfo::Enum(StrId::STR_PROGRESS_BAR_THICKNESS, &CrossPointSettings::statusBarProgressBarThickness, + {StrId::STR_PROGRESS_BAR_THIN, StrId::STR_PROGRESS_BAR_MEDIUM, StrId::STR_PROGRESS_BAR_THICK}, + "statusBarProgressBarThickness", StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Enum(StrId::STR_TITLE, &CrossPointSettings::statusBarTitle, + {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarTitle", + StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Toggle(StrId::STR_BATTERY, &CrossPointSettings::statusBarBattery, "statusBarBattery", + StrId::STR_CUSTOMISE_STATUS_BAR), + }; + // Only show tilt page turn setting when the QMI8658 IMU is present (X3) + if (halTiltSensor.isAvailable()) { + // Insert after the short power button setting (end of Controls section) + for (auto it = v.begin(); it != v.end(); ++it) { + if (it->nameId == StrId::STR_SHORT_PWR_BTN) { + v.insert(it + 1, SettingInfo::Enum(StrId::STR_TILT_PAGE_TURN, &CrossPointSettings::tiltPageTurn, + {StrId::STR_STATE_OFF, StrId::STR_NORMAL, StrId::STR_INVERTED}, + "tiltPageTurn", StrId::STR_CAT_CONTROLS)); + break; + } + } + } + return v; + }(); return list; } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index bd700b76..5b4d668c 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -184,7 +184,7 @@ void EpubReaderActivity::loop() { return; } - auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); + auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput); if (!prevTriggered && !nextTriggered) { return; } @@ -202,7 +202,7 @@ void EpubReaderActivity::loop() { return; } - const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs; + const bool skipChapter = !fromTilt && SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs; // Don't skip chapter after screenshot if (gpio.wasReleased(HalGPIO::BTN_POWER) && gpio.wasReleased(HalGPIO::BTN_DOWN)) { diff --git a/src/activities/reader/ReaderUtils.h b/src/activities/reader/ReaderUtils.h index 8694be08..8f8eb073 100644 --- a/src/activities/reader/ReaderUtils.h +++ b/src/activities/reader/ReaderUtils.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "MappedInputManager.h" @@ -32,21 +33,24 @@ inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) { struct PageTurnResult { bool prev; bool next; + bool fromTilt; }; inline PageTurnResult detectPageTurn(const MappedInputManager& input) { const bool usePress = !SETTINGS.longPressChapterSkip; - const bool prev = usePress ? (input.wasPressed(MappedInputManager::Button::PageBack) || - input.wasPressed(MappedInputManager::Button::Left)) - : (input.wasReleased(MappedInputManager::Button::PageBack) || - input.wasReleased(MappedInputManager::Button::Left)); + const bool tiltNext = SETTINGS.tiltPageTurn && halTiltSensor.wasTiltedForward(); + const bool tiltPrev = SETTINGS.tiltPageTurn && halTiltSensor.wasTiltedBack(); + const bool prev = tiltPrev || (usePress ? (input.wasPressed(MappedInputManager::Button::PageBack) || + input.wasPressed(MappedInputManager::Button::Left)) + : (input.wasReleased(MappedInputManager::Button::PageBack) || + input.wasReleased(MappedInputManager::Button::Left))); const bool powerTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && input.wasReleased(MappedInputManager::Button::Power); - const bool next = usePress ? (input.wasPressed(MappedInputManager::Button::PageForward) || powerTurn || - input.wasPressed(MappedInputManager::Button::Right)) - : (input.wasReleased(MappedInputManager::Button::PageForward) || powerTurn || - input.wasReleased(MappedInputManager::Button::Right)); - return {prev, next}; + const bool next = tiltNext || (usePress ? (input.wasPressed(MappedInputManager::Button::PageForward) || powerTurn || + input.wasPressed(MappedInputManager::Button::Right)) + : (input.wasReleased(MappedInputManager::Button::PageForward) || powerTurn || + input.wasReleased(MappedInputManager::Button::Right))); + return {prev, next, tiltPrev || tiltNext}; } inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) { diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index fba7bc0b..597d52fc 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -71,7 +71,7 @@ void TxtReaderActivity::loop() { return; } - auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); + auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput); if (!prevTriggered && !nextTriggered) { return; } diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index d2b6f18e..a04100f7 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "CrossPointSettings.h" @@ -82,17 +83,20 @@ void XtcReaderActivity::loop() { // When long-press chapter skip is disabled, turn pages on press instead of release. const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip; - const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) - : (mappedInput.wasReleased(MappedInputManager::Button::PageBack) || - mappedInput.wasReleased(MappedInputManager::Button::Left)); + const bool tiltNext = SETTINGS.tiltPageTurn && halTiltSensor.wasTiltedForward(); + const bool tiltPrev = SETTINGS.tiltPageTurn && halTiltSensor.wasTiltedBack(); + const bool prevTriggered = + tiltPrev || (usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) || + mappedInput.wasPressed(MappedInputManager::Button::Left)) + : (mappedInput.wasReleased(MappedInputManager::Button::PageBack) || + mappedInput.wasReleased(MappedInputManager::Button::Left))); const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && mappedInput.wasReleased(MappedInputManager::Button::Power); - const bool nextTriggered = usePressForPageTurn - ? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn || - mappedInput.wasPressed(MappedInputManager::Button::Right)) - : (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn || - mappedInput.wasReleased(MappedInputManager::Button::Right)); + const bool nextTriggered = + tiltNext || (usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || + powerPageTurn || mappedInput.wasPressed(MappedInputManager::Button::Right)) + : (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || + powerPageTurn || mappedInput.wasReleased(MappedInputManager::Button::Right))); if (!prevTriggered && !nextTriggered) { return; @@ -109,7 +113,8 @@ void XtcReaderActivity::loop() { return; } - const bool skipPages = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipPageMs; + const bool fromTilt = tiltPrev || tiltNext; + const bool skipPages = !fromTilt && SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipPageMs; const int skipAmount = skipPages ? 10 : 1; if (prevTriggered) { diff --git a/src/main.cpp b/src/main.cpp index a3929d06..e2807816 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -186,6 +187,7 @@ void enterDeepSleep() { activityManager.goToSleep(); + halTiltSensor.deepSleep(); display.deepSleep(); LOG_DBG("MAIN", "Entering deep sleep"); @@ -231,6 +233,7 @@ void setup() { HalSystem::begin(); gpio.begin(); powerManager.begin(); + halTiltSensor.begin(); #ifdef ENABLE_SERIAL_LOG if (gpio.isUsbConnected()) { @@ -318,6 +321,7 @@ void loop() { static unsigned long lastMemPrint = 0; gpio.update(); + halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.orientation, activityManager.isReaderActivity()); renderer.setFadingFix(SETTINGS.fadingFix); @@ -346,7 +350,8 @@ void loop() { // Check for any user activity (button press or release) or active background work static unsigned long lastActivityTime = millis(); - if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || activityManager.preventAutoSleep()) { + if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || halTiltSensor.hadActivity() || + activityManager.preventAutoSleep()) { lastActivityTime = millis(); // Reset inactivity timer powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity }