diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 400aabc676..7f35bf41e3 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -167,6 +167,13 @@ static void lsIdle() if (pressed) { powerFSM.trigger(EVENT_PRESS); } +#ifdef MOTION_WAKE_INT_PIN + // Not the button: the accelerometer can have raised the line instead. + else if (config.display.wake_on_tap_or_motion && + digitalRead(MOTION_WAKE_INT_PIN) == (MOTION_WAKE_INT_ACTIVE_HIGH ? HIGH : LOW)) { + powerFSM.trigger(EVENT_INPUT); + } +#endif break; } default: diff --git a/src/configuration.h b/src/configuration.h index 2018fe678c..45e18bf6f0 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -642,6 +642,33 @@ along with this program. If not, see . #define HAS_SCREEN 0 #endif +// ----------------------------------------------------------------------------- +// Motion sensor wake +// ----------------------------------------------------------------------------- + +/* The motion driver that owns this pin attaches the ISR. sleep.cpp reuses it as a + light-sleep wake source and PowerFSM attributes the resulting GPIO wake to motion. + Must stay below the exclusion cascade: MESHTASTIC_MINIMIZE_BUILD derives + MESHTASTIC_EXCLUDE_I2C above, and no motion driver is built when it is set. */ +#if !MESHTASTIC_EXCLUDE_I2C +#if defined(BMA4XX_INT) && defined(HAS_BMA423) +#define MOTION_WAKE_INT_PIN BMA4XX_INT +#define MOTION_WAKE_INT_ACTIVE_HIGH 1 +#elif defined(BHI260AP_INT) && defined(HAS_BHI260AP) +#define MOTION_WAKE_INT_PIN BHI260AP_INT +#define MOTION_WAKE_INT_ACTIVE_HIGH 1 +#elif defined(STK8XXX_INT) && defined(HAS_STK8XXX) +#define MOTION_WAKE_INT_PIN STK8XXX_INT +#define MOTION_WAKE_INT_ACTIVE_HIGH 1 +#elif defined(ICM_20948_INT_PIN) && defined(HAS_ICM20948) +#define MOTION_WAKE_INT_PIN ICM_20948_INT_PIN +#define MOTION_WAKE_INT_ACTIVE_HIGH 0 +#elif defined(QMA_6100P_INT_PIN) && defined(HAS_QMA6100P) +#define MOTION_WAKE_INT_PIN QMA_6100P_INT_PIN +#define MOTION_WAKE_INT_ACTIVE_HIGH 0 +#endif +#endif + #ifndef USE_ETHERNET_DEFAULT #define USE_ETHERNET_DEFAULT 0 #endif diff --git a/src/motion/BHI260APSensor.cpp b/src/motion/BHI260APSensor.cpp index c9efda96c6..bf5290dce3 100644 --- a/src/motion/BHI260APSensor.cpp +++ b/src/motion/BHI260APSensor.cpp @@ -3,8 +3,19 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BHI260AP) && __has_include() #define BOSCH_BHI260_KLIO +#include "mesh/Throttle.h" #include + +#ifdef BHI260AP_INT +static volatile bool BHI_IRQ = false; +#endif + BHI260APSensor::BHI260APSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} + +void BHI260APSensor::onWristTilt(uint8_t, const uint8_t *, uint32_t, uint64_t *, void *user_data) +{ + static_cast(user_data)->wakeRequested = true; +} // https://github.com/lewisxhe/SensorLib/blob/master/examples/Sensors/IMU/BHI260AP_InterruptSettings/BHI260AP_InterruptSettings.ino bool BHI260APSensor::init() @@ -29,18 +40,25 @@ bool BHI260APSensor::init() // sensor.configAccelerometer(sensor.RANGE_2G, sensor.ODR_100HZ, sensor.BW_NORMAL_AVG4, sensor.PERF_CONTINUOUS_MODE); // sensor.enableAccelerometer(); - // sensor.configInterrupt(); #ifdef BHI260AP_INT + // Defaults: active-high, level-triggered, push-pull, FIFO sources unmasked. + InterruptConfig intConfig; + sensor.configureInterrupt(intConfig); pinMode(BHI260AP_INT, INPUT); attachInterrupt( - BHI260AP_INT, - [] { - // Set interrupt to set irq value to true - }, - RISING); // Select the interrupt mode according to the actual circuit + BHI260AP_INT, [] { BHI_IRQ = true; }, RISING); #endif + // Wrist tilt wakes the screen. Not every firmware image ships it and SensorAnyMotion + // is BHI360-only, so fall back to step counting alone. + constexpr uint8_t wristTilt = static_cast(BoschSensorID::WRIST_TILT_GESTURE); + if (sensor.onResultEvent(wristTilt, onWristTilt, this) && sensor.configure(wristTilt, 1.0f, 0)) { + LOG_DEBUG("BHI260AP wrist tilt wake enabled"); + } else { + LOG_WARN("BHI260AP firmware has no wrist tilt gesture, motion wake unavailable"); + } + // stepDetector->enable(1.0, 0); stepCounter->enable(1.0, 0); LOG_DEBUG("BHI260AP init ok"); @@ -52,6 +70,14 @@ bool BHI260APSensor::init() int32_t BHI260APSensor::runOnce() { +#ifdef BHI260AP_INT + // The INT line is the fast path; the keepalive keeps the step counter alive without it. + if (!BHI_IRQ && !Throttle::hasElapsed(lastPollMs, MOTION_SENSOR_IRQ_KEEPALIVE_MS)) + return MOTION_SENSOR_CHECK_INTERVAL_MS; + BHI_IRQ = false; + lastPollMs = millis(); +#endif + sensor.update(); if (stepCounter->hasUpdated()) { steps = stepCounter->getStepCount(); @@ -59,14 +85,16 @@ int32_t BHI260APSensor::runOnce() if (screen) screen->steps = steps; } - // LOG_WARN("Step count: %u", stepCounter->getStepCount()); - // if (sensor.readIrqStatus()) { - // if (sensor.isTilt() || sensor.isDoubleTap()) { - // wakeScreen(); - // return 500; - // } - //} + if (wakeRequested) { + wakeRequested = false; + wakeScreen(); + } +#ifdef BHI260AP_INT + // Tick fast for tilt latency; without the INT line every tick would be an I2C drain. + return MOTION_SENSOR_CHECK_INTERVAL_MS; +#else return 1000; +#endif } -#endif \ No newline at end of file +#endif diff --git a/src/motion/BHI260APSensor.h b/src/motion/BHI260APSensor.h index b0a4064872..ec8163c45e 100644 --- a/src/motion/BHI260APSensor.h +++ b/src/motion/BHI260APSensor.h @@ -15,10 +15,16 @@ class BHI260APSensor : public MotionSensor { private: SensorBHI260AP sensor; - volatile bool BHI_IRQ = false; SensorStepCounter *stepCounter; SensorStepDetector *stepDetector; uint32_t steps = 0; + bool wakeRequested = false; +#ifdef BHI260AP_INT + uint32_t lastPollMs = 0; +#endif + + // Fires from sensor.update() when the fusion hub reports a wrist tilt. + static void onWristTilt(uint8_t sensor_id, const uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data); public: explicit BHI260APSensor(ScanI2C::FoundDevice foundDevice); diff --git a/src/motion/BMA423Sensor.cpp b/src/motion/BMA423Sensor.cpp index 98491311b9..6271d70caa 100755 --- a/src/motion/BMA423Sensor.cpp +++ b/src/motion/BMA423Sensor.cpp @@ -2,6 +2,12 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BMA423) && __has_include() +#include "mesh/Throttle.h" + +#ifdef BMA4XX_INT +static volatile bool BMA_IRQ = false; +#endif + BMA423Sensor::BMA423Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} bool BMA423Sensor::init() @@ -24,6 +30,13 @@ bool BMA423Sensor::init() sensor.setRemapAxes(SensorRemap::BOTTOM_LAYER_BOTTOM_LEFT_CORNER); #endif +#ifdef BMA4XX_INT + // enableTiltDetector()/enableTapDetector() only map the feature onto INT1, and the BMA4 + // reset default leaves that pin's output driver off. Arm it push-pull active-high. + if (!sensor.setInterruptPinConfig(InterruptPinMap::PIN1, false, false, true, false)) + LOG_DEBUG("BMA423 INT1 pin config failed, keeping the polled path"); // not fatal +#endif + // The tap detector defaults to double tap; tilt and double tap both wake the screen. sensor.setOnTiltDetectedCallback([this] { wakeRequested = true; }); sensor.setOnTapCallback([this](TapType) { wakeRequested = true; }); @@ -32,12 +45,26 @@ bool BMA423Sensor::init() return false; } +#ifdef BMA4XX_INT + pinMode(BMA4XX_INT, INPUT); + attachInterrupt( + BMA4XX_INT, [] { BMA_IRQ = true; }, RISING); +#endif + LOG_DEBUG("BMA423 init ok"); return true; } int32_t BMA423Sensor::runOnce() { +#ifdef BMA4XX_INT + // INT1 is the fast path; update() reads and clears the status register and fires the callbacks. + if (!BMA_IRQ && !Throttle::hasElapsed(lastPollMs, MOTION_SENSOR_IRQ_KEEPALIVE_MS)) + return MOTION_SENSOR_CHECK_INTERVAL_MS; + BMA_IRQ = false; + lastPollMs = millis(); +#endif + wakeRequested = false; sensor.update(); if (wakeRequested) { diff --git a/src/motion/BMA423Sensor.h b/src/motion/BMA423Sensor.h index 7ce5525c66..512457daf9 100755 --- a/src/motion/BMA423Sensor.h +++ b/src/motion/BMA423Sensor.h @@ -14,6 +14,9 @@ class BMA423Sensor : public MotionSensor private: SensorBMA423 sensor; bool wakeRequested = false; +#ifdef BMA4XX_INT + uint32_t lastPollMs = 0; +#endif public: explicit BMA423Sensor(ScanI2C::FoundDevice foundDevice); diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index 8238b3dbc9..a05f9aeca7 100644 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -2,6 +2,7 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() #include "detect/ScanI2CTwoWire.h" +#include "mesh/Throttle.h" #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // screen is defined in main.cpp @@ -34,21 +35,6 @@ bool ICM20948Sensor::init() return wakeOnMotionOk; } -#ifdef ICM_20948_INT_PIN - -int32_t ICM20948Sensor::runOnce() -{ - // Wake on motion using hardware interrupts - this is the most efficient way to check for motion - if (ICM20948_IRQ) { - ICM20948_IRQ = false; - sensor->clearInterrupts(); - wakeScreen(); - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; -} - -#else - int32_t ICM20948Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN @@ -105,7 +91,21 @@ int32_t ICM20948Sensor::runOnce() screen->setHeading(heading); #endif - // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) +#ifdef ICM_20948_INT_PIN + if (ICM20948_IRQ) { + ICM20948_IRQ = false; + intPinProven = true; + sensor->clearInterrupts(); + wakeScreen(); + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + // Back off to the keepalive only once the pin has actually fired. No vendor firmware + // uses this line, so an unproven one keeps full-rate polling instead of costing latency. + if (intPinProven && !Throttle::hasElapsed(lastWomPollMs, MOTION_SENSOR_IRQ_KEEPALIVE_MS)) + return MOTION_SENSOR_CHECK_INTERVAL_MS; + lastWomPollMs = millis(); +#endif + auto status = sensor->setBank(0); if (sensor->status != ICM_20948_Stat_Ok) { LOG_DEBUG("ICM20948 isWakeOnMotion failed to set bank - %s", sensor->statusString()); @@ -126,8 +126,6 @@ int32_t ICM20948Sensor::runOnce() return MOTION_SENSOR_CHECK_INTERVAL_MS; } -#endif - void ICM20948Sensor::calibrate(uint16_t forSeconds) { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h index e84c7ea1bd..152ea9c95c 100755 --- a/src/motion/ICM20948Sensor.h +++ b/src/motion/ICM20948Sensor.h @@ -24,10 +24,8 @@ #define ICM_20948_WOM_THRESHOLD 16U #endif -// Define a pin in variant.h to use interrupts to read the ICM-20948 -#ifndef ICM_20948_WOM_THRESHOLD -#define ICM_20948_INT_PIN 255 -#endif +// Define ICM_20948_INT_PIN in variant.h to drive wake-on-motion from the INT pin +// instead of polling. The driver configures it active-low. // Uncomment this line to enable helpful debug messages on Serial // #define ICM_20948_DEBUG 1 @@ -83,6 +81,10 @@ class ICM20948Sensor : public MotionSensor ICM20948Singleton *sensor = nullptr; bool showingScreen = false; bool isAsleep = false; +#ifdef ICM_20948_INT_PIN + uint32_t lastWomPollMs = 0; + bool intPinProven = false; +#endif static constexpr const char *compassCalibrationFileName = "/prefs/compass_icm20948.dat"; #ifdef MUZI_BASE float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h index ef84e1b19b..5a7111c52b 100755 --- a/src/motion/MotionSensor.h +++ b/src/motion/MotionSensor.h @@ -3,6 +3,8 @@ #define _MOTION_SENSOR_H_ #define MOTION_SENSOR_CHECK_INTERVAL_MS 50 +// Safety-net drain for the interrupt-driven drivers: a dead INT pin degrades to polling. +#define MOTION_SENSOR_IRQ_KEEPALIVE_MS 1000 #define MOTION_SENSOR_CLICK_THRESHOLD 40 #include "../configuration.h" diff --git a/src/sleep.cpp b/src/sleep.cpp index 31c91e80d0..2b6f58843c 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -477,6 +477,12 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r #endif #if defined(WAKE_ON_TOUCH) gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL); +#endif +#ifdef MOTION_WAKE_INT_PIN + // Only arm motion wake when the user asked for it, otherwise every tilt costs a wakeup. + if (config.display.wake_on_tap_or_motion) + gpio_wakeup_enable((gpio_num_t)MOTION_WAKE_INT_PIN, + MOTION_WAKE_INT_ACTIVE_HIGH ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL); #endif enableLoraInterrupt(); #ifdef PMU_IRQ @@ -524,6 +530,10 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r #if defined(WAKE_ON_TOUCH) gpio_wakeup_disable((gpio_num_t)SCREEN_TOUCH_INT); #endif +#ifdef MOTION_WAKE_INT_PIN + // Unconditional: the config can have changed while we were asleep. + gpio_wakeup_disable((gpio_num_t)MOTION_WAKE_INT_PIN); +#endif #if !defined(SOC_PM_SUPPORT_EXT_WAKEUP) && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) if (radioType != RF95_RADIO) { gpio_wakeup_disable((gpio_num_t)LORA_DIO1); diff --git a/variants/nrf52840/t-impulse-plus/variant.h b/variants/nrf52840/t-impulse-plus/variant.h index e3a6c7ee63..33cfaff247 100644 --- a/variants/nrf52840/t-impulse-plus/variant.h +++ b/variants/nrf52840/t-impulse-plus/variant.h @@ -157,6 +157,8 @@ static const uint8_t SCL = PIN_WIRE_SCL; // IMU (ICM20948 on Wire1) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define HAS_ICM20948 +// D27, not (0 + 7): pinMode/attachInterrupt index g_ADigitalPinMap, where 7 is P1.13 (RF_VC1). +#define ICM_20948_INT_PIN D27 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Charger (SGM41562 on Wire1 @ 0x03)