#include "HalTiltSensor.h" #include HalTiltSensor halTiltSensor; // Singleton instance bool HalTiltSensor::readGyro(float& gx, float& gy, float& gz) const { Imu::Sample sample; if (!_sdkImu.read(sample)) return false; gx = sample.gx; gy = sample.gy; gz = sample.gz; return true; } void HalTiltSensor::begin() { _available = _sdkImu.begin(); if (_available) { _initMs = millis(); _lastPollMs = millis(); // begin() leaves the sensors sampling; stand them by until tilt page turn // actually wakes them, so a disabled IMU doesn't drain the battery. if (!_sdkImu.sleep()) { LOG_ERR("GYR", "IMU standby failed"); } LOG_INF("GYR", "SDK IMU initialized"); return; } LOG_ERR("GYR", "SDK IMU not found"); } bool HalTiltSensor::wake() { if (!_available) { return false; } if (!_sdkImu.wake()) { LOG_ERR("GYR", "IMU wake failed"); return false; } _lastPollMs = millis(); _lastTiltMs = millis(); _wakeMs = millis(); _isAwake = true; return true; } bool HalTiltSensor::deepSleep() { if (!_available) { return false; } if (!_sdkImu.sleep()) { LOG_ERR("GYR", "IMU sleep failed"); return false; } clearPendingEvents(); _inTilt = false; _isAwake = false; return true; } 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 }