Merge pull request #198 from jpirnay/feat-x3tilt
feat: Support X3 tilt for page movement
This commit is contained in:
@@ -305,6 +305,11 @@ STR_DIR_DOWN: "Down"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter"
|
||||
STR_FILTER_CONTRAST: "Contrast"
|
||||
STR_TILT_PAGE_TURN: "Tilt Page Turn"
|
||||
STR_TILT_STABILIZATION: "Tilt Mode"
|
||||
STR_TILT_MODE_RAW: "Raw Acceleration"
|
||||
STR_TILT_MODE_SMOOTH: "Smoothed Acceleration"
|
||||
STR_TILT_MODE_KALMAN: "Gyro/Accel Fusion"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Customise Status Bar"
|
||||
STR_CHAPTER_PAGE_COUNT: "Chapter Page Count"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "Book Progress Percentage"
|
||||
|
||||
@@ -251,6 +251,11 @@ STR_DIR_DOWN: "Bas"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_SLEEP_COVER_FILTER: "Filtre écran de veille"
|
||||
STR_FILTER_CONTRAST: "Contraste"
|
||||
STR_TILT_PAGE_TURN: "Changement de page par inclinaison"
|
||||
STR_TILT_STABILIZATION: "Mode inclinaison"
|
||||
STR_TILT_MODE_RAW: "Acceleration brute"
|
||||
STR_TILT_MODE_SMOOTH: "Acceleration lisse"
|
||||
STR_TILT_MODE_KALMAN: "Fusion gyroscope/accelerometre"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Personnaliser la barre d'état"
|
||||
STR_CHAPTER_PAGE_COUNT: "Nombre de pages du chapitre"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "Pourcentage de progression"
|
||||
|
||||
@@ -242,6 +242,11 @@ STR_DIR_DOWN: "Runter"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_SLEEP_COVER_FILTER: "Standby-Coverfilter"
|
||||
STR_FILTER_CONTRAST: "Kontrast"
|
||||
STR_TILT_PAGE_TURN: "Seitenblättern durch Neigen"
|
||||
STR_TILT_STABILIZATION: "Neigungsmodus"
|
||||
STR_TILT_MODE_RAW: "Rohbeschleunigung"
|
||||
STR_TILT_MODE_SMOOTH: "Geglättete Beschleunigung"
|
||||
STR_TILT_MODE_KALMAN: "Gyro-/Beschleunigungsfusion"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Statusleiste anpassen"
|
||||
STR_CHAPTER_PAGE_COUNT: "Kapitel-Seitenanzahl"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "Buchfortschritt in %"
|
||||
|
||||
@@ -251,6 +251,11 @@ STR_DIR_DOWN: "Giù"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_SLEEP_COVER_FILTER: "Filtro copertina"
|
||||
STR_FILTER_CONTRAST: "Contrasto"
|
||||
STR_TILT_PAGE_TURN: "Cambio pagina con inclinazione"
|
||||
STR_TILT_STABILIZATION: "Modalita inclinazione"
|
||||
STR_TILT_MODE_RAW: "Accelerazione grezza"
|
||||
STR_TILT_MODE_SMOOTH: "Accelerazione smussata"
|
||||
STR_TILT_MODE_KALMAN: "Fusione giroscopio/accelerometro"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Personalizza barra di stato"
|
||||
STR_CHAPTER_PAGE_COUNT: "Conteggio pagine capitolo"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "Percentuale avanzamento lettura"
|
||||
|
||||
@@ -434,6 +434,10 @@ STR_CRASH_DESCRIPTION: "Szczegółowy raport zapisany do crash_report.txt. Prosi
|
||||
STR_CRASH_REASON: "Powód awarii:"
|
||||
STR_CRASH_NO_REASON: "(Nie odnotowano powodu)"
|
||||
STR_TILT_PAGE_TURN: "Kartkowanie przechyłem"
|
||||
STR_TILT_STABILIZATION: "Tryb przechyłu"
|
||||
STR_TILT_MODE_RAW: "Surowe przyspieszenie"
|
||||
STR_TILT_MODE_SMOOTH: "Wygładzone przyspieszenie"
|
||||
STR_TILT_MODE_KALMAN: "Fuzja żyroskopu/akcelerometru"
|
||||
STR_RENDER_BENCHMARK: "Benchmark renderowania"
|
||||
STR_WEATHER: "Pogoda"
|
||||
STR_WEATHER_LOCATION: "Lokalizacja"
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
#include "HalTiltSensor.h"
|
||||
|
||||
#include <Logging.h>
|
||||
|
||||
HalTiltSensor halTiltSensor;
|
||||
|
||||
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, static_cast<uint8_t>(1));
|
||||
if (Wire.available() < 1) {
|
||||
return false;
|
||||
}
|
||||
*val = Wire.read();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HalTiltSensor::readReg16LE(uint8_t reg, int16_t* val) const {
|
||||
Wire.beginTransmission(_i2cAddr);
|
||||
Wire.write(reg);
|
||||
if (Wire.endTransmission(false) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (Wire.requestFrom(_i2cAddr, static_cast<uint8_t>(2), static_cast<uint8_t>(true)) < 2) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t lo = Wire.read();
|
||||
const uint8_t hi = Wire.read();
|
||||
*val = static_cast<int16_t>((static_cast<uint16_t>(hi) << 8) | lo);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HalTiltSensor::readAccel(float& ax, float& ay, float& az) const {
|
||||
int16_t rawAx = 0;
|
||||
int16_t rawAy = 0;
|
||||
int16_t rawAz = 0;
|
||||
if (!readReg16LE(REG_AX_L, &rawAx) || !readReg16LE(REG_AX_L + 2, &rawAy) || !readReg16LE(REG_AX_L + 4, &rawAz)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr float SCALE = 1.0f / 16384.0f;
|
||||
ax = rawAx * SCALE;
|
||||
ay = rawAy * SCALE;
|
||||
az = rawAz * SCALE;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HalTiltSensor::readAccelGyro(float& ax, float& ay, float& az, float& gx, float& gy, float& gz) const {
|
||||
int16_t rawAx = 0;
|
||||
int16_t rawAy = 0;
|
||||
int16_t rawAz = 0;
|
||||
int16_t rawGx = 0;
|
||||
int16_t rawGy = 0;
|
||||
int16_t rawGz = 0;
|
||||
if (!readReg16LE(REG_AX_L, &rawAx) || !readReg16LE(REG_AX_L + 2, &rawAy) || !readReg16LE(REG_AX_L + 4, &rawAz) ||
|
||||
!readReg16LE(REG_GYRO_X_L, &rawGx) || !readReg16LE(REG_GYRO_Y_L, &rawGy) || !readReg16LE(REG_GYRO_Z_L, &rawGz)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr float ACC_SCALE = 1.0f / 16384.0f;
|
||||
constexpr float GYRO_SCALE = 512.0f / 32768.0f;
|
||||
ax = rawAx * ACC_SCALE;
|
||||
ay = rawAy * ACC_SCALE;
|
||||
az = rawAz * ACC_SCALE;
|
||||
gx = rawGx * GYRO_SCALE;
|
||||
gy = rawGy * GYRO_SCALE;
|
||||
gz = rawGz * GYRO_SCALE;
|
||||
return true;
|
||||
}
|
||||
|
||||
void HalTiltSensor::begin() {
|
||||
if (!gpio.deviceIsX3()) {
|
||||
_available = false;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t whoami = 0;
|
||||
_i2cAddr = TILT_I2C_ADDR;
|
||||
if (!readReg(REG_WHO_AM_I, &whoami) || whoami != TILT_WHO_AM_I_VALUE) {
|
||||
_i2cAddr = TILT_I2C_ADDR_ALT;
|
||||
if (!readReg(REG_WHO_AM_I, &whoami) || whoami != TILT_WHO_AM_I_VALUE) {
|
||||
LOG_INF("TILT", "QMI8658 IMU not found");
|
||||
_available = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INF("TILT", "QMI8658 IMU found at 0x%02X", _i2cAddr);
|
||||
|
||||
if (!writeReg(REG_CTRL1, 0x40) || !writeReg(REG_CTRL2, CTRL2_2G_125HZ) || !writeReg(REG_CTRL3, CTRL3_512DPS_125HZ) ||
|
||||
!writeReg(REG_CTRL7, CTRL7_ACCEL_GYRO_EN)) {
|
||||
LOG_INF("TILT", "QMI8658 register configuration failed");
|
||||
_available = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_available = true;
|
||||
_lastPollMs = millis();
|
||||
_lastTiltMs = millis();
|
||||
_lastKalmanMicros = 0;
|
||||
_filterInitialized = false;
|
||||
_filterStartMs = millis();
|
||||
LOG_INF("TILT", "QMI8658 accelerometer initialized (±2g, 125 Hz) and gyro enabled");
|
||||
}
|
||||
|
||||
void HalTiltSensor::deepSleep() {
|
||||
if (!_available) {
|
||||
return;
|
||||
}
|
||||
clearPendingEvents();
|
||||
_inTilt = false;
|
||||
_filterInitialized = false;
|
||||
_lastKalmanMicros = 0;
|
||||
}
|
||||
|
||||
void HalTiltSensor::clearPendingEvents() {
|
||||
_tiltForwardEvent = false;
|
||||
_tiltBackEvent = false;
|
||||
_hadActivity = false;
|
||||
}
|
||||
|
||||
void HalTiltSensor::update(bool enabled, uint8_t mode, uint8_t orientation) {
|
||||
if (!enabled || !_available) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsigned long now = millis();
|
||||
if ((now - _lastPollMs) < POLL_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
_lastPollMs = now;
|
||||
|
||||
bool isAngleMode = mode == 2;
|
||||
float tiltValue = 0.0f;
|
||||
if (isAngleMode) {
|
||||
float ax, ay, az, gx, gy, gz;
|
||||
if (!readAccelGyro(ax, ay, az, gx, gy, gz)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float accRoll = atan2(ay, sqrt(ax * ax + az * az)) * (180.0f / PI);
|
||||
const float accPitch = atan2(-ax, sqrt(ay * ay + az * az)) * (180.0f / PI);
|
||||
|
||||
const unsigned long nowMicros = micros();
|
||||
const unsigned long deltaMicros = nowMicros - _lastKalmanMicros;
|
||||
float dt = 0.008f;
|
||||
if (_lastKalmanMicros != 0 && deltaMicros <= 150000u) {
|
||||
dt = static_cast<float>(deltaMicros) * 1e-6f;
|
||||
} else {
|
||||
_kalmanRoll.setAngle(accRoll);
|
||||
_kalmanPitch.setAngle(accPitch);
|
||||
}
|
||||
_lastKalmanMicros = nowMicros;
|
||||
|
||||
const float stableRoll = _kalmanRoll.update(accRoll, gx, dt);
|
||||
const float stablePitch = _kalmanPitch.update(accPitch, gy, dt);
|
||||
|
||||
switch (orientation) {
|
||||
case 0: // PORTRAIT
|
||||
tiltValue = stablePitch;
|
||||
break;
|
||||
case 2: // INVERTED
|
||||
tiltValue = -stablePitch;
|
||||
break;
|
||||
case 1: // LANDSCAPE_CW
|
||||
tiltValue = stableRoll;
|
||||
break;
|
||||
case 3: // LANDSCAPE_CCW
|
||||
tiltValue = -stableRoll;
|
||||
break;
|
||||
default:
|
||||
tiltValue = stablePitch;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
float ax, ay, az;
|
||||
if (!readAccel(ax, ay, az)) {
|
||||
return;
|
||||
}
|
||||
|
||||
float tiltAxis;
|
||||
switch (orientation) {
|
||||
case 0: // PORTRAIT
|
||||
tiltAxis = ay;
|
||||
break;
|
||||
case 2: // INVERTED
|
||||
tiltAxis = -ay;
|
||||
break;
|
||||
case 1: // LANDSCAPE_CW
|
||||
tiltAxis = ax;
|
||||
break;
|
||||
case 3: // LANDSCAPE_CCW
|
||||
tiltAxis = -ax;
|
||||
break;
|
||||
default:
|
||||
tiltAxis = ay;
|
||||
break;
|
||||
}
|
||||
|
||||
if (mode == 1) {
|
||||
if (!_filterInitialized) {
|
||||
_filteredAxis = tiltAxis;
|
||||
_filterInitialized = true;
|
||||
_filterStartMs = now;
|
||||
} else {
|
||||
_filteredAxis = _filteredAxis * (1.0f - FILTER_ALPHA) + tiltAxis * FILTER_ALPHA;
|
||||
}
|
||||
if ((now - _filterStartMs) < FILTER_WARMUP_MS) {
|
||||
return;
|
||||
}
|
||||
tiltAxis = _filteredAxis;
|
||||
}
|
||||
tiltValue = tiltAxis;
|
||||
}
|
||||
|
||||
if (_inTilt) {
|
||||
const float neutralThreshold = isAngleMode ? NEUTRAL_THRESHOLD_DEG : NEUTRAL_THRESHOLD_G;
|
||||
if (fabsf(tiltValue) < neutralThreshold) {
|
||||
_inTilt = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ((now - _lastTiltMs) < COOLDOWN_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tiltValue > (isAngleMode ? TILT_THRESHOLD_DEG : TILT_THRESHOLD_G)) {
|
||||
_tiltForwardEvent = true;
|
||||
_inTilt = true;
|
||||
_hadActivity = true;
|
||||
_lastTiltMs = now;
|
||||
} else if (tiltValue < (isAngleMode ? -TILT_THRESHOLD_DEG : -TILT_THRESHOLD_G)) {
|
||||
_tiltBackEvent = true;
|
||||
_inTilt = true;
|
||||
_hadActivity = true;
|
||||
_lastTiltMs = now;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#include "HalGPIO.h"
|
||||
|
||||
class HalTiltSensor;
|
||||
extern HalTiltSensor halTiltSensor;
|
||||
|
||||
class HalTiltSensor {
|
||||
bool _available = false;
|
||||
uint8_t _i2cAddr = 0;
|
||||
bool _tiltForwardEvent = false;
|
||||
bool _tiltBackEvent = false;
|
||||
bool _inTilt = false;
|
||||
bool _hadActivity = false;
|
||||
bool _filterInitialized = false;
|
||||
float _filteredAxis = 0.0f;
|
||||
unsigned long _filterStartMs = 0;
|
||||
unsigned long _lastTiltMs = 0;
|
||||
unsigned long _lastKalmanMicros = 0;
|
||||
mutable unsigned long _lastPollMs = 0;
|
||||
|
||||
static constexpr float TILT_THRESHOLD_G = 0.45f; // ~27° tilt to trigger
|
||||
static constexpr float NEUTRAL_THRESHOLD_G = 0.25f; // Must return below this before next trigger
|
||||
static constexpr float TILT_THRESHOLD_DEG = 27.0f; // Angle threshold for fused tilt detection
|
||||
static constexpr float NEUTRAL_THRESHOLD_DEG = 15.0f;
|
||||
static constexpr unsigned long COOLDOWN_MS = 600; // Minimum ms between triggers
|
||||
static constexpr unsigned long POLL_INTERVAL_MS = 50; // 20 Hz polling
|
||||
static constexpr float FILTER_ALPHA = 0.25f; // Smoothing factor for axis stabilization
|
||||
static constexpr unsigned long FILTER_WARMUP_MS = 300; // Stabilization warmup after first sample
|
||||
|
||||
static constexpr uint8_t TILT_I2C_ADDR = 0x6A;
|
||||
static constexpr uint8_t TILT_I2C_ADDR_ALT = 0x6B;
|
||||
static constexpr uint8_t TILT_WHO_AM_I_VALUE = 0x05;
|
||||
static constexpr uint8_t REG_WHO_AM_I = 0x00;
|
||||
static constexpr uint8_t REG_CTRL1 = 0x02;
|
||||
static constexpr uint8_t REG_CTRL2 = 0x03;
|
||||
static constexpr uint8_t REG_CTRL3 = 0x04;
|
||||
static constexpr uint8_t REG_CTRL7 = 0x08;
|
||||
static constexpr uint8_t REG_AX_L = 0x35;
|
||||
static constexpr uint8_t REG_GYRO_X_L = 0x3B;
|
||||
static constexpr uint8_t REG_GYRO_Y_L = 0x3D;
|
||||
static constexpr uint8_t REG_GYRO_Z_L = 0x3F;
|
||||
|
||||
static constexpr uint8_t CTRL2_2G_125HZ = 0x06;
|
||||
static constexpr uint8_t CTRL3_512DPS_125HZ = 0x45;
|
||||
static constexpr uint8_t CTRL7_ACCEL_GYRO_EN = 0x03;
|
||||
|
||||
struct KalmanFilter {
|
||||
KalmanFilter() {
|
||||
Q_angle = 0.001f;
|
||||
Q_bias = 0.003f;
|
||||
R_measure = 0.03f;
|
||||
angle = 0.0f;
|
||||
bias = 0.0f;
|
||||
P[0][0] = 0.0f;
|
||||
P[0][1] = 0.0f;
|
||||
P[1][0] = 0.0f;
|
||||
P[1][1] = 0.0f;
|
||||
}
|
||||
|
||||
float update(float newAngle, float newRate, float dt) {
|
||||
float rate = newRate - bias;
|
||||
angle += dt * rate;
|
||||
|
||||
P[0][0] += dt * (dt * P[1][1] - P[0][1] - P[1][0] + Q_angle);
|
||||
P[0][1] -= dt * P[1][1];
|
||||
P[1][0] -= dt * P[1][1];
|
||||
P[1][1] += Q_bias * dt;
|
||||
|
||||
float S = P[0][0] + R_measure;
|
||||
float K0 = P[0][0] / S;
|
||||
float K1 = P[1][0] / S;
|
||||
|
||||
float y = newAngle - angle;
|
||||
angle += K0 * y;
|
||||
bias += K1 * y;
|
||||
|
||||
float P00_temp = P[0][0];
|
||||
float P01_temp = P[0][1];
|
||||
|
||||
P[0][0] -= K0 * P00_temp;
|
||||
P[0][1] -= K0 * P01_temp;
|
||||
P[1][0] -= K1 * P00_temp;
|
||||
P[1][1] -= K1 * P01_temp;
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
void setAngle(float a) { angle = a; }
|
||||
|
||||
private:
|
||||
float Q_angle;
|
||||
float Q_bias;
|
||||
float R_measure;
|
||||
float angle;
|
||||
float bias;
|
||||
float P[2][2];
|
||||
};
|
||||
|
||||
KalmanFilter _kalmanRoll;
|
||||
KalmanFilter _kalmanPitch;
|
||||
|
||||
bool writeReg(uint8_t reg, uint8_t val) const;
|
||||
bool readReg(uint8_t reg, uint8_t* val) const;
|
||||
bool readReg16LE(uint8_t reg, int16_t* val) const;
|
||||
bool readAccel(float& ax, float& ay, float& az) const;
|
||||
bool readAccelGyro(float& ax, float& ay, float& az, float& gx, float& gy, float& gz) const;
|
||||
|
||||
public:
|
||||
void begin();
|
||||
void deepSleep();
|
||||
void update(bool enabled, uint8_t mode, uint8_t orientation);
|
||||
|
||||
bool wasTiltedForward();
|
||||
bool wasTiltedBack();
|
||||
bool hadActivity();
|
||||
|
||||
bool isAvailable() const { return _available; }
|
||||
void clearPendingEvents();
|
||||
};
|
||||
@@ -137,6 +137,14 @@ class CrossPointSettings {
|
||||
// Image rendering in EPUB reader
|
||||
enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT };
|
||||
|
||||
// Action mapped to each tilt gesture direction.
|
||||
enum TILT_GESTURE_ACTION {
|
||||
TILT_ACT_NONE = 0,
|
||||
TILT_ACT_NEXT_PAGE = 1,
|
||||
TILT_ACT_PREV_PAGE = 2,
|
||||
TILT_GESTURE_ACTION_COUNT
|
||||
};
|
||||
|
||||
// Text darkness for AA glyph rendering (forwarded to GfxRenderer::setTextDarkness)
|
||||
enum TEXT_DARKNESS {
|
||||
DARKNESS_NORMAL = 0, // true 4-level AA
|
||||
@@ -257,6 +265,14 @@ class CrossPointSettings {
|
||||
uint8_t imageRendering = IMAGES_DISPLAY;
|
||||
// Dithering mode for decoded images (EPUB/JPG/PNG)
|
||||
uint8_t imageDithering = IMAGE_DITHER_BAYER;
|
||||
// Tilt-based page turning (X3 only — requires QMI8658 IMU)
|
||||
uint8_t tiltPageTurn = 0;
|
||||
// Tilt input processing mode: 0 = raw accel, 1 = smoothed accel, 2 = gyro/accelerometer fusion
|
||||
uint8_t tiltStabilization = 0;
|
||||
// Action when the computed tilt value crosses the positive threshold.
|
||||
uint8_t tiltPositiveAction = TILT_ACT_NEXT_PAGE;
|
||||
// Action when the computed tilt value crosses the negative threshold.
|
||||
uint8_t tiltNegativeAction = TILT_ACT_PREV_PAGE;
|
||||
// Enable synthetic TOC fallback for malformed/sparse TOC books (1 = enabled, 0 = disabled)
|
||||
uint8_t syntheticTocFallback = 1;
|
||||
// Default bionic reading in EPUB pages when no per-book override is set (1 = enabled, 0 = disabled)
|
||||
|
||||
@@ -180,8 +180,9 @@ bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) {
|
||||
|
||||
bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path) {
|
||||
JsonDocument doc;
|
||||
const auto settings = getSettingsList();
|
||||
|
||||
for (const auto& info : getSettingsList()) {
|
||||
for (const auto& info : settings) {
|
||||
if (!info.key) continue;
|
||||
// Dynamic entries (KOReader etc.) are stored in their own files — skip.
|
||||
if (!info.valuePtr && !info.stringOffset) continue;
|
||||
@@ -258,8 +259,9 @@ bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool*
|
||||
migrateMissingStatusSetting("statusBarItemsPosition", s.statusBarItemsPosition, "statusBarItemsPosition",
|
||||
CrossPointSettings::STATUS_BAR_ITEMS_BOTTOM,
|
||||
CrossPointSettings::STATUS_BAR_ITEMS_POSITION_COUNT);
|
||||
const auto settings = getSettingsList();
|
||||
|
||||
for (const auto& info : getSettingsList()) {
|
||||
for (const auto& info : settings) {
|
||||
if (!info.key) continue;
|
||||
// Dynamic entries (KOReader etc.) are stored in their own files — skip.
|
||||
if (!info.valuePtr && !info.stringOffset) continue;
|
||||
|
||||
+39
-1
@@ -1,7 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <HalGPIO.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
@@ -25,6 +28,7 @@
|
||||
// works inside a submenu exactly as it does in the parent tab.
|
||||
// Add with .withSubmenu(StrId::STR_MY_SUBMENU).
|
||||
// key — JSON property name used by the web settings API (nullptr = device-only).
|
||||
// deviceTarget — hardware visibility (BOTH by default; override with .withDeviceTarget()).
|
||||
//
|
||||
// ACTION-type entries and entries without a key are device-only and are added directly
|
||||
// in SettingsActivity::onEnter(), not here.
|
||||
@@ -222,6 +226,25 @@ inline const std::vector<SettingInfo> list = {
|
||||
SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongPower, {StrId::STR_BTN_DEF_SLEEP},
|
||||
"btnLongPower", StrId::STR_CAT_CONTROLS)
|
||||
.withSubmenu(StrId::STR_BTN_POWER),
|
||||
SettingInfo::Toggle(StrId::STR_TILT_PAGE_TURN, &CrossPointSettings::tiltPageTurn, "tiltPageTurn",
|
||||
StrId::STR_CAT_CONTROLS)
|
||||
.withSubmenu(StrId::STR_TILT_PAGE_TURN)
|
||||
.withDeviceTarget(SettingDeviceTarget::X3),
|
||||
SettingInfo::Enum(StrId::STR_TILT_STABILIZATION, &CrossPointSettings::tiltStabilization,
|
||||
{StrId::STR_TILT_MODE_RAW, StrId::STR_TILT_MODE_SMOOTH, StrId::STR_TILT_MODE_KALMAN},
|
||||
"tiltStabilization", StrId::STR_CAT_CONTROLS)
|
||||
.withSubmenu(StrId::STR_TILT_PAGE_TURN)
|
||||
.withDeviceTarget(SettingDeviceTarget::X3),
|
||||
SettingInfo::Enum(StrId::STR_DIR_RIGHT, &CrossPointSettings::tiltPositiveAction,
|
||||
{StrId::STR_NONE_OPT, StrId::STR_NEXT_PAGE, StrId::STR_PREV_PAGE}, "tiltPositiveAction",
|
||||
StrId::STR_CAT_CONTROLS)
|
||||
.withSubmenu(StrId::STR_TILT_PAGE_TURN)
|
||||
.withDeviceTarget(SettingDeviceTarget::X3),
|
||||
SettingInfo::Enum(StrId::STR_DIR_LEFT, &CrossPointSettings::tiltNegativeAction,
|
||||
{StrId::STR_NONE_OPT, StrId::STR_NEXT_PAGE, StrId::STR_PREV_PAGE}, "tiltNegativeAction",
|
||||
StrId::STR_CAT_CONTROLS)
|
||||
.withSubmenu(StrId::STR_TILT_PAGE_TURN)
|
||||
.withDeviceTarget(SettingDeviceTarget::X3),
|
||||
|
||||
#undef BTN_ACT_OPTIONS
|
||||
|
||||
@@ -322,4 +345,19 @@ inline const std::vector<SettingInfo> list = {
|
||||
};
|
||||
} // namespace SettingsListDetail
|
||||
|
||||
inline const std::vector<SettingInfo>& getSettingsList() { return SettingsListDetail::list; }
|
||||
inline std::vector<SettingInfo> getSettingsList() {
|
||||
std::vector<SettingInfo> settings = SettingsListDetail::list;
|
||||
const bool isX3 = gpio.deviceIsX3();
|
||||
settings.erase(std::remove_if(settings.begin(), settings.end(),
|
||||
[isX3](const SettingInfo& setting) {
|
||||
if (setting.deviceTarget == SettingDeviceTarget::BOTH) {
|
||||
return false;
|
||||
}
|
||||
if (setting.deviceTarget == SettingDeviceTarget::X3) {
|
||||
return !isX3;
|
||||
}
|
||||
return isX3;
|
||||
}),
|
||||
settings.end());
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <CrossPointSettings.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalTiltSensor.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <cstdint>
|
||||
@@ -94,18 +95,39 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
|
||||
// Also suppress immediate page-turns if a double-click action is configured for the button,
|
||||
// because the button event system delays short events until the double-click window expires.
|
||||
using BA = CrossPointSettings::BUTTON_ACTION;
|
||||
const bool prev = (SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
||||
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageBack) &&
|
||||
input.wasReleased(MappedInputManager::Button::PageBack)) ||
|
||||
(SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
||||
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Left) &&
|
||||
input.wasReleased(MappedInputManager::Button::Left));
|
||||
const bool next = (SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
||||
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageForward) &&
|
||||
input.wasReleased(MappedInputManager::Button::PageForward)) ||
|
||||
(SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
||||
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Right) &&
|
||||
input.wasReleased(MappedInputManager::Button::Right));
|
||||
using TA = CrossPointSettings::TILT_GESTURE_ACTION;
|
||||
const bool tiltNegative = SETTINGS.tiltPageTurn && halTiltSensor.wasTiltedBack();
|
||||
const bool tiltPositive = SETTINGS.tiltPageTurn && halTiltSensor.wasTiltedForward();
|
||||
bool tiltPrev = false;
|
||||
bool tiltNext = false;
|
||||
auto applyTiltAction = [&](uint8_t action) {
|
||||
if (action == TA::TILT_ACT_NEXT_PAGE) {
|
||||
tiltNext = true;
|
||||
} else if (action == TA::TILT_ACT_PREV_PAGE) {
|
||||
tiltPrev = true;
|
||||
}
|
||||
};
|
||||
if (tiltPositive) {
|
||||
applyTiltAction(SETTINGS.tiltPositiveAction);
|
||||
}
|
||||
if (tiltNegative) {
|
||||
applyTiltAction(SETTINGS.tiltNegativeAction);
|
||||
}
|
||||
const bool prevButtonReleased = (SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
||||
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageBack) &&
|
||||
input.wasReleased(MappedInputManager::Button::PageBack)) ||
|
||||
(SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
||||
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Left) &&
|
||||
input.wasReleased(MappedInputManager::Button::Left));
|
||||
const bool nextButtonReleased = (SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
||||
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageForward) &&
|
||||
input.wasReleased(MappedInputManager::Button::PageForward)) ||
|
||||
(SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
||||
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Right) &&
|
||||
input.wasReleased(MappedInputManager::Button::Right));
|
||||
|
||||
const bool prev = tiltPrev || prevButtonReleased;
|
||||
const bool next = tiltNext || nextButtonReleased;
|
||||
return {prev, next};
|
||||
}
|
||||
|
||||
|
||||
@@ -140,7 +140,13 @@ void TxtReaderActivity::onExit() {
|
||||
}
|
||||
|
||||
void TxtReaderActivity::loop() {
|
||||
if (inputDrainGuard.shouldDrain(mappedInput)) {
|
||||
const bool drainActive = inputDrainGuard.shouldDrain(mappedInput);
|
||||
if (!drainActive && drainWasActive) {
|
||||
halTiltSensor.clearPendingEvents();
|
||||
}
|
||||
drainWasActive = drainActive;
|
||||
|
||||
if (drainActive) {
|
||||
buttonEvents.drain();
|
||||
return;
|
||||
}
|
||||
@@ -196,6 +202,25 @@ void TxtReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevTriggered) {
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
requestUpdate();
|
||||
}
|
||||
} else if (nextTriggered) {
|
||||
if (currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
requestUpdate();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TxtReaderActivity::initializeReader() {
|
||||
|
||||
@@ -16,6 +16,7 @@ class TxtReaderActivity final : public Activity {
|
||||
int totalPages = 1;
|
||||
int pagesUntilFullRefresh = 0;
|
||||
ReaderUtils::InputDrainGuard inputDrainGuard;
|
||||
bool drainWasActive = false;
|
||||
|
||||
// Bookmarks (starred pages)
|
||||
BookmarkStore bookmarkStore;
|
||||
|
||||
@@ -30,18 +30,12 @@ constexpr std::array<ButtonRow, 7> kButtonRows = {{
|
||||
}};
|
||||
|
||||
// Find the SettingInfo for a given (button submenu, press kind) pair in the shared settings list.
|
||||
const SettingInfo* findEntry(StrId submenu, StrId pressKind) {
|
||||
const auto& list = getSettingsList();
|
||||
auto it = std::find_if(list.begin(), list.end(), [submenu, pressKind](const SettingInfo& s) {
|
||||
std::string cellValue(const std::vector<SettingInfo>& settings, StrId submenu, StrId pressKind) {
|
||||
auto it = std::find_if(settings.begin(), settings.end(), [submenu, pressKind](const SettingInfo& s) {
|
||||
return s.submenu == submenu && s.nameId == pressKind && s.category == StrId::STR_CAT_CONTROLS;
|
||||
});
|
||||
return it != list.end() ? &*it : nullptr;
|
||||
}
|
||||
|
||||
std::string cellValue(StrId submenu, StrId pressKind) {
|
||||
const SettingInfo* s = findEntry(submenu, pressKind);
|
||||
if (!s) return {};
|
||||
return s->getDisplayValue();
|
||||
if (it == settings.end()) return {};
|
||||
return it->getDisplayValue();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -129,15 +123,17 @@ void ButtonActionsOverviewActivity::render(RenderLock&&) {
|
||||
renderer.fillPolygon(underlineX, underlineY, 4, true);
|
||||
y += 4;
|
||||
|
||||
const auto settings = getSettingsList();
|
||||
|
||||
// Data rows
|
||||
for (const auto& row : kButtonRows) {
|
||||
const std::string label = I18N.get(row.labelStrId);
|
||||
const std::string clippedLabel = renderer.truncatedText(fontId, label.c_str(), colW[0], EpdFontFamily::BOLD);
|
||||
renderer.drawText(fontId, colX[0], y, clippedLabel.c_str(), true, EpdFontFamily::BOLD);
|
||||
|
||||
const std::string vShort = cellValue(row.submenu, StrId::STR_BTN_SHORT_PRESS);
|
||||
const std::string vDouble = cellValue(row.submenu, StrId::STR_BTN_DOUBLE_PRESS);
|
||||
const std::string vLong = cellValue(row.submenu, StrId::STR_BTN_LONG_PRESS);
|
||||
const std::string vShort = cellValue(settings, row.submenu, StrId::STR_BTN_SHORT_PRESS);
|
||||
const std::string vDouble = cellValue(settings, row.submenu, StrId::STR_BTN_DOUBLE_PRESS);
|
||||
const std::string vLong = cellValue(settings, row.submenu, StrId::STR_BTN_LONG_PRESS);
|
||||
|
||||
renderer.drawText(fontId, colX[1], y, renderer.truncatedText(fontId, vShort.c_str(), colW[1]).c_str());
|
||||
renderer.drawText(fontId, colX[2], y, renderer.truncatedText(fontId, vDouble.c_str(), colW[2]).c_str());
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING };
|
||||
|
||||
enum class SettingDeviceTarget { BOTH, X3, X4 };
|
||||
|
||||
enum class SettingAction {
|
||||
None,
|
||||
RemapFrontButtons,
|
||||
@@ -56,6 +58,7 @@ struct SettingInfo {
|
||||
const char* key = nullptr; // JSON API key (nullptr for ACTION types)
|
||||
StrId category = StrId::STR_NONE_OPT; // Category for web UI grouping
|
||||
bool obfuscated = false; // Save/load via base64 obfuscation (passwords)
|
||||
SettingDeviceTarget deviceTarget = SettingDeviceTarget::BOTH;
|
||||
|
||||
// Direct char[] string fields (for settings stored in CrossPointSettings)
|
||||
size_t stringOffset = 0;
|
||||
@@ -234,6 +237,13 @@ struct SettingInfo {
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Restrict visibility of this setting to a specific hardware target.
|
||||
// Default is BOTH when this method is not used.
|
||||
SettingInfo& withDeviceTarget(SettingDeviceTarget target) {
|
||||
deviceTarget = target;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// For internal use by SettingsActivity: placeholder entry that launches the submenu.
|
||||
static SettingInfo SubmenuEntry(StrId titleId) {
|
||||
SettingInfo s;
|
||||
|
||||
@@ -54,7 +54,7 @@ void SettingsActivity::onEnter() {
|
||||
}
|
||||
vec.push_back(s);
|
||||
};
|
||||
auto addToMoved = [](std::vector<SettingInfo>& vec, StrId& lastSub, SettingInfo&& s) {
|
||||
auto addToMoved = [](std::vector<SettingInfo>& vec, StrId& lastSub, SettingInfo s) {
|
||||
if (s.subcategory != StrId::STR_NONE_OPT && s.subcategory != lastSub) {
|
||||
vec.push_back(SettingInfo::Separator(s.subcategory));
|
||||
lastSub = s.subcategory;
|
||||
|
||||
+7
-1
@@ -9,6 +9,7 @@
|
||||
#include <HalPowerManager.h>
|
||||
#include <HalStorage.h>
|
||||
#include <HalSystem.h>
|
||||
#include <HalTiltSensor.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <SPI.h>
|
||||
@@ -141,6 +142,7 @@ void enterDeepSleep() {
|
||||
APP_STATE.saveToFile();
|
||||
|
||||
activityManager.goToSleep();
|
||||
halTiltSensor.deepSleep();
|
||||
|
||||
display.deepSleep();
|
||||
LOG_DBG("MAIN", "Entering deep sleep (powerBtn isPressed=%d, rawPin=%d)", gpio.isPressed(HalGPIO::BTN_POWER),
|
||||
@@ -200,6 +202,7 @@ void setup() {
|
||||
HalSystem::begin();
|
||||
gpio.begin();
|
||||
powerManager.begin();
|
||||
halTiltSensor.begin();
|
||||
gpio_deep_sleep_hold_dis(); // Release deep sleep GPIO hold state from previous sleep cycle
|
||||
|
||||
const auto wakeupReason = gpio.getWakeupReason();
|
||||
@@ -207,6 +210,7 @@ void setup() {
|
||||
if (wakeupReason == HalGPIO::WakeupReason::AfterUSBPower) {
|
||||
// If USB power caused a cold boot, go back to sleep immediately without initializing subsystems
|
||||
LOG_DBG("MAIN", "Wakeup reason: After USB Power => Deep sleep");
|
||||
halTiltSensor.deepSleep();
|
||||
powerManager.startDeepSleep(gpio);
|
||||
return;
|
||||
}
|
||||
@@ -331,6 +335,7 @@ void loop() {
|
||||
gpio.update();
|
||||
buttonEventManager.update();
|
||||
HalClock::updatePeriodic();
|
||||
halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.tiltStabilization, SETTINGS.orientation);
|
||||
|
||||
renderer.setFadingFix(SETTINGS.fadingFix);
|
||||
renderer.setTextDarkness(SETTINGS.textDarkness);
|
||||
@@ -362,7 +367,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
|
||||
}
|
||||
|
||||
@@ -1232,7 +1232,7 @@ void CrossPointWebServer::handleSettingsPage() const {
|
||||
}
|
||||
|
||||
void CrossPointWebServer::handleGetSettings() const {
|
||||
const auto& settings = getSettingsList();
|
||||
const auto settings = getSettingsList();
|
||||
|
||||
String result;
|
||||
result.reserve(4096);
|
||||
@@ -1351,7 +1351,7 @@ void CrossPointWebServer::handlePostSettings() {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& settings = getSettingsList();
|
||||
const auto settings = getSettingsList();
|
||||
int applied = 0;
|
||||
|
||||
for (const auto& s : settings) {
|
||||
|
||||
Reference in New Issue
Block a user