Repository navigation
Low power detection sensor module for nRF52 & ESP32 #8778
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
32be616
ad3ff9a
4f0faed
58e0a45
a22f262
aa70b69
0616c5b
566a34f
ffee394
590a0aa
9a82b45
dba8d61
a54015a
8f3183a
e3893b2
22f6a66
86940c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,24 @@ | |
| #include "configuration.h" | ||
| #include "main.h" | ||
| #include <Throttle.h> | ||
|
|
||
| #ifdef ARCH_NRF52 | ||
| #include "sleep.h" | ||
| // Reserved values for GPREGRET are: 0xF5 (NRF52_MAGIC_LFS_IS_CORRUPT) | ||
| // and for the bootloader 0xB1, 0xA8, 0x4E, 0x57, 0x6D, lets choose free ones, | ||
| // although the bootloader shouldn't be called in the following soft reset. | ||
| constexpr uint32_t BOOT_FROM_COLD = 0x00; | ||
| constexpr uint32_t BOOT_FROM_TIMEOUT = 0xC0; | ||
| constexpr uint32_t BOOT_FROM_GPIOEVENT = 0xFE; | ||
| #elif defined(ESP32_WITH_EXT0) | ||
| #include "esp32/clk.h" | ||
| #include "sleep.h" | ||
| #include "soc/rtc.h" | ||
| #include <driver/rtc_io.h> | ||
| RTC_DATA_ATTR static uint32_t time_acc_s = 0; | ||
| RTC_DATA_ATTR static uint32_t sleepTime_s; | ||
| #endif | ||
|
|
||
| DetectionSensorModule *detectionSensorModule; | ||
|
|
||
| #define GPIO_POLLING_INTERVAL 100 | ||
|
|
@@ -65,26 +83,143 @@ int32_t DetectionSensorModule::runOnce() | |
| return disable(); | ||
|
|
||
| if (firstTime) { | ||
|
|
||
| #ifdef DETECTION_SENSOR_EN | ||
| pinMode(DETECTION_SENSOR_EN, OUTPUT); | ||
| digitalWrite(DETECTION_SENSOR_EN, HIGH); | ||
| #endif | ||
|
|
||
| // This is the first time the OSThread library has called this function, so do some setup | ||
| firstTime = false; | ||
|
|
||
| if (moduleConfig.detection_sensor.monitor_pin > 0) { | ||
| pinMode(moduleConfig.detection_sensor.monitor_pin, moduleConfig.detection_sensor.use_pullup ? INPUT_PULLUP : INPUT); | ||
| #ifdef ARCH_NRF52 | ||
| if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { | ||
| nrf_gpio_pin_sense_t toSense = | ||
| (moduleConfig.detection_sensor.detection_trigger_type & 1) ? NRF_GPIO_PIN_SENSE_HIGH : NRF_GPIO_PIN_SENSE_LOW; | ||
| nrf_gpio_cfg_input(moduleConfig.detection_sensor.monitor_pin, | ||
| moduleConfig.detection_sensor.use_pullup ? NRF_GPIO_PIN_PULLUP : NRF_GPIO_PIN_NOPULL); | ||
| nrf_gpio_cfg_sense_set(moduleConfig.detection_sensor.monitor_pin, toSense); | ||
|
|
||
| uint32_t regret = BOOT_FROM_COLD; | ||
| if (sd_power_gpregret_get(0, ®ret) != NRF_SUCCESS) { | ||
| // necessary if softdevice is not enabled yet | ||
| regret = NRF_POWER->GPREGRET; | ||
| } | ||
| if (NRF_P0->LATCH || NRF_P1->LATCH) { | ||
| LOG_INFO("Woke up from eternal sleep by GPIO.", __builtin_ctz(NRF_P0->LATCH ? NRF_P0->LATCH : NRF_P1->LATCH), | ||
| NRF_P0->LATCH ? 0 : 1); | ||
| NRF_P1->LATCH = 0xFFFFFFFF; | ||
| NRF_P0->LATCH = 0xFFFFFFFF; | ||
| sendDetectionMessage(); | ||
| } else if (regret == BOOT_FROM_TIMEOUT) { | ||
| LOG_INFO("Woke up by timeout."); | ||
| sendCurrentStateMessage(getState()); | ||
| } else if (regret == BOOT_FROM_GPIOEVENT) { | ||
| LOG_INFO("Woke up from interval sleep by GPIO."); | ||
| sendDetectionMessage(); | ||
| } else { | ||
| // We booted fresh. Enforce sending on first detection event. | ||
| lastSentToMesh = -Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs); | ||
| } | ||
| if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS)) { | ||
| NRF_POWER->GPREGRET = BOOT_FROM_COLD; | ||
| } | ||
|
|
||
| LOG_INFO("Cold boot. Init in power saving mode"); | ||
| // Choose the minimum wakeup time (config.power.min_wake_sec) depending to your needs. | ||
| // The least time should be 5s to send just the detection message. | ||
| // Choose 45s+ for comfortable connectivity via BLE or USB. | ||
| // Device Telemetry is sent after ~62s. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will it improve power usage if for example we sent also telemetry message (if needed of course given time periods elapsed) in the minimum (5s-10s) wakeup windows instead of keeping device up for whole minute just for the telemetry timer to trigger the same?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I question if device telemetry data (i mean the radio metrics) is any worth on a device which is sleeping most of the time, because no useful radio statistic can be acquired during that time.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Radio statistics of course not - but either environment statistics or device battery is useful. I know battery stats are part of the detection message but it can not be used with monitoring software looking for generic telemetry frames. On device boot we already send NodeInfo - if the boot reason is wakeup and node is sensor, maybe it would be worth to send telemetry packet too instead of wasting battery to just wait for arbitrary number of seconds. This could be configurable.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are 2 great use cases for the detection sensor:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand, one question: If I want to monitor some remote site with alarm detection and device is waking up periodically (in addition to GPIO alarm trigger) i need to keep it up at least for 70s in order to be able to see what battery I have left there, right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want to wait for the device metrics to be sent (which include the battery voltage) it would be, but not anymore. I added the battery voltage to the state broadcast.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So essentially we can set min_wake_secs to 5s, state_broadcast_secs to 24h and minimum_broadcast_secs to 10min and we get a nice alarm sensor.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, thanks for explanation! I would suggest renaming "state_broadcast_secs" to "state_broadcast_interval". Same with minimum broadcast :)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I just renamed: |
||
| return Default::getConfiguredOrDefaultMs(config.power.min_wake_secs, 90); | ||
| } else | ||
| #elif defined(ESP32_WITH_EXT0) | ||
| if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { | ||
|
|
||
| esp_sleep_wakeup_cause_t wakeCause = esp_sleep_get_wakeup_cause(); | ||
| uint64_t timeNow_us; | ||
| uint32_t timeDiff_s; | ||
|
|
||
| switch (wakeCause) { | ||
| case ESP_SLEEP_WAKEUP_EXT0: | ||
| case ESP_SLEEP_WAKEUP_EXT1: | ||
| LOG_INFO("Woke up from interval sleep by GPIO. Sending detection message."); | ||
| timeNow_us = rtc_time_slowclk_to_us(rtc_time_get(), esp_clk_slowclk_cal_get()); | ||
| timeDiff_s = (timeNow_us / 1000000 - sleepTime_s) + config.power.min_wake_secs; | ||
| // LOG_INFO("sleeptime (effectivly lastSentToMesh): %u", timeDiff_s - config.power.min_wake_secs); | ||
| // LOG_INFO("time since last run (effectivly lastSentToMesh): %us", timeDiff_s); | ||
|
|
||
| if (timeDiff_s + time_acc_s > moduleConfig.detection_sensor.minimum_broadcast_secs) { | ||
| // LOG_INFO("sending as %u > %u", timeDiff_s + time_acc_s, | ||
| // moduleConfig.detection_sensor.minimum_broadcast_secs); | ||
| time_acc_s = 0; | ||
| sendDetectionMessage(); | ||
| } else { | ||
| // LOG_INFO("not sending as did not reach broadcast threshold. %us < %us", timeDiff_s + time_acc_s, | ||
| // moduleConfig.detection_sensor.minimum_broadcast_secs); | ||
| time_acc_s += timeDiff_s; | ||
| } | ||
| break; | ||
| case ESP_SLEEP_WAKEUP_TIMER: | ||
| LOG_INFO("Woke up by timeout. Sending state message."); | ||
| sendCurrentStateMessage(getState()); | ||
| break; | ||
| case ESP_SLEEP_WAKEUP_TOUCHPAD: | ||
| case ESP_SLEEP_WAKEUP_ULP: | ||
| default: | ||
| // We booted fresh. Enforce sending on first detection event. | ||
| LOG_INFO("Cold boot. Init in power saving mode"); | ||
| break; | ||
| } | ||
|
|
||
| if (rtc_gpio_is_valid_gpio((gpio_num_t)moduleConfig.detection_sensor.monitor_pin)) { | ||
| if (moduleConfig.detection_sensor.use_pullup) { | ||
| rtc_gpio_pulldown_dis((gpio_num_t)moduleConfig.detection_sensor.monitor_pin); | ||
| rtc_gpio_pullup_en((gpio_num_t)moduleConfig.detection_sensor.monitor_pin); | ||
| } else { | ||
| rtc_gpio_pulldown_dis((gpio_num_t)moduleConfig.detection_sensor.monitor_pin); | ||
| rtc_gpio_pullup_dis((gpio_num_t)moduleConfig.detection_sensor.monitor_pin); | ||
| } | ||
| if (esp_sleep_enable_ext0_wakeup((gpio_num_t)moduleConfig.detection_sensor.monitor_pin, | ||
| (moduleConfig.detection_sensor.detection_trigger_type & 1) ? 1 : 0) != | ||
| ESP_OK) | ||
| LOG_ERROR("error enabling ext0 on gpio %d", moduleConfig.detection_sensor.monitor_pin); | ||
| return Default::getConfiguredOrDefaultMs(config.power.min_wake_secs, 90); | ||
| } else { | ||
| LOG_WARN("Enabled, but specified pin is not an RTC pin. Disabling module"); | ||
| printRtcPins(); | ||
| return disable(); | ||
| } | ||
| } else | ||
| #elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) | ||
|
rbomze marked this conversation as resolved.
|
||
| LOG_WARN("Enabled, but used ESP32 variant lacks necessary EXT0. Disabling module"); | ||
| return disable(); | ||
| #endif | ||
| pinMode(moduleConfig.detection_sensor.monitor_pin, | ||
| moduleConfig.detection_sensor.use_pullup ? INPUT_PULLUP : INPUT); | ||
| } else { | ||
| LOG_WARN("Detection Sensor Module: Set to enabled but no monitor pin is set. Disable module"); | ||
| LOG_WARN("Enabled, but no monitor pin is set. Disabling module"); | ||
| return disable(); | ||
| } | ||
| LOG_INFO("Detection Sensor Module: init"); | ||
| LOG_INFO("Init in default mode"); | ||
|
|
||
| return setStartDelay(); | ||
| } | ||
|
|
||
| // LOG_DEBUG("Detection Sensor Module: Current pin state: %i", digitalRead(moduleConfig.detection_sensor.monitor_pin)); | ||
| #if defined(ARCH_NRF52) || defined(ESP32_WITH_EXT0) | ||
| if ((config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving)) { | ||
| // If 'State Broadcast Interval' (moduleConfig.detection_sensor.state_broadcast_secs) is specified it will be used, if | ||
| // unset the sleep will last 'forever', interrupted by specified GPIO event | ||
| // nRF52: Using a timeout the module enters a low power loop, without it will shutdown with least power consumption | ||
| // ESP32: Always using deep sleep with RTC | ||
| // TODO: check why DELAY_FOREVER does not work on ESP | ||
| uint32_t nightyNightMs = | ||
| Default::getConfiguredOrDefault(moduleConfig.detection_sensor.state_broadcast_secs * 1000, portMAX_DELAY); | ||
|
|
||
| #ifdef ESP32_WITH_EXT0 | ||
| sleepTime_s = rtc_time_slowclk_to_us(rtc_time_get(), esp_clk_slowclk_cal_get()) / 1000000; | ||
| #endif | ||
| doDeepSleep(nightyNightMs, false, true); | ||
| } | ||
| #endif | ||
|
|
||
| if (!Throttle::isWithinTimespanMs(lastSentToMesh, | ||
| Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs))) { | ||
|
|
@@ -97,7 +232,7 @@ int32_t DetectionSensorModule::runOnce() | |
| sendDetectionMessage(); | ||
| return DELAYED_INTERVAL; | ||
| case DetectionSensorVerdictSendState: | ||
| sendCurrentStateMessage(isDetected); | ||
| sendCurrentStateMessage(getState()); | ||
| return DELAYED_INTERVAL; | ||
| case DetectionSensorVerdictNoop: | ||
| break; | ||
|
|
@@ -110,7 +245,7 @@ int32_t DetectionSensorModule::runOnce() | |
| !Throttle::isWithinTimespanMs(lastSentToMesh, | ||
| Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.state_broadcast_secs, | ||
| default_telemetry_broadcast_interval_secs))) { | ||
| sendCurrentStateMessage(hasDetectionEvent()); | ||
| sendCurrentStateMessage(getState()); | ||
| return DELAYED_INTERVAL; | ||
| } | ||
| return GPIO_POLLING_INTERVAL; | ||
|
|
@@ -124,6 +259,9 @@ void DetectionSensorModule::sendDetectionMessage() | |
| meshtastic_MeshPacket *p = allocDataPacket(); | ||
| p->want_ack = false; | ||
| p->decoded.payload.size = strlen(message); | ||
| if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) | ||
| p->priority = meshtastic_MeshPacket_Priority_RELIABLE; | ||
|
|
||
| memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); | ||
| if (moduleConfig.detection_sensor.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { | ||
| p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character | ||
|
|
@@ -142,10 +280,14 @@ void DetectionSensorModule::sendDetectionMessage() | |
| void DetectionSensorModule::sendCurrentStateMessage(bool state) | ||
| { | ||
| char *message = new char[40]; | ||
| sprintf(message, "%s state: %i", moduleConfig.detection_sensor.name, state); | ||
| sprintf(message, "%s state %s, %d%%", moduleConfig.detection_sensor.name, state ? "high" : "low", | ||
| powerStatus->getBatteryChargePercent()); | ||
| meshtastic_MeshPacket *p = allocDataPacket(); | ||
| p->want_ack = false; | ||
| p->decoded.payload.size = strlen(message); | ||
| if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) | ||
| p->priority = meshtastic_MeshPacket_Priority_RELIABLE; | ||
|
|
||
| memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); | ||
| lastSentToMesh = millis(); | ||
| if (!channels.isDefaultChannel(0)) { | ||
|
|
@@ -156,9 +298,72 @@ void DetectionSensorModule::sendCurrentStateMessage(bool state) | |
| delete[] message; | ||
| } | ||
|
|
||
| boolean DetectionSensorModule::getState() | ||
| { | ||
| return digitalRead(moduleConfig.detection_sensor.monitor_pin); | ||
| } | ||
|
|
||
| bool DetectionSensorModule::hasDetectionEvent() | ||
| { | ||
| bool currentState = digitalRead(moduleConfig.detection_sensor.monitor_pin); | ||
| // LOG_DEBUG("Detection Sensor Module: Current state: %i", currentState); | ||
| bool currentState = getState(); | ||
| return (moduleConfig.detection_sensor.detection_trigger_type & 1) ? currentState : !currentState; | ||
| } | ||
| } | ||
|
|
||
| #ifdef ARCH_NRF52 | ||
| boolean DetectionSensorModule::shouldLoop() | ||
| { | ||
| return moduleConfig.detection_sensor.enabled && config.power.is_power_saving && moduleConfig.detection_sensor.monitor_pin > 0; | ||
| } | ||
|
|
||
| void DetectionSensorModule::lpDelay() | ||
| { | ||
| sd_power_mode_set(NRF_POWER_MODE_LOWPWR); | ||
| delay(moduleConfig.detection_sensor.minimum_broadcast_secs - config.power.min_wake_secs); | ||
| } | ||
|
|
||
| void DetectionSensorModule::lpLoop(uint32_t msecToWake) | ||
| { | ||
| for (uint32_t i = msecToWake / 100;; i--) { | ||
| delay(100); | ||
| if (hasDetectionEvent() && | ||
| !Throttle::isWithinTimespanMs( | ||
| lastSentToMesh, Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs))) { | ||
| if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && | ||
| sd_power_gpregret_set(0, BOOT_FROM_GPIOEVENT) == NRF_SUCCESS)) { | ||
| // necessary if softdevice is not enabled yet or never was | ||
| NRF_POWER->GPREGRET = BOOT_FROM_GPIOEVENT; | ||
| } | ||
| break; | ||
| } | ||
| if (i == 0) { | ||
| if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, BOOT_FROM_TIMEOUT) == NRF_SUCCESS)) { | ||
| NRF_POWER->GPREGRET = BOOT_FROM_TIMEOUT; | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| #elif defined(ESP32_WITH_EXT0) | ||
| void DetectionSensorModule::printRtcPins() | ||
| { | ||
| #if defined(CONFIG_IDF_TARGET_ESP32) | ||
| LOG_WARN("Available GPIOs for wakeup: 0,2,4,12-15,25-27,32-39"); | ||
| #elif defined(CONFIG_IDF_TARGET_ESP32S2) | ||
| LOG_WARN("Available GPIOs for wakeup: 0-21"); | ||
| #elif defined(CONFIG_IDF_TARGET_ESP32S3) | ||
| LOG_WARN("Available GPIOs for wakeup: 0-21"); | ||
| #endif | ||
| } | ||
|
|
||
| bool DetectionSensorModule::skipGPIO(int gpio) | ||
| { | ||
| return moduleConfig.detection_sensor.enabled && config.power.is_power_saving && | ||
| moduleConfig.detection_sensor.monitor_pin == gpio; | ||
| } | ||
| #else | ||
| bool DetectionSensorModule::skipGPIO(int gpio) | ||
| { | ||
| // always return false for ESP32s without EXT0 (e.g ESP32c6) | ||
| return false; | ||
| } | ||
| #endif | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason SD is initialized only on Bluetooth enable sequence. I will be fixing that in #8793 but maybe a quick workaround would be to init soft device early on boot to have one common API across firmware to avoid need to checking that constantly.