Torxgewinde / Firebeetle-2-ESP32-E

Using the Firebeetle ESP32-E as battery powered sensor (SKU:DFR0654-F)
GNU General Public License v2.0
57 stars 2 forks source link

REED switch input, reading out gas-meter (was: Connection of pir sensor) #4

Closed VoyteckPL closed 1 year ago

VoyteckPL commented 1 year ago

Which gpio pins are used to connect pir sensor? Is it possible to use reed sensor instead? I would lile to use this project for gas meter.

Torxgewinde commented 1 year ago

Hi, The comments point this out:

//PIR motion sensor is connected to GPIO4 (Pin: D12)
#define PIR_GPIO 4
#define PIR_DEEPSLEEP_PIN GPIO_NUM_4

In general reading a reed-switch should also be able to trigger the wakeup. Depending on the number of times it wakes the processor, just counting the events and transmitting a few times a day might be required to achieve good runtime. This certainly requires several changes, but is certainly achievable.

VoyteckPL commented 1 year ago

Thanks. Is there any chance you could help me with the code? I can see you made a pro job here. I'm a total noob when it comes to arduino. I see there are some pir specific option like ignore after motion etc. I would be grateful if you could help.

Torxgewinde commented 1 year ago

@VoyteckPL : Sure, I am willing to assist, however I am not coding it for you. I am also interested in measuring my gasmeter with a reed switch or the ESP32 internal hall sensor. If you have specific questions I suggest you post code and we can discuss and tweak it until it works. If the result is indeed powersaving enough, that is something I can only imagine but not promise yet. It SHOULD be OK in my opinion.

VoyteckPL commented 1 year ago

Ok. When Firebeetle arrives I will do some testing. When I look at your code it seems to be good for reed switch with very little adjustments.

Torxgewinde commented 1 year ago

That's cool. I also ordered a REED switch to test it and am looking forward to collaborate.

VoyteckPL commented 1 year ago

Nice! My firebeetle should arrive tomorrow 🙂

VoyteckPL commented 1 year ago

image

I'm trying to compile the code but I get this error. Which exactly libraries did you use?

include

include

include "esp_adc_cal.h"

I have FireBeetle 2 board.

Torxgewinde commented 1 year ago

I also need to check with the current Arduino-IDE.

The working versions are:

Maybe some path have changed and need to be updated. I will also use the current IDE and see what might have changed...

Edit: added MQTT library info

VoyteckPL commented 1 year ago

Ok I will check and report back

Torxgewinde commented 1 year ago

It also compiles, uploads and send serial output with:

It should compile and run. I haven't entered my WiFi-credentials yet and a full test.

Bildschirmfoto_2022-12-30_12-00-50

VoyteckPL commented 1 year ago

Fantastic. Thanks. I will do some testing today.

VoyteckPL commented 1 year ago

Yay! I was able to compile. The problem was I didn't have 2.0.6 Board Definition installed. I had to manually add this link to preferences :

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

By default it was only 1.0.6.

VoyteckPL commented 1 year ago

Ok so I sucessfully uploaded firmware! :) My reed sensor should be between pins GND and D12 (GPIO4) right? I won't be using that 3rd cable for 3.3V as I would for PIR sensor.

Torxgewinde commented 1 year ago

Yes, if you use D12 you won't have to lookup other #define , which saves you some lookups.

To provide current to the sensor, a Pullup or Pulldown resistor must be actived. The Arduino function pinMode(PIR_GPIO, INPUT_PULLUP) can be used. When enabling the internal pullup, you connect the reed-switch to D12 and GND.

The 3.3V is not required, right.

VoyteckPL commented 1 year ago

Thank you! Unfortunately I have no idea how to adjust your code :( I tried but C++ is too hard for me at the moment as I'm newbie.... I can only help with logic and experience as I tested similar solution with Zigbee, Tasmota and ESPHome and I noticed the things which are really important for this gas meter. I can support you somehow if you take it in account. I would also do some testing and report back.

  1. Debounce - I used around 1000 ms in Tasmota. This gave me perfect results without any false "clicks". This is a must. With this when gas meter stops just between on and off magnet it can cause multiple fake "clicks"
  2. Internal counter in software - example situation > you do something in HA which needs restart but the gas consumption happends in background. In this case the gas consumption will continue but it won't be counted if the counter is in external software.
  3. Adjust internal counter value wirelessly (maybe via MQTT) - sometimes you may want to adjust the counter to sync with real gas meter or for some other reason (testing). This should be doable wirelessly.
  4. Report battery - already implemented
  5. Wifi quality signal in percent - really useful if gas meter is far away.
  6. Ability to see if sensor is ON or OFF - just for information and for statistics.
  7. Device should report always if sensor changes from ON to OFF or from OFF to ON.

I suppose that is all. What I also noticed > I have only gas heater: When water is heated (cycle which takes around 20 minutes) the usage is 0.01 m3 per ~15 seconds When heating is on (it can take longer periods of time for example few hours of constant but slow gas consumption) the usage is 0.01 m3 per ~ 90 seconds.

I'm really sorry I can't help with the coding. I'm sure this solution will become very popular. I tested zigbee and it was not reliable (no signal quality info, loosing packets)

Torxgewinde commented 1 year ago

Ok, no worries. Please chime back in anytime when you feel like picking up the project again and thank you for the hints, surely it helps when working on it again.

VoyteckPL commented 1 year ago

No problem. If I could support you some other way - I'm open 😉 we can talk on WhatsApp if you wish.

Torxgewinde commented 1 year ago

To read the Reed-switch and light the LED accordingly:

const int ReedPin = 4;
const int ledPin = 2;

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(ReedPin, INPUT_PULLUP);
}

void loop() {
  digitalWrite(ledPin, digitalRead(ReedPin));
}

To read the reed-switch and debounce with a second:

/*******************************************************************************
#                                                                              #
# Using the Firebeetle 2 ESP32-E as battery powered gasmeter-reader            #
# Project: https://github.com/Torxgewinde/Firebeetle-2-ESP32-E                 # 
#                                                                              #
# Firebeetle documentation at:                                                 #
# https://wiki.dfrobot.com/FireBeetle_Board_ESP32_E_SKU_DFR0654                #                                                             #
#                                                                              # 
# Copyright (C) 2022 Tom Stöveken                                              #
#                                                                              #
# This program is free software; you can redistribute it and/or modify         #
# it under the terms of the GNU General Public License as published by         #
# the Free Software Foundation; version 2 of the License.                      #
#                                                                              #
# This program is distributed in the hope that it will be useful,              #
# but WITHOUT ANY WARRANTY; without even the implied warranty of               #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                #
# GNU General Public License for more details.                                 #
#                                                                              #
# You should have received a copy of the GNU General Public License            #
# along with this program; if not, write to the Free Software                  #
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA    #
#                                                                              #
********************************************************************************/

#include <WiFi.h>
#include <MQTT.h>

#include "esp_adc_cal.h"

#define ESSID "WIFI SSID"
#define PSK   "WIFI PASSWORD"

#define LOW_BATTERY_VOLTAGE 3.20
#define VERY_LOW_BATTERY_VOLTAGE 3.10
#define CRITICALLY_LOW_BATTERY_VOLTAGE 3.00

//char *MQTTServer = "server.lan";
IPAddress MQTTServer = IPAddress(192,168,1,1);
uint16_t MQTTPort = 1883;
String MQTTUsername = "username";
String MQTTPassword = "password";
String MQTTDeviceName = "Gaszaehler";
String MQTTRootTopic = "Keller/Gaszaehler";

enum _state {
  S_STARTUP = 0,
  S_DEBOUNCE_LOW,
  S_DEBOUNCE_HIGH,
  S_IDLE
};

enum _message {
  M_COUNTER = 0,
  M_STATUS
};

RTC_NOINIT_ATTR struct {
  uint8_t bssid[6];
  uint8_t channel;

  float BatteryVoltage;      //battery voltage in V
  uint64_t NumberOfRestarts; //number of restarts
  uint64_t ActiveTime;       //time being active in ms

  enum _state state;         //keep track of current state

  uint64_t counter;
} cache;

//REED contact is connected to GPIO4 (Pin: D12)
#define REED_GPIO 4
#define REED_DEEPSLEEP_PIN GPIO_NUM_4

#define DEBOUNCE_TIME 1*1000000ULL

//periodically wakeup and report battery status even without motion
#define LONG_TIME 12*60*60*1000000ULL

/******************************************************************************
Description.: bring the WiFi up
Input Value.: When "tryCachedValuesFirst" is true the function tries to use
              cached values before attempting a scan + association
Return Value: true if WiFi is up, false if it timed out
******************************************************************************/
bool WiFiUP(bool tryCachedValuesFirst) {
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);

  if(tryCachedValuesFirst && cache.channel > 0) {
    Serial.printf("Cached values as follows:\r\n");
    Serial.printf(" Channel....: %d\r\n", cache.channel);
    Serial.printf(" BSSID......: %x:%x:%x:%x:%x:%x\r\n", cache.bssid[0], \
                                                         cache.bssid[1], \
                                                         cache.bssid[2], \
                                                         cache.bssid[3], \
                                                         cache.bssid[4], \
                                                         cache.bssid[5]);

    WiFi.begin(ESSID, PSK, cache.channel, cache.bssid);

    for (unsigned long i=millis(); millis()-i < 10000;) {
      delay(10);

      if (WiFi.status() == WL_CONNECTED) {
        Serial.printf("WiFi connected with cached values (%lu)\r\n", millis()-i);
        return true;
      } 
    }
  }

  cache.channel = 0;
  for (uint32_t i = 0; i < sizeof(cache.bssid); i++)
    cache.bssid[i] = 0;

  // try it with the slower process
  WiFi.begin(ESSID, PSK);

  for (unsigned long i=millis(); millis()-i < 60000;) {
    delay(10);

    if (WiFi.status() == WL_CONNECTED) {
      Serial.printf("WiFi connected (%lu)\r\n", millis()-i);

      uint8_t *bssid = WiFi.BSSID();
      for (uint32_t i = 0; i < sizeof(cache.bssid); i++)
        cache.bssid[i] = bssid[i];
      cache.channel = WiFi.channel();

      return true;
    }
  }

  Serial.printf("WiFi NOT connected\r\n");
  return false;
}

/******************************************************************************
Description.: reads the battery voltage through the voltage divider at GPIO34
              if the ESP32-E has calibration eFused those will be used.
              In comparison with a regular voltmeter the values of ESP32 and
              multimeter differ only about 0.05V
Input Value.: -
Return Value: battery voltage in volts
******************************************************************************/
float readBattery() {
  uint32_t value = 0;
  int rounds = 11;
  esp_adc_cal_characteristics_t adc_chars;

  //battery voltage divided by 2 can be measured at GPIO34, which equals ADC1_CHANNEL6
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
  switch(esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars)) {
    case ESP_ADC_CAL_VAL_EFUSE_TP:
      Serial.println("Characterized using Two Point Value");
      break;
    case ESP_ADC_CAL_VAL_EFUSE_VREF:
      Serial.printf("Characterized using eFuse Vref (%d mV)\r\n", adc_chars.vref);
      break;
    default:
      Serial.printf("Characterized using Default Vref (%d mV)\r\n", 1100);
  }

  //to avoid noise, sample the pin several times and average the result
  for(int i=1; i<=rounds; i++) {
    value += adc1_get_raw(ADC1_CHANNEL_6);
  }
  value /= (uint32_t)rounds;

  //due to the voltage divider (1M+1M) values must be multiplied by 2
  //and convert mV to V
  return esp_adc_cal_raw_to_voltage(value, &adc_chars)*2.0/1000.0;
}

/******************************************************************************
Description.: send MQTT message
Input Value.: msg selects the message to send
Return Value: true if OK, false if errors occured
******************************************************************************/
bool SendMessage(enum _message msg) {
  char buf[256] = {0};

  //read RTC
  struct timeval tv;
  gettimeofday(&tv, NULL);

  //establish connection to MQTT server
  WiFiClient net;
  MQTTClient MQTTClient;
  MQTTClient.begin(MQTTServer, MQTTPort, net);
  MQTTClient.setOptions(30, true, 5000);

  if( MQTTClient.connect(MQTTDeviceName.c_str(), MQTTUsername.c_str(), MQTTPassword.c_str())) {
    switch(msg) {
      case M_COUNTER:
        Serial.printf("Sending counter: %d\r\n", cache.counter);

        MQTTClient.publish(MQTTRootTopic+"/counter", String(cache.counter), false, 2);
        break;
      case M_STATUS:
        Serial.printf("Sending status\r\n");

        MQTTClient.publish(MQTTRootTopic+"/counter", String(cache.counter), false, 2);
        MQTTClient.publish(MQTTRootTopic+"/BatteryVoltage", String(cache.BatteryVoltage, 3), false, 2);
        snprintf(buf, sizeof(buf)-1, "%ld.%06ld", tv.tv_sec, tv.tv_usec);
        MQTTClient.publish(MQTTRootTopic+"/BatteryRuntime", buf, false, 2);
        snprintf(buf, sizeof(buf)-1, "%llu", cache.NumberOfRestarts);
        MQTTClient.publish(MQTTRootTopic+"/Restarts", buf, false, 2);
        snprintf(buf, sizeof(buf)-1, "%llu", cache.ActiveTime);
        MQTTClient.publish(MQTTRootTopic+"/ActiveTime", buf, false, 2);
        snprintf(buf, sizeof(buf)-1, "%ld", WiFi.RSSI());
        MQTTClient.publish(MQTTRootTopic+"/RSSI", buf, false, 2);
        break;
      default:
        Serial.printf("unkown message, not sending anything\r\n");
    }
    MQTTClient.disconnect();
    return true;
  }

  return false;
}

/******************************************************************************
Description.: since this is a battery sensor, everything happens in setup
              and when the tonguing' is done the device enters deep-sleep
Input Value.: -
Return Value: -
******************************************************************************/
void setup() {
  //visual feedback when we are active, turn on onboard LED
  pinMode(2, OUTPUT);
  digitalWrite(2, HIGH);

  cache.NumberOfRestarts++;

  Serial.begin(115200);
  Serial.print("===================================================\r\n");
  Serial.printf("FireBeetle active\r\n" \
                " Compiled at: " __DATE__ " - " __TIME__ "\r\n" \
                " ESP-IDF: %s\r\n", esp_get_idf_version());

  //read battery voltage
  cache.BatteryVoltage = readBattery();
  Serial.printf("Voltage: %4.3f V\r\n", cache.BatteryVoltage);

  //a reset is required to wakeup again from below CRITICALLY_LOW_BATTERY_VOLTAGE
  //this is to prevent damaging the empty battery by saving as much power as possible
  if (cache.BatteryVoltage < CRITICALLY_LOW_BATTERY_VOLTAGE) {
    Serial.println("Battery critically low, hibernating...");

    //switch off everything that might consume power
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_VDDSDIO, ESP_PD_OPTION_OFF);
    //esp_sleep_pd_config(ESP_PD_DOMAIN_CPU, ESP_PD_OPTION_OFF);

    //disable all wakeup sources
    esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);

    cache.ActiveTime += millis();
    digitalWrite(2, LOW);
    esp_deep_sleep_start();

    Serial.println("This should never get printed");
    return;
  }

  //if battery is below LOW_BATTERY_VOLTAGE but still above CRITICALLY_LOW_BATTERY_VOLTAGE, 
  //stop doing the regular work
  //when put on charge the device will wakeup after a while and recognize voltage is OK
  //this way the battery can run low, put still wakeup without physical interaction
  if (cache.BatteryVoltage < LOW_BATTERY_VOLTAGE) {
    Serial.println("Battery low, deep sleeping...");

    //sleep ~60 minutes if battery is CRITICALLY_LOW_BATTERY_VOLTAGE to VERY_LOW_BATTERY_VOLTAGE
    //sleep ~10 minutes if battery is VERY_LOW_BATTERY_VOLTAGE to LOW_BATTERY_VOLTAGE
    uint64_t sleeptime = (cache.BatteryVoltage >= VERY_LOW_BATTERY_VOLTAGE) ? \
                           10*60*1000000ULL : 60*60*1000000ULL;

    esp_sleep_enable_timer_wakeup(sleeptime);
    cache.ActiveTime += millis();
    digitalWrite(2, LOW);
    esp_deep_sleep_start();

    Serial.println("This should never get printed");
    return;
  }

  //read GPIO level
  pinMode(REED_GPIO, INPUT_PULLUP);
  int level = digitalRead(REED_GPIO);
  Serial.printf("Reed-switch is: %s\r\n", (level)?"NOMAGNET":"MAGNET");

  //check if a reset/power-on occured
  if (esp_reset_reason() == ESP_RST_POWERON) {
      Serial.printf("ESP was just switched ON\r\n");
      cache.state = S_STARTUP;
      cache.ActiveTime = 0;
      cache.NumberOfRestarts = 0;
      cache.counter = 0;

      //set RTC to 0
      struct timeval tv;
      tv.tv_sec = 0;
      tv.tv_usec = 0;
      settimeofday(&tv, NULL);

      //default is to have WiFi off
      if (WiFi.getMode() != WIFI_OFF) {
        Serial.printf("WiFi wasn't off!\r\n");
        WiFi.persistent(true);
        WiFi.mode(WIFI_OFF);
      }

      //try to connect
      WiFiUP(false);
      WiFi.disconnect(true, true);

      //transition to new state
      cache.state = (level == HIGH) ? S_DEBOUNCE_HIGH : S_DEBOUNCE_LOW;
      Serial.printf("transition to state nr.: %d\r\n", cache.state);
      //wake by timer after debounce time is up
      esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
  }

  // check if ESP returns from deepsleep
  if (esp_reset_reason() == ESP_RST_DEEPSLEEP) {
    switch(esp_sleep_get_wakeup_cause()) {
      case ESP_SLEEP_WAKEUP_TIMER:
        Serial.printf("ESP woke up due to timer\r\n");

        switch(cache.state) {
          case S_DEBOUNCE_LOW:
          case S_DEBOUNCE_HIGH:
            if(level == LOW && cache.state == S_DEBOUNCE_LOW) { 
              cache.counter++;
              WiFiUP(true);
              SendMessage(M_COUNTER);
              WiFi.disconnect(true, true);
            }

            Serial.printf("transition to state S_IDLE\r\n");
            cache.state = S_IDLE;
            esp_sleep_enable_timer_wakeup(LONG_TIME);
            esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, (level == LOW)?1:0);
            break;

          case S_IDLE:
            WiFiUP(true);
            SendMessage(M_STATUS);
            WiFi.disconnect(true, true);

            Serial.printf("remaining in state S_IDLE\r\n");
            cache.state = S_IDLE;
            esp_sleep_enable_timer_wakeup(LONG_TIME);
            esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, (level == LOW)?1:0);
            break;

          default:
            Serial.printf("ESP woke up by timer in an unknown state\r\n");
        }
        break;

      case ESP_SLEEP_WAKEUP_EXT0:
        Serial.printf("ESP woke up by EXT0\r\n");

        if (level == HIGH) {
          cache.state = S_DEBOUNCE_HIGH;
          Serial.printf("transition to state S_DEBOUNCE_HIGH\r\n");
        }
        else {
          cache.state = S_DEBOUNCE_LOW;
          Serial.printf("transition to state S_DEBOUNCE_LOW\r\n");
        }

        esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
        break;

      default:
        Serial.printf("ESP woke up due to an unknown reason\r\n");
    }
  }

  Serial.printf("counter: %llu\r\n", cache.counter);
  Serial.printf("=== entering deepsleep after %d ms ===\r\n.\r\n.\r\n.\r\n", millis());
  cache.ActiveTime += millis();
  digitalWrite(2, LOW);
  esp_deep_sleep_start();

  Serial.println("This should never get printed");
}

void loop() {
  Serial.println("This should never get printed");
}

It is not done yet, but this is what I currently have. Providing the gasmeter start-value is still missing...

VoyteckPL commented 1 year ago

Amazing work! Let me know if I can buy you a coffee somehow!

VoyteckPL commented 1 year ago

One question : in this example if gas meter stops or OPEN or CLOSED position will it always go to deep sleep?

Torxgewinde commented 1 year ago

The idea is, that it wakes up by changes to the reed-state. Only if the magnet is present (trigger) and still present after a second (for debounce) it increments the counter and transmits the counter via MQTT. To conserve battery it sleeps most of the time.

What is missing to wakeup every N-counts or every N-hours to transmit the whole status and the offset to start counting from. It is work in progress and not done yet!

VoyteckPL commented 1 year ago

This is very promising! Just uploaded and tested and speed is amazing. Around 2 seconds maximum for connection and send mqtt! The only problem I noticed so far is reset of counter after lost power and I only see counter in MQTT (no other values like voltage etc.) image

VoyteckPL commented 1 year ago

Just one more clue ;) Usually one revolution of gas meter is 0.01 m3. It would be nice to have it with the decimals as result in MQTT rather than converting in from 1 to 0.01 in frontend.

Torxgewinde commented 1 year ago

Yes, i changed "counter" to be a "retained" value now. That way the broker will store the value even when resetting the ESP32 or changing the battery.

The new behavior will be:

Conversion of counts to actual qubic-meters m³ might follow. I am happy if battery runtime can be checked and is acceptable - that is my main concern for now.

/*******************************************************************************
#                                                                              #
# Using the Firebeetle 2 ESP32-E as battery powered gasmeter-reader            #
# Project: https://github.com/Torxgewinde/Firebeetle-2-ESP32-E                 # 
#                                                                              #
# Firebeetle documentation at:                                                 #
# https://wiki.dfrobot.com/FireBeetle_Board_ESP32_E_SKU_DFR0654                #                                                             #
#                                                                              # 
# Copyright (C) 2022 Tom Stöveken                                              #
#                                                                              #
# This program is free software; you can redistribute it and/or modify         #
# it under the terms of the GNU General Public License as published by         #
# the Free Software Foundation; version 2 of the License.                      #
#                                                                              #
# This program is distributed in the hope that it will be useful,              #
# but WITHOUT ANY WARRANTY; without even the implied warranty of               #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                #
# GNU General Public License for more details.                                 #
#                                                                              #
# You should have received a copy of the GNU General Public License            #
# along with this program; if not, write to the Free Software                  #
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA    #
#                                                                              #
********************************************************************************/

#include <WiFi.h>
#include <MQTT.h>
#include <stdlib.h> 

#include "esp_adc_cal.h"

#define ESSID ""
#define PSK   ""

#define LOW_BATTERY_VOLTAGE 3.20
#define VERY_LOW_BATTERY_VOLTAGE 3.10
#define CRITICALLY_LOW_BATTERY_VOLTAGE 3.00

//char *MQTTServer = "server.lan";
IPAddress MQTTServer = IPAddress(192,168,1,1);
uint16_t MQTTPort = 1883;
String MQTTUsername = "";
String MQTTPassword = "";
String MQTTDeviceName = "Gaszaehler";
String MQTTRootTopic = "Keller/Gaszaehler";

enum _state {
  S_STARTUP = 0,
  S_DEBOUNCE_LOW,
  S_DEBOUNCE_HIGH,
  S_IDLE
};

enum _message {
  M_COUNTER = 0,
  M_STATUS
};

RTC_NOINIT_ATTR struct {
  uint8_t bssid[6];
  uint8_t channel;

  float BatteryVoltage;      //battery voltage in V
  uint64_t NumberOfRestarts; //number of restarts
  uint64_t ActiveTime;       //time being active in ms

  enum _state state;         //keep track of current state

  uint64_t counter;          //gasmeter-counter
} cache;

//REED contact is connected to GPIO4 (Pin: D12)
#define REED_GPIO 4
#define REED_DEEPSLEEP_PIN GPIO_NUM_4

#define DEBOUNCE_TIME 1*1000000ULL

//periodically wakeup and report battery status
#define LONG_TIME 6*60*60*1000000ULL

/******************************************************************************
Description.: bring the WiFi up
Input Value.: When "tryCachedValuesFirst" is true the function tries to use
              cached values before attempting a scan + association
Return Value: true if WiFi is up, false if it timed out
******************************************************************************/
bool WiFiUP(bool tryCachedValuesFirst) {
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);

  if(tryCachedValuesFirst && cache.channel > 0) {
    Serial.printf("Cached values as follows:\r\n");
    Serial.printf(" Channel....: %d\r\n", cache.channel);
    Serial.printf(" BSSID......: %x:%x:%x:%x:%x:%x\r\n", cache.bssid[0], \
                                                         cache.bssid[1], \
                                                         cache.bssid[2], \
                                                         cache.bssid[3], \
                                                         cache.bssid[4], \
                                                         cache.bssid[5]);

    WiFi.begin(ESSID, PSK, cache.channel, cache.bssid);

    for (unsigned long i=millis(); millis()-i < 10000;) {
      delay(10);

      if (WiFi.status() == WL_CONNECTED) {
        Serial.printf("WiFi connected with cached values (%lu)\r\n", millis()-i);
        return true;
      } 
    }
  }

  cache.channel = 0;
  for (uint32_t i = 0; i < sizeof(cache.bssid); i++)
    cache.bssid[i] = 0;

  // try it with the slower process
  WiFi.begin(ESSID, PSK);

  for (unsigned long i=millis(); millis()-i < 60000;) {
    delay(10);

    if (WiFi.status() == WL_CONNECTED) {
      Serial.printf("WiFi connected (%lu)\r\n", millis()-i);

      uint8_t *bssid = WiFi.BSSID();
      for (uint32_t i = 0; i < sizeof(cache.bssid); i++)
        cache.bssid[i] = bssid[i];
      cache.channel = WiFi.channel();

      return true;
    }
  }

  Serial.printf("WiFi NOT connected\r\n");
  return false;
}

/******************************************************************************
Description.: reads the battery voltage through the voltage divider at GPIO34
              if the ESP32-E has calibration eFused those will be used.
              In comparison with a regular voltmeter the values of ESP32 and
              multimeter differ only about 0.05V
Input Value.: -
Return Value: battery voltage in volts
******************************************************************************/
float readBattery() {
  uint32_t value = 0;
  int rounds = 11;
  esp_adc_cal_characteristics_t adc_chars;

  //battery voltage divided by 2 can be measured at GPIO34, which equals ADC1_CHANNEL6
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
  switch(esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars)) {
    case ESP_ADC_CAL_VAL_EFUSE_TP:
      Serial.println("Characterized using Two Point Value");
      break;
    case ESP_ADC_CAL_VAL_EFUSE_VREF:
      Serial.printf("Characterized using eFuse Vref (%d mV)\r\n", adc_chars.vref);
      break;
    default:
      Serial.printf("Characterized using Default Vref (%d mV)\r\n", 1100);
  }

  //to avoid noise, sample the pin several times and average the result
  for(int i=1; i<=rounds; i++) {
    value += adc1_get_raw(ADC1_CHANNEL_6);
  }
  value /= (uint32_t)rounds;

  //due to the voltage divider (1M+1M) values must be multiplied by 2
  //and convert mV to V
  return esp_adc_cal_raw_to_voltage(value, &adc_chars)*2.0/1000.0;
}

/******************************************************************************
Description.: send MQTT message
Input Value.: msg selects the message to send
Return Value: true if OK, false if errors occured
******************************************************************************/
bool SendMessage(enum _message msg) {
  char buf[256] = {0};

  //read RTC
  struct timeval tv;
  gettimeofday(&tv, NULL);

  //establish connection to MQTT server
  WiFiClient net;
  MQTTClient MQTTClient(256);
  MQTTClient.begin(MQTTServer, MQTTPort, net);
  MQTTClient.setOptions(30, true, 5000);

  if( MQTTClient.connect(MQTTDeviceName.c_str(), MQTTUsername.c_str(), MQTTPassword.c_str())) {
    switch(msg) {
      case M_COUNTER:
        Serial.printf("Sending counter: %d\r\n", cache.counter);

        MQTTClient.publish(MQTTRootTopic+"/counter", String(cache.counter), true, 2);
        break;
      case M_STATUS:
        Serial.printf("Sending status\r\n");

        MQTTClient.publish(MQTTRootTopic+"/counter", String(cache.counter), true, 2);
        MQTTClient.publish(MQTTRootTopic+"/BatteryVoltage", String(cache.BatteryVoltage, 3), false, 2);
        snprintf(buf, sizeof(buf)-1, "%ld.%06ld", tv.tv_sec, tv.tv_usec);
        MQTTClient.publish(MQTTRootTopic+"/BatteryRuntime", buf, false, 2);
        snprintf(buf, sizeof(buf)-1, "%llu", cache.NumberOfRestarts);
        MQTTClient.publish(MQTTRootTopic+"/Restarts", buf, false, 2);
        snprintf(buf, sizeof(buf)-1, "%llu", cache.ActiveTime);
        MQTTClient.publish(MQTTRootTopic+"/ActiveTime", buf, false, 2);
        snprintf(buf, sizeof(buf)-1, "%ld", WiFi.RSSI());
        MQTTClient.publish(MQTTRootTopic+"/RSSI", buf, false, 2);
        break;
      default:
        Serial.printf("unkown message, not sending anything\r\n");
    }

    MQTTClient.disconnect();
    return true;
  }

  return false;
}

/******************************************************************************
Description.: get the counter from MQTT, if it is retained we use it as start
Input Value.: timeout in ms
Return Value: true if OK, false if errors occured
******************************************************************************/
bool GetCounter(unsigned int timeout) {
  char buf[256] = {0};
  bool gotCounter = false;

  //establish connection to MQTT server
  WiFiClient net;
  MQTTClient MQTTClient(256);
  MQTTClient.begin(MQTTServer, MQTTPort, net);
  MQTTClient.setOptions(30, true, 5000);

  //callback as lambda-function, capture gotCounter by reference to signal when done
  MQTTClient.onMessage((MQTTClientCallbackSimpleFunction)([&gotCounter](String &topic, String &payload) -> void {
    Serial.println("Received MQTT message: " + topic + " - " + payload);

    if ( topic == MQTTRootTopic+"/counter") {
      cache.counter = strtoull(payload.c_str(), NULL, 10);
      gotCounter = true;
    }

    return;
  }));

  if( MQTTClient.connect(MQTTDeviceName.c_str(), MQTTUsername.c_str(), MQTTPassword.c_str()) ) {
    MQTTClient.subscribe(MQTTRootTopic+"/counter");

    for (int i=0; i<=timeout; i++) {
      MQTTClient.loop();
      if(gotCounter) {
        Serial.println("received counter, will use it");
        break;
      }
      usleep(1000);
    }

    MQTTClient.disconnect();
    return true;
  }

  return false;
}

/******************************************************************************
Description.: since this is a battery sensor, everything happens in setup
              and when the tonguing' is done the device enters deep-sleep
Input Value.: -
Return Value: -
******************************************************************************/
void setup() {
  //visual feedback when we are active, turn on onboard LED
  pinMode(2, OUTPUT);
  digitalWrite(2, HIGH);

  cache.NumberOfRestarts++;

  Serial.begin(115200);
  Serial.print("===================================================\r\n");
  Serial.printf("FireBeetle active\r\n" \
                " Compiled at: " __DATE__ " - " __TIME__ "\r\n" \
                " ESP-IDF: %s\r\n", esp_get_idf_version());

  //read battery voltage
  cache.BatteryVoltage = readBattery();
  Serial.printf("Voltage: %4.3f V\r\n", cache.BatteryVoltage);

  //a reset is required to wakeup again from below CRITICALLY_LOW_BATTERY_VOLTAGE
  //this is to prevent damaging the empty battery by saving as much power as possible
  if (cache.BatteryVoltage < CRITICALLY_LOW_BATTERY_VOLTAGE) {
    Serial.println("Battery critically low, hibernating...");

    //switch off everything that might consume power
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_VDDSDIO, ESP_PD_OPTION_OFF);
    //esp_sleep_pd_config(ESP_PD_DOMAIN_CPU, ESP_PD_OPTION_OFF);

    //disable all wakeup sources
    esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);

    cache.ActiveTime += millis();
    digitalWrite(2, LOW);
    esp_deep_sleep_start();

    Serial.println("This should never get printed");
    return;
  }

  //if battery is below LOW_BATTERY_VOLTAGE but still above CRITICALLY_LOW_BATTERY_VOLTAGE, 
  //stop doing the regular work
  //when put on charge the device will wakeup after a while and recognize voltage is OK
  //this way the battery can run low, put still wakeup without physical interaction
  if (cache.BatteryVoltage < LOW_BATTERY_VOLTAGE) {
    Serial.println("Battery low, deep sleeping...");

    //sleep ~60 minutes if battery is CRITICALLY_LOW_BATTERY_VOLTAGE to VERY_LOW_BATTERY_VOLTAGE
    //sleep ~10 minutes if battery is VERY_LOW_BATTERY_VOLTAGE to LOW_BATTERY_VOLTAGE
    uint64_t sleeptime = (cache.BatteryVoltage >= VERY_LOW_BATTERY_VOLTAGE) ? \
                           10*60*1000000ULL : 60*60*1000000ULL;

    esp_sleep_enable_timer_wakeup(sleeptime);
    cache.ActiveTime += millis();
    digitalWrite(2, LOW);
    esp_deep_sleep_start();

    Serial.println("This should never get printed");
    return;
  }

  //read GPIO level
  pinMode(REED_GPIO, INPUT_PULLUP);
  int level = digitalRead(REED_GPIO);
  Serial.printf("Reed-switch is: %s\r\n", (level)?"NOMAGNET":"MAGNET");

  //check if a reset/power-on occured
  if (esp_reset_reason() == ESP_RST_POWERON) {
      Serial.printf("ESP was just switched ON\r\n");
      cache.state = S_STARTUP;
      cache.ActiveTime = 0;
      cache.NumberOfRestarts = 0;
      cache.counter = 0;

      //set RTC to 0
      struct timeval tv;
      tv.tv_sec = 0;
      tv.tv_usec = 0;
      settimeofday(&tv, NULL);

      //default is to have WiFi off
      if (WiFi.getMode() != WIFI_OFF) {
        Serial.printf("WiFi wasn't off!\r\n");
        WiFi.persistent(true);
        WiFi.mode(WIFI_OFF);
      }

      //try to connect to WiFi
      WiFiUP(false);
      //retrieve the previous gasmeter-counter, will return quickly if it is retained
      GetCounter(10*1000);
      WiFi.disconnect(true, true);

      //transition to new state
      cache.state = (level == HIGH) ? S_DEBOUNCE_HIGH : S_DEBOUNCE_LOW;
      Serial.printf("transition to state nr.: %d\r\n", cache.state);
      //wake by timer after debounce time is up
      esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
  }

  // check if ESP returns from deepsleep
  if (esp_reset_reason() == ESP_RST_DEEPSLEEP) {
    switch(esp_sleep_get_wakeup_cause()) {
      case ESP_SLEEP_WAKEUP_TIMER:
        Serial.printf("ESP woke up due to timer\r\n");

        switch(cache.state) {
          case S_DEBOUNCE_LOW:
          case S_DEBOUNCE_HIGH:
            if(level == LOW && cache.state == S_DEBOUNCE_LOW) { 
              cache.counter++;
              WiFiUP(true);
              SendMessage(M_COUNTER);
              WiFi.disconnect(true, true);
            }

            Serial.printf("transition to state S_IDLE\r\n");
            cache.state = S_IDLE;
            esp_sleep_enable_timer_wakeup(LONG_TIME);
            esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, (level == LOW)?1:0);
            break;

          case S_IDLE:
            WiFiUP(true);
            SendMessage(M_STATUS);
            WiFi.disconnect(true, true);

            Serial.printf("remaining in state S_IDLE\r\n");
            cache.state = S_IDLE;
            esp_sleep_enable_timer_wakeup(LONG_TIME);
            esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, (level == LOW)?1:0);
            break;

          default:
            Serial.printf("ESP woke up by timer in an unknown state\r\n");
        }
        break;

      case ESP_SLEEP_WAKEUP_EXT0:
        Serial.printf("ESP woke up by EXT0\r\n");

        if (level == HIGH) {
          cache.state = S_DEBOUNCE_HIGH;
          Serial.printf("transition to state S_DEBOUNCE_HIGH\r\n");
        }
        else {
          cache.state = S_DEBOUNCE_LOW;
          Serial.printf("transition to state S_DEBOUNCE_LOW\r\n");
        }

        esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
        break;

      default:
        Serial.printf("ESP woke up due to an unknown reason\r\n");
    }
  }

  Serial.printf("counter: %llu\r\n", cache.counter);
  Serial.printf("=== entering deepsleep after %d ms ===\r\n", millis());
  cache.ActiveTime += millis();
  digitalWrite(2, LOW);
  esp_deep_sleep_start();

  Serial.println("This should never get printed");
}

void loop() {
  Serial.println("This should never get printed");
}
VoyteckPL commented 1 year ago

image Damn, it is fast!

Still not sending full MQTT data (voltage etc.) ;) I had to at this:

image

Also one bug - when power is lost and reed switch is in MAGNET state 1 unit gets added to counter.

VoyteckPL commented 1 year ago

I also wonder what happends when MQTT server will be down (due to electricity failure). What will happen with retain message with current counter value. Wouldn't it be better to store it in EEPROM?

Torxgewinde commented 1 year ago

Not sending the full details is to send as little as possible (to preserve battery). I implemented now:

The slight error after power-on is not worth the effort to get rid of it. Replacing the battery (hopefully) happens once or twice per year and miscounting by one under certain circumstances is not worth the effort IMHO.

If the MQTT-broker gets down the ESP32 is still keeping the counter. The ESP32 only "learns" the retained value at first-start (=power-cycle or reset). Every time the ESP32 transmits the counter, it is now "retained", thus refreshed from the ESP32. To really loose the broker value and the ESP32, both must be restarted at more or less the same time. Since I plan to have the ESP32 running from battery, this would be a rare coincidence --> Acceptable IMHO.

/*******************************************************************************
#                                                                              #
# Using the Firebeetle 2 ESP32-E as battery powered gasmeter-reader            #
# Project: https://github.com/Torxgewinde/Firebeetle-2-ESP32-E                 # 
#                                                                              #
# Firebeetle documentation at:                                                 #
# https://wiki.dfrobot.com/FireBeetle_Board_ESP32_E_SKU_DFR0654                #                                                             #
#                                                                              # 
# Copyright (C) 2022 Tom Stöveken                                              #
#                                                                              #
# This program is free software; you can redistribute it and/or modify         #
# it under the terms of the GNU General Public License as published by         #
# the Free Software Foundation; version 2 of the License.                      #
#                                                                              #
# This program is distributed in the hope that it will be useful,              #
# but WITHOUT ANY WARRANTY; without even the implied warranty of               #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                #
# GNU General Public License for more details.                                 #
#                                                                              #
# You should have received a copy of the GNU General Public License            #
# along with this program; if not, write to the Free Software                  #
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA    #
#                                                                              #
********************************************************************************/

#include <WiFi.h>
#include <MQTT.h>
#include <stdlib.h> 

#include "esp_adc_cal.h"

#define ESSID ""
#define PSK   ""

#define LOW_BATTERY_VOLTAGE 3.20
#define VERY_LOW_BATTERY_VOLTAGE 3.10
#define CRITICALLY_LOW_BATTERY_VOLTAGE 3.00

//char *MQTTServer = "server.lan";
IPAddress MQTTServer = IPAddress(192,168,1,1);
uint16_t MQTTPort = 1883;
String MQTTUsername = "";
String MQTTPassword = "";
String MQTTDeviceName = "Gaszaehler";
String MQTTRootTopic = "Keller/Gaszaehler";

enum _state {
  S_STARTUP = 0,
  S_DEBOUNCE_LOW,
  S_DEBOUNCE_HIGH,
  S_IDLE
};

enum _message {
  M_COUNTER = 0,
  M_STATUS
};

RTC_NOINIT_ATTR struct {
  uint8_t bssid[6];
  uint8_t channel;

  float BatteryVoltage;      //battery voltage in V
  uint64_t NumberOfRestarts; //number of restarts
  uint64_t ActiveTime;       //time being active in ms

  enum _state state;         //keep track of current state

  uint64_t counter;          //gasmeter-counter
} cache;

//REED contact is connected to GPIO4 (Pin: D12)
#define REED_GPIO 4
#define REED_DEEPSLEEP_PIN GPIO_NUM_4

#define DEBOUNCE_TIME 1*1000000ULL

//periodically wakeup and report battery status
#define LONG_TIME 60*60*1000000ULL

/******************************************************************************
Description.: bring the WiFi up
Input Value.: When "tryCachedValuesFirst" is true the function tries to use
              cached values before attempting a scan + association
Return Value: true if WiFi is up, false if it timed out
******************************************************************************/
bool WiFiUP(bool tryCachedValuesFirst) {
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);

  if(tryCachedValuesFirst && cache.channel > 0) {
    Serial.printf("Cached values as follows:\r\n");
    Serial.printf(" Channel....: %d\r\n", cache.channel);
    Serial.printf(" BSSID......: %x:%x:%x:%x:%x:%x\r\n", cache.bssid[0], \
                                                         cache.bssid[1], \
                                                         cache.bssid[2], \
                                                         cache.bssid[3], \
                                                         cache.bssid[4], \
                                                         cache.bssid[5]);

    WiFi.begin(ESSID, PSK, cache.channel, cache.bssid);

    for (unsigned long i=millis(); millis()-i < 10000;) {
      delay(10);

      if (WiFi.status() == WL_CONNECTED) {
        Serial.printf("WiFi connected with cached values (%lu)\r\n", millis()-i);
        return true;
      } 
    }
  }

  cache.channel = 0;
  for (uint32_t i = 0; i < sizeof(cache.bssid); i++)
    cache.bssid[i] = 0;

  // try it with the slower process
  WiFi.begin(ESSID, PSK);

  for (unsigned long i=millis(); millis()-i < 60000;) {
    delay(10);

    if (WiFi.status() == WL_CONNECTED) {
      Serial.printf("WiFi connected (%lu)\r\n", millis()-i);

      uint8_t *bssid = WiFi.BSSID();
      for (uint32_t i = 0; i < sizeof(cache.bssid); i++)
        cache.bssid[i] = bssid[i];
      cache.channel = WiFi.channel();

      return true;
    }
  }

  Serial.printf("WiFi NOT connected\r\n");
  return false;
}

/******************************************************************************
Description.: reads the battery voltage through the voltage divider at GPIO34
              if the ESP32-E has calibration eFused those will be used.
              In comparison with a regular voltmeter the values of ESP32 and
              multimeter differ only about 0.05V
Input Value.: -
Return Value: battery voltage in volts
******************************************************************************/
float readBattery() {
  uint32_t value = 0;
  int rounds = 11;
  esp_adc_cal_characteristics_t adc_chars;

  //battery voltage divided by 2 can be measured at GPIO34, which equals ADC1_CHANNEL6
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
  switch(esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars)) {
    case ESP_ADC_CAL_VAL_EFUSE_TP:
      Serial.println("Characterized using Two Point Value");
      break;
    case ESP_ADC_CAL_VAL_EFUSE_VREF:
      Serial.printf("Characterized using eFuse Vref (%d mV)\r\n", adc_chars.vref);
      break;
    default:
      Serial.printf("Characterized using Default Vref (%d mV)\r\n", 1100);
  }

  //to avoid noise, sample the pin several times and average the result
  for(int i=1; i<=rounds; i++) {
    value += adc1_get_raw(ADC1_CHANNEL_6);
  }
  value /= (uint32_t)rounds;

  //due to the voltage divider (1M+1M) values must be multiplied by 2
  //and convert mV to V
  return esp_adc_cal_raw_to_voltage(value, &adc_chars)*2.0/1000.0;
}

/******************************************************************************
Description.: send MQTT message
Input Value.: msg selects the message to send
Return Value: true if OK, false if errors occured
******************************************************************************/
bool SendMessage(enum _message msg) {
  char buf[256] = {0};

  //read RTC
  struct timeval tv;
  gettimeofday(&tv, NULL);

  //establish connection to MQTT server
  WiFiClient net;
  MQTTClient MQTTClient(256);
  MQTTClient.begin(MQTTServer, MQTTPort, net);
  MQTTClient.setOptions(30, true, 5000);

  if( MQTTClient.connect(MQTTDeviceName.c_str(), MQTTUsername.c_str(), MQTTPassword.c_str())) {
    switch(msg) {
      case M_COUNTER:
        Serial.printf("Sending counter: %d\r\n", cache.counter);

        MQTTClient.publish(MQTTRootTopic+"/counter", String(cache.counter), true, 2);
        snprintf(buf, sizeof(buf)-1, "%.2f", cache.counter/100.0);
        MQTTClient.publish(MQTTRootTopic+"/qubicmeter", buf, false, 2);
        break;
      case M_STATUS:
        Serial.printf("Sending status\r\n");

        MQTTClient.publish(MQTTRootTopic+"/counter", String(cache.counter), true, 2);
        snprintf(buf, sizeof(buf)-1, "%.2f", cache.counter/100.0);
        MQTTClient.publish(MQTTRootTopic+"/qubicmeter", buf, false, 2);
        MQTTClient.publish(MQTTRootTopic+"/BatteryVoltage", String(cache.BatteryVoltage, 3), false, 2);
        snprintf(buf, sizeof(buf)-1, "%ld.%06ld", tv.tv_sec, tv.tv_usec);
        MQTTClient.publish(MQTTRootTopic+"/BatteryRuntime", buf, false, 2);
        snprintf(buf, sizeof(buf)-1, "%llu", cache.NumberOfRestarts);
        MQTTClient.publish(MQTTRootTopic+"/Restarts", buf, false, 2);
        snprintf(buf, sizeof(buf)-1, "%llu", cache.ActiveTime);
        MQTTClient.publish(MQTTRootTopic+"/ActiveTime", buf, false, 2);
        snprintf(buf, sizeof(buf)-1, "%ld", WiFi.RSSI());
        MQTTClient.publish(MQTTRootTopic+"/RSSI", buf, false, 2);
        break;
      default:
        Serial.printf("unkown message, not sending anything\r\n");
    }

    MQTTClient.disconnect();
    return true;
  }

  return false;
}

/******************************************************************************
Description.: get the counter from MQTT, if it is retained we use it as start
Input Value.: timeout in ms
Return Value: true if OK, false if errors occured
******************************************************************************/
bool GetCounter(unsigned int timeout) {
  char buf[256] = {0};
  bool gotCounter = false;

  //establish connection to MQTT server
  WiFiClient net;
  MQTTClient MQTTClient(256);
  MQTTClient.begin(MQTTServer, MQTTPort, net);
  MQTTClient.setOptions(30, true, 5000);

  //callback as lambda-function, capture gotCounter by reference to signal when done
  MQTTClient.onMessage((MQTTClientCallbackSimpleFunction)([&gotCounter](String &topic, String &payload) -> void {
    Serial.println("Received MQTT message: " + topic + " - " + payload);

    if ( topic == MQTTRootTopic+"/counter") {
      cache.counter = strtoull(payload.c_str(), NULL, 10);
      gotCounter = true;
    }

    return;
  }));

  if( MQTTClient.connect(MQTTDeviceName.c_str(), MQTTUsername.c_str(), MQTTPassword.c_str()) ) {
    MQTTClient.subscribe(MQTTRootTopic+"/counter");

    for (int i=0; i<=timeout; i++) {
      MQTTClient.loop();
      if(gotCounter) {
        Serial.println("received counter, will use it");
        break;
      }
      usleep(1000);
    }

    MQTTClient.disconnect();
    return true;
  }

  return false;
}

/******************************************************************************
Description.: since this is a battery sensor, everything happens in setup
              and when the tonguing' is done the device enters deep-sleep
Input Value.: -
Return Value: -
******************************************************************************/
void setup() {
  //visual feedback when we are active, turn on onboard LED
  pinMode(2, OUTPUT);
  digitalWrite(2, HIGH);

  cache.NumberOfRestarts++;

  Serial.begin(115200);
  Serial.print("===================================================\r\n");
  Serial.printf("FireBeetle active\r\n" \
                " Compiled at: " __DATE__ " - " __TIME__ "\r\n" \
                " ESP-IDF: %s\r\n", esp_get_idf_version());

  //read battery voltage
  cache.BatteryVoltage = readBattery();
  Serial.printf("Voltage: %4.3f V\r\n", cache.BatteryVoltage);

  //a reset is required to wakeup again from below CRITICALLY_LOW_BATTERY_VOLTAGE
  //this is to prevent damaging the empty battery by saving as much power as possible
  if (cache.BatteryVoltage < CRITICALLY_LOW_BATTERY_VOLTAGE) {
    Serial.println("Battery critically low, hibernating...");

    //switch off everything that might consume power
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_VDDSDIO, ESP_PD_OPTION_OFF);
    //esp_sleep_pd_config(ESP_PD_DOMAIN_CPU, ESP_PD_OPTION_OFF);

    //disable all wakeup sources
    esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);

    cache.ActiveTime += millis();
    digitalWrite(2, LOW);
    esp_deep_sleep_start();

    Serial.println("This should never get printed");
    return;
  }

  //if battery is below LOW_BATTERY_VOLTAGE but still above CRITICALLY_LOW_BATTERY_VOLTAGE, 
  //stop doing the regular work
  //when put on charge the device will wakeup after a while and recognize voltage is OK
  //this way the battery can run low, put still wakeup without physical interaction
  if (cache.BatteryVoltage < LOW_BATTERY_VOLTAGE) {
    Serial.println("Battery low, deep sleeping...");

    //sleep ~60 minutes if battery is CRITICALLY_LOW_BATTERY_VOLTAGE to VERY_LOW_BATTERY_VOLTAGE
    //sleep ~10 minutes if battery is VERY_LOW_BATTERY_VOLTAGE to LOW_BATTERY_VOLTAGE
    uint64_t sleeptime = (cache.BatteryVoltage >= VERY_LOW_BATTERY_VOLTAGE) ? \
                           10*60*1000000ULL : 60*60*1000000ULL;

    esp_sleep_enable_timer_wakeup(sleeptime);
    cache.ActiveTime += millis();
    digitalWrite(2, LOW);
    esp_deep_sleep_start();

    Serial.println("This should never get printed");
    return;
  }

  //read GPIO level
  pinMode(REED_GPIO, INPUT_PULLUP);
  int level = digitalRead(REED_GPIO);
  Serial.printf("Reed-switch is: %s\r\n", (level)?"NOMAGNET":"MAGNET");

  //check if a reset/power-on occured
  if (esp_reset_reason() == ESP_RST_POWERON) {
      Serial.printf("ESP was just switched ON\r\n");
      cache.state = S_STARTUP;
      cache.ActiveTime = 0;
      cache.NumberOfRestarts = 0;
      cache.counter = 0;

      //set RTC to 0
      struct timeval tv;
      tv.tv_sec = 0;
      tv.tv_usec = 0;
      settimeofday(&tv, NULL);

      //default is to have WiFi off
      if (WiFi.getMode() != WIFI_OFF) {
        Serial.printf("WiFi wasn't off!\r\n");
        WiFi.persistent(true);
        WiFi.mode(WIFI_OFF);
      }

      //try to connect to WiFi
      WiFiUP(false);
      //retrieve the previous gasmeter-counter, will return quickly if it is retained
      GetCounter(10*1000);
      WiFi.disconnect(true, true);

      //transition to new state
      cache.state = (level == HIGH) ? S_DEBOUNCE_HIGH : S_DEBOUNCE_LOW;
      Serial.printf("transition to state nr.: %d\r\n", cache.state);
      //wake by timer after debounce time is up
      esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
  }

  // check if ESP returns from deepsleep
  if (esp_reset_reason() == ESP_RST_DEEPSLEEP) {
    switch(esp_sleep_get_wakeup_cause()) {
      case ESP_SLEEP_WAKEUP_TIMER:
        Serial.printf("ESP woke up due to timer\r\n");

        switch(cache.state) {
          case S_DEBOUNCE_LOW:
          case S_DEBOUNCE_HIGH:
            if(level == LOW && cache.state == S_DEBOUNCE_LOW) { 
              cache.counter++;
              WiFiUP(true);
              (cache.counter % 10 == 0) ? SendMessage(M_STATUS) : SendMessage(M_COUNTER);
              WiFi.disconnect(true, true);
            }

            Serial.printf("transition to state S_IDLE\r\n");
            cache.state = S_IDLE;
            esp_sleep_enable_timer_wakeup(LONG_TIME);
            esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, (level == LOW)?1:0);
            break;

          case S_IDLE:
            WiFiUP(true);
            SendMessage(M_STATUS);
            WiFi.disconnect(true, true);

            Serial.printf("remaining in state S_IDLE\r\n");
            cache.state = S_IDLE;
            esp_sleep_enable_timer_wakeup(LONG_TIME);
            esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, (level == LOW)?1:0);
            break;

          default:
            Serial.printf("ESP woke up by timer in an unknown state\r\n");
        }
        break;

      case ESP_SLEEP_WAKEUP_EXT0:
        Serial.printf("ESP woke up by EXT0\r\n");

        if (level == HIGH) {
          cache.state = S_DEBOUNCE_HIGH;
          Serial.printf("transition to state S_DEBOUNCE_HIGH\r\n");
        }
        else {
          cache.state = S_DEBOUNCE_LOW;
          Serial.printf("transition to state S_DEBOUNCE_LOW\r\n");
        }

        esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
        break;

      default:
        Serial.printf("ESP woke up due to an unknown reason\r\n");
    }
  }

  Serial.printf("counter: %llu\r\n", cache.counter);
  Serial.printf("=== entering deepsleep after %d ms ===\r\n", millis());
  cache.ActiveTime += millis();
  digitalWrite(2, LOW);
  esp_deep_sleep_start();

  Serial.println("This should never get printed");
}

void loop() {
  Serial.println("This should never get printed");
}
VoyteckPL commented 1 year ago

I'm impressed. I will connect it to my gas meter soon and run some tests 🙂 is it possible to send mqtt message with current gas meter value so it is possible to sync current state?

Torxgewinde commented 1 year ago

Yes, to start from a certain value:

VoyteckPL commented 1 year ago

Superb. Thank you very much for this!

VoyteckPL commented 1 year ago

Just one more thing. If esp is closed in a box or similar update of current value could be problematic... You need access to dismantle battery for update. Maybe a separate message could be sent with update value and after update it could be set to 0 (and zero would be ignored by software as update value). I hope you get my idea

VoyteckPL commented 1 year ago

Regarding real life usage of gas. In my case it is ~ 1000 m3 a year. It can between 800 m3 and 1200 m3 depending on conditions. So to calculate it per impulse ~ 10.000 impulses a year. Not sure how to count estimated life time. I will be using 2 or maybe even 1 pcs of 18650 which has total capacity 3450 mAh. I've seen your charts and with that kind of usage it looks very optimistic.

Torxgewinde commented 1 year ago

Ignoring the wakeups of ~60ms for maintaining the counter and just focusing on the transmissions, a 2000 mAh battery should be good for ~70.000 transmissions. Over the thumb I would half the value and would expect it to last for ~35.000 impulses: image

A rule of thumb: Put in as much batteries as fits and what is affordable, it is never too much :)

To update the counter from the broker: How about inserting a power switch into the battery line? That way the ESP32 can easily be power-cycled and the wiring for the battery is needed anyway. It will consume zero energy in addition and is quite intuitive and easy to understand.

VoyteckPL commented 1 year ago

I totally understand 🙂 but my gas meter is 30m from my house so in case I want to adjust counter I will have to walk so far 😆 i also thought about switch

Torxgewinde commented 1 year ago

Ok, the function GetCounter(10*1000) gets the counter from the broker. You could make it look for the value before each transmission:

...
if(level == LOW && cache.state == S_DEBOUNCE_LOW) {
              WiFiUP(true);
              GetCounter(10*1000);
              cache.counter++;
              (cache.counter % 10 == 0) ? SendMessage(M_STATUS) : SendMessage(M_COUNTER);
              WiFi.disconnect(true, true);
            }
...

I would try to get along without reading the value, as it extends the active-time significantly. In your case it might be needed for convenience.

Two more thoughts:

VoyteckPL commented 1 year ago

Never heard of these batteries. Are there any compact sizes? I have 2x18650 Sanyo ga.

VoyteckPL commented 1 year ago

IMG_20230108_161309 Test setup running 🙂🙂🙂

Torxgewinde commented 1 year ago

I would say, looks good. Mine is now also in place and happily counting and logging. We will see how well the battery lasts. I mounted a 2000 mAh LiPo-Pouch just like for the PIR sensor.

VoyteckPL commented 1 year ago

I leave it for 2 weeks like this. Voltage is 4.1

VoyteckPL commented 1 year ago

Btw do you know a good way to add external antenna to Firebeetle?

VoyteckPL commented 1 year ago

Cut the "low-power" pad to the on-board RGB LED, otherwise the Firebeetle idle current is not low.

I don't get it... What has to be done? I can't find any guides online..

Torxgewinde commented 1 year ago

https://wiki.dfrobot.com/FireBeetle_Board_ESP32_E_SKU_DFR0654 --> Low Power Pad: This pad is specially designed for low power consumption. It is connected by default. You can cut off the thin wire in the middle with a knife to disconnect it. After disconnection, the static power consumption can be reduced by 500 μA. The power consumption can be reduced to 13 μA after controlling the maincontroller enter the sleep mode through the program. Note: when the pad is disconnected, you can only drive RGB LED light via the USB Power supply.

image

VoyteckPL commented 1 year ago

Thank you.

Torxgewinde commented 1 year ago

Good news: It counts and runs from battery, Bad news: it misses a few revolutions. I am afraid there is still something to be done to read the reed-switch properly.

VoyteckPL commented 1 year ago

Mine is in sync so far

Torxgewinde commented 1 year ago

In my case it was missing some counts when the gas-heater was ramping up to the maximum power. For such cases I had to reduce the BOUNCE_TIME to 500msec. This might be a value that needs to be adjusted to each reed-setup and gas-heater. If your setup is working there is no need to change it, in my case it was necessary.

Here is the sketch in its current revision. I also implemented the reaction to all states I could imagine and coded it very verbose to not miss a state:

/*******************************************************************************
#                                                                              #
# Using the Firebeetle 2 ESP32-E as battery powered gasmeter-reader            #
# Project: https://github.com/Torxgewinde/Firebeetle-2-ESP32-E                 # 
#                                                                              #
# Firebeetle documentation at:                                                 #
# https://wiki.dfrobot.com/FireBeetle_Board_ESP32_E_SKU_DFR0654                #
#                                                                              # 
# Copyright (C) 2022 Tom Stöveken                                              #
#                                                                              #
# This program is free software; you can redistribute it and/or modify         #
# it under the terms of the GNU General Public License as published by         #
# the Free Software Foundation; version 2 of the License.                      #
#                                                                              #
# This program is distributed in the hope that it will be useful,              #
# but WITHOUT ANY WARRANTY; without even the implied warranty of               #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                #
# GNU General Public License for more details.                                 #
#                                                                              #
# You should have received a copy of the GNU General Public License            #
# along with this program; if not, write to the Free Software                  #
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA    #
#                                                                              #
********************************************************************************/

#include <WiFi.h>
#include <MQTT.h>
#include <stdlib.h> 

#include "esp_adc_cal.h"
#include "driver/rtc_io.h"

#define ESSID "My Wifi SSID"
#define PSK   "My Wifi Password"

#define LOW_BATTERY_VOLTAGE 3.20
#define VERY_LOW_BATTERY_VOLTAGE 3.10
#define CRITICALLY_LOW_BATTERY_VOLTAGE 3.00

//char *MQTTServer = "server.lan";
IPAddress MQTTServer = IPAddress(192,168,1,1);
uint16_t MQTTPort = 1883;
String MQTTUsername = "username";
String MQTTPassword = "password";
String MQTTDeviceName = "Gaszaehler";
String MQTTRootTopic = "Keller/Gaszaehler";

enum _state {
  S_STARTUP = 0,
  S_DEBOUNCE_LOW,
  S_DEBOUNCE_HIGH,
  S_LOW,
  S_HIGH
};

enum _message {
  M_COUNTER = 0,
  M_STATUS
};

RTC_NOINIT_ATTR struct {
  uint8_t bssid[6];
  uint8_t channel;

  float BatteryVoltage;      //battery voltage in V
  uint64_t NumberOfRestarts; //number of restarts
  uint64_t ActiveTime;       //time being active in ms

  enum _state state;         //keep track of current state

  uint64_t counter;          //gasmeter-counter
} cache;

//REED contact is connected to GPIO4 (Pin: D12)
#define REED_GPIO 4
#define REED_DEEPSLEEP_PIN GPIO_NUM_4

//Time the reed-switch must remain in a certain state before level considered valid
//#define DEBOUNCE_TIME 1*1000000ULL // 1000 msec
#define DEBOUNCE_TIME 500*1000ULL //500 msec

//periodically wakeup and report battery status
#define LONG_TIME 2*60*60*1000000ULL // 2h

/******************************************************************************
Description.: bring the WiFi up
Input Value.: When "tryCachedValuesFirst" is true the function tries to use
              cached values before attempting a scan + association
Return Value: true if WiFi is up, false if it timed out
******************************************************************************/
bool WiFiUP(bool tryCachedValuesFirst) {
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);

  if(tryCachedValuesFirst && cache.channel > 0) {
    Serial.printf("Cached values as follows:\r\n");
    Serial.printf(" Channel....: %d\r\n", cache.channel);
    Serial.printf(" BSSID......: %x:%x:%x:%x:%x:%x\r\n", cache.bssid[0], \
                                                         cache.bssid[1], \
                                                         cache.bssid[2], \
                                                         cache.bssid[3], \
                                                         cache.bssid[4], \
                                                         cache.bssid[5]);

    WiFi.begin(ESSID, PSK, cache.channel, cache.bssid);

    for (unsigned long i=millis(); millis()-i < 10000;) {
      delay(10);

      if (WiFi.status() == WL_CONNECTED) {
        Serial.printf("WiFi connected with cached values (%lu)\r\n", millis()-i);
        return true;
      } 
    }
  }

  cache.channel = 0;
  for (uint32_t i = 0; i < sizeof(cache.bssid); i++)
    cache.bssid[i] = 0;

  // try it with the slower process
  WiFi.begin(ESSID, PSK);

  for (unsigned long i=millis(); millis()-i < 60000;) {
    delay(10);

    if (WiFi.status() == WL_CONNECTED) {
      Serial.printf("WiFi connected (%lu)\r\n", millis()-i);

      uint8_t *bssid = WiFi.BSSID();
      for (uint32_t i = 0; i < sizeof(cache.bssid); i++)
        cache.bssid[i] = bssid[i];
      cache.channel = WiFi.channel();

      return true;
    }
  }

  Serial.printf("WiFi NOT connected\r\n");
  return false;
}

/******************************************************************************
Description.: reads the battery voltage through the voltage divider at GPIO34
              if the ESP32-E has calibration eFused those will be used.
              In comparison with a regular voltmeter the values of ESP32 and
              multimeter differ only about 0.05V
Input Value.: -
Return Value: battery voltage in volts
******************************************************************************/
float readBattery() {
  uint32_t value = 0;
  int rounds = 11;
  esp_adc_cal_characteristics_t adc_chars;

  //battery voltage divided by 2 can be measured at GPIO34, which equals ADC1_CHANNEL6
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
  switch(esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars)) {
    case ESP_ADC_CAL_VAL_EFUSE_TP:
      Serial.println("Characterized using Two Point Value");
      break;
    case ESP_ADC_CAL_VAL_EFUSE_VREF:
      Serial.printf("Characterized using eFuse Vref (%d mV)\r\n", adc_chars.vref);
      break;
    default:
      Serial.printf("Characterized using Default Vref (%d mV)\r\n", 1100);
  }

  //to avoid noise, sample the pin several times and average the result
  for(int i=1; i<=rounds; i++) {
    value += adc1_get_raw(ADC1_CHANNEL_6);
  }
  value /= (uint32_t)rounds;

  //due to the voltage divider (1M+1M) values must be multiplied by 2
  //and convert mV to V
  return esp_adc_cal_raw_to_voltage(value, &adc_chars)*2.0/1000.0;
}

/******************************************************************************
Description.: send MQTT message
Input Value.: msg selects the message to send
Return Value: true if OK, false if errors occured
******************************************************************************/
bool SendMessage(enum _message msg) {
  char buf[256] = {0};

  //read RTC
  struct timeval tv;
  gettimeofday(&tv, NULL);

  //establish connection to MQTT server
  WiFiClient net;
  MQTTClient MQTTClient(256);
  MQTTClient.begin(MQTTServer, MQTTPort, net);
  MQTTClient.setOptions(30, true, 5000);

  if( MQTTClient.connect(MQTTDeviceName.c_str(), MQTTUsername.c_str(), MQTTPassword.c_str())) {
    switch(msg) {
      case M_COUNTER:
        Serial.printf("Sending counter: %d\r\n", cache.counter);

        MQTTClient.publish(MQTTRootTopic+"/counter", String(cache.counter), true, 2);
        snprintf(buf, sizeof(buf)-1, "%.2f", cache.counter/100.0);
        MQTTClient.publish(MQTTRootTopic+"/qubicmeter", buf, false, 2);
        break;
      case M_STATUS:
        Serial.printf("Sending status\r\n");

        MQTTClient.publish(MQTTRootTopic+"/counter", String(cache.counter), true, 2);
        snprintf(buf, sizeof(buf)-1, "%.2f", cache.counter/100.0);
        MQTTClient.publish(MQTTRootTopic+"/qubicmeter", buf, false, 2);
        MQTTClient.publish(MQTTRootTopic+"/BatteryVoltage", String(cache.BatteryVoltage, 3), false, 2);
        snprintf(buf, sizeof(buf)-1, "%ld.%06ld", tv.tv_sec, tv.tv_usec);
        MQTTClient.publish(MQTTRootTopic+"/BatteryRuntime", buf, false, 2);
        snprintf(buf, sizeof(buf)-1, "%llu", cache.NumberOfRestarts);
        MQTTClient.publish(MQTTRootTopic+"/Restarts", buf, false, 2);
        snprintf(buf, sizeof(buf)-1, "%llu", cache.ActiveTime);
        MQTTClient.publish(MQTTRootTopic+"/ActiveTime", buf, false, 2);
        snprintf(buf, sizeof(buf)-1, "%ld", WiFi.RSSI());
        MQTTClient.publish(MQTTRootTopic+"/RSSI", buf, false, 2);
        break;
      default:
        Serial.printf("unkown message, not sending anything\r\n");
    }

    MQTTClient.disconnect();
    return true;
  }

  return false;
}

/******************************************************************************
Description.: get the counter from MQTT, if it is retained we use it as start
Input Value.: timeout in ms
Return Value: true if OK, false if errors occured
******************************************************************************/
bool GetCounter(unsigned int timeout) {
  char buf[256] = {0};
  bool gotCounter = false;

  //establish connection to MQTT server
  WiFiClient net;
  MQTTClient MQTTClient(256);
  MQTTClient.begin(MQTTServer, MQTTPort, net);
  MQTTClient.setOptions(30, true, 5000);

  //callback as lambda-function, capture gotCounter by reference to signal when done
  MQTTClient.onMessage((MQTTClientCallbackSimpleFunction)([&gotCounter](String &topic, String &payload) -> void {
    Serial.println("Received MQTT message: " + topic + " - " + payload);

    if ( topic == MQTTRootTopic+"/counter") {
      cache.counter = strtoull(payload.c_str(), NULL, 10);
      gotCounter = true;
    }

    return;
  }));

  if( MQTTClient.connect(MQTTDeviceName.c_str(), MQTTUsername.c_str(), MQTTPassword.c_str()) ) {
    MQTTClient.subscribe(MQTTRootTopic+"/counter");

    for (int i=0; i<=timeout; i++) {
      MQTTClient.loop();
      if(gotCounter) {
        Serial.println("received counter, will use it");
        break;
      }
      usleep(1000);
    }

    MQTTClient.disconnect();
    return true;
  }

  return false;
}

/******************************************************************************
Description.: since this is a battery sensor, everything happens in setup
              and when the tonguing' is done the device enters deep-sleep
Input Value.: -
Return Value: -
******************************************************************************/
void setup() {
  //visual feedback when we are active, turn on onboard LED
  pinMode(2, OUTPUT);
  digitalWrite(2, HIGH);

  cache.NumberOfRestarts++;

  Serial.begin(115200);
  Serial.print("===================================================\r\n");
  Serial.printf("FireBeetle active\r\n" \
                " Compiled at: " __DATE__ " - " __TIME__ "\r\n" \
                " ESP-IDF: %s\r\n", esp_get_idf_version());

  //read battery voltage
  cache.BatteryVoltage = readBattery();
  Serial.printf("Voltage: %4.3f V\r\n", cache.BatteryVoltage);

  //a reset is required to wakeup again from below CRITICALLY_LOW_BATTERY_VOLTAGE
  //this is to prevent damaging the empty battery by saving as much power as possible
  if (cache.BatteryVoltage < CRITICALLY_LOW_BATTERY_VOLTAGE) {
    Serial.println("Battery critically low, hibernating...");

    //switch off everything that might consume power
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_VDDSDIO, ESP_PD_OPTION_OFF);
    //esp_sleep_pd_config(ESP_PD_DOMAIN_CPU, ESP_PD_OPTION_OFF);

    //disable all wakeup sources
    esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);

    cache.ActiveTime += millis();
    digitalWrite(2, LOW);
    esp_deep_sleep_start();

    Serial.println("This should never get printed");
    return;
  }

  //if battery is below LOW_BATTERY_VOLTAGE but still above CRITICALLY_LOW_BATTERY_VOLTAGE, 
  //stop doing the regular work
  //when put on charge the device will wakeup after a while and recognize voltage is OK
  //this way the battery can run low, put still wakeup without physical interaction
  if (cache.BatteryVoltage < LOW_BATTERY_VOLTAGE) {
    Serial.println("Battery low, deep sleeping...");

    //sleep ~60 minutes if battery is CRITICALLY_LOW_BATTERY_VOLTAGE to VERY_LOW_BATTERY_VOLTAGE
    //sleep ~10 minutes if battery is VERY_LOW_BATTERY_VOLTAGE to LOW_BATTERY_VOLTAGE
    uint64_t sleeptime = (cache.BatteryVoltage >= VERY_LOW_BATTERY_VOLTAGE) ? \
                           10*60*1000000ULL : 60*60*1000000ULL;

    esp_sleep_enable_timer_wakeup(sleeptime);
    cache.ActiveTime += millis();
    digitalWrite(2, LOW);
    esp_deep_sleep_start();

    Serial.println("This should never get printed");
    return;
  }

  //configure GPIO of reed-switch
  pinMode(REED_GPIO, INPUT_PULLUP);
  rtc_gpio_pullup_en(REED_DEEPSLEEP_PIN);
  rtc_gpio_pulldown_dis(REED_DEEPSLEEP_PIN);

  //read level of reed-switch
  int level = digitalRead(REED_GPIO);
  Serial.printf("Reed-switch: %s\r\n", (level)?"NOMAGNET (=HIGH)":"MAGNET (=LOW)");

  //check if a reset/power-on occured
  if (esp_reset_reason() == ESP_RST_POWERON) {
      Serial.printf("ESP was just switched ON\r\n");
      cache.state = S_STARTUP;
      cache.ActiveTime = 0;
      cache.NumberOfRestarts = 0;
      cache.counter = 0;

      //set RTC to 0
      struct timeval tv;
      tv.tv_sec = 0;
      tv.tv_usec = 0;
      settimeofday(&tv, NULL);

      //default is to have WiFi off
      if (WiFi.getMode() != WIFI_OFF) {
        Serial.printf("WiFi wasn't off!\r\n");
        WiFi.persistent(true);
        WiFi.mode(WIFI_OFF);
      }

      //try to connect to WiFi
      WiFiUP(false);
      //retrieve the previous gasmeter-counter, will return quickly if it is retained
      GetCounter(10*1000);
      WiFi.disconnect(true, true);

      //transition to new state
      if(level == HIGH) {
        cache.state = S_DEBOUNCE_HIGH;
        Serial.printf("RESET: S_STARTUP -> S_DEBOUNCE_HIGH\r\n");
        esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
        esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, LOW);
      } else {
        cache.state = S_DEBOUNCE_LOW;
        Serial.printf("RESET: S_STARTUP -> S_DEBOUNCE_LOW\r\n");
        esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
        esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, HIGH);
      }
  }

  // check if ESP returns from deepsleep
  if (esp_reset_reason() == ESP_RST_DEEPSLEEP) {
    switch(esp_sleep_get_wakeup_cause()) {
      case ESP_SLEEP_WAKEUP_TIMER:
        Serial.printf("ESP woke up due to timer\r\n");

        //This state should not occur, no debounce state
        if(level == HIGH && cache.state == S_LOW) {
          cache.state = S_DEBOUNCE_HIGH;
          Serial.printf("TIMER: S_LOW -> S_DEBOUNCE_HIGH\r\n");
          esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, LOW);
          break;
        }

        //can occur if LONG_TIME passed and level remains low
        if(level == LOW && cache.state == S_LOW) {
          cache.state = S_LOW;
          Serial.printf("TIMER: S_LOW -> S_LOW\r\n");
          esp_sleep_enable_timer_wakeup(LONG_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, HIGH);

          WiFiUP(true);
          SendMessage(M_STATUS);
          WiFi.disconnect(true, true);
          break;
        }

        //can occur if LONG_TIME passed and level remains high
        if(level == HIGH && cache.state == S_HIGH) {
          cache.state = S_HIGH;
          Serial.printf("TIMER: S_HIGH -> S_HIGH\r\n");
          esp_sleep_enable_timer_wakeup(LONG_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, LOW);

          WiFiUP(true);
          SendMessage(M_STATUS);
          WiFi.disconnect(true, true);
          break;
        }

        //this state should not occur, no debounce state
        if(level == LOW && cache.state == S_HIGH) { 
          cache.state = S_DEBOUNCE_LOW;
          Serial.printf("TIMER: S_HIGH -> S_DEBOUNCE_LOW\r\n");
          esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, HIGH);
          break;
        }

        //level was HIGH for the whole DEBOUNCE_TIME and still is
        if(level == HIGH && cache.state == S_DEBOUNCE_HIGH) { 
          cache.state = S_HIGH;
          Serial.printf("TIMER: S_DEBOUNCE_HIGH -> S_HIGH\r\n");
          esp_sleep_enable_timer_wakeup(LONG_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, LOW);
          break;
        }

        //timer is up, but level changed without triggering EXT0, strange but deal with it
        if(level == LOW && cache.state == S_DEBOUNCE_HIGH) { 
          cache.state = S_DEBOUNCE_LOW;
          Serial.printf("TIMER: S_DEBOUNCE_HIGH -> S_DEBOUNCE_LOW\r\n");
          esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, HIGH);
          break;
        }

        //timer is up, but level changed without triggering EXT0, strange but deal with it
        if(level == HIGH && cache.state == S_DEBOUNCE_LOW) { 
          cache.state = S_DEBOUNCE_HIGH;
          Serial.printf("TIMER: S_DEBOUNCE_LOW -> S_DEBOUNCE_HIGH\r\n");
          esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, LOW);
          break;
        }

        //level was LOW for the whole debounce time and still is
        if(level == LOW && cache.state == S_DEBOUNCE_LOW) {
          cache.state = S_LOW;
          Serial.printf("TIMER: S_DEBOUNCE_LOW -> S_LOW\r\n");
          esp_sleep_enable_timer_wakeup(LONG_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, HIGH);

          WiFiUP(true);
          cache.counter++;
          (cache.counter % 10 == 0) ? SendMessage(M_STATUS) : SendMessage(M_COUNTER);
          WiFi.disconnect(true, true);
          break;
        }
        break;

      case ESP_SLEEP_WAKEUP_EXT0:
        Serial.printf("ESP woke up by EXT0\r\n");

        //level just changed from LOW to HIGH, start debounce time
        if(level == HIGH && cache.state == S_LOW) { 
          cache.state = S_DEBOUNCE_HIGH;
          Serial.printf("EXT0: S_LOW -> S_DEBOUNCE_HIGH\r\n");
          esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, LOW);
          break;
        }

        //Level was LOW, still is, no reason to change state
        if(level == LOW && cache.state == S_LOW) { 
          cache.state = S_LOW;
          Serial.printf("EXT0: S_LOW -> S_LOW\r\n");
          esp_sleep_enable_timer_wakeup(LONG_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, HIGH);
          break;
        }

        //level was HIGH, still is, no reason to change state
        if(level == HIGH && cache.state == S_HIGH) { 
          cache.state = S_HIGH;
          Serial.printf("EXT0: S_HIGH -> S_HIGH\r\n");
          esp_sleep_enable_timer_wakeup(LONG_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, LOW);
          break;
        }     

        //level was HIGH, just changed to LOW, start to debounce, trigger EXT0 if bouncing to HIGH
        if(level == LOW && cache.state == S_HIGH) { 
          cache.state = S_DEBOUNCE_LOW;
          Serial.printf("EXT0: S_HIGH -> S_DEBOUNCE_LOW\r\n");
          esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, HIGH);
          break;
        }

        //level was HIGH, still is, however something triggered EXT0, restart DEBOUNCE_TIME
        if(level == HIGH && cache.state == S_DEBOUNCE_HIGH) {
          cache.state = S_DEBOUNCE_HIGH;
          Serial.printf("EXT0: S_DEBOUNCE_HIGH -> S_DEBOUNCE_HIGH, but restart DEBOUNCE_TIME\r\n");
          esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, LOW);
          break;
        }

        //level was HIGH, just changed to LOW, might be just bouncing
        if(level == LOW && cache.state == S_DEBOUNCE_HIGH) {
          cache.state = S_DEBOUNCE_LOW;
          Serial.printf("EXT0: S_DEBOUNCE_HIGH -> S_DEBOUNCE_LOW\r\n");
          esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, HIGH);
          break;
        }

        //level was LOW, now it is HIGH, might just be bouncing
        if(level == HIGH && cache.state == S_DEBOUNCE_LOW) { 
          cache.state = S_DEBOUNCE_HIGH;
          Serial.printf("EXT0: S_DEBOUNCE_LOW -> S_DEBOUNCE_HIGH\r\n");
          esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, LOW);
          break;
        }

        //level was LOW, still is, however something triggered EXT0, restart DEBOUNCE_TIME
        if(level == LOW && cache.state == S_DEBOUNCE_LOW) { 
          cache.state = S_DEBOUNCE_LOW;
          Serial.printf("EXT0: S_DEBOUNCE_LOW -> S_DEBOUNCE_LOW, but restart DEBOUNCE_TIME\r\n");
          esp_sleep_enable_timer_wakeup(DEBOUNCE_TIME);
          esp_sleep_enable_ext0_wakeup(REED_DEEPSLEEP_PIN, HIGH);
          break;
        }
        break;

      default:
        Serial.printf("ESP woke up due to an unknown reason\r\n");
    }
  }

  Serial.printf("counter: %llu\r\n", cache.counter);
  Serial.printf("=== entering deepsleep after %d ms ===\r\n", millis());
  cache.ActiveTime += millis();
  digitalWrite(2, LOW);
  esp_deep_sleep_start();

  Serial.println("This should never get printed");
}

void loop() {
  Serial.println("This should never get printed");
}
VoyteckPL commented 1 year ago

One question. What is the unit of measurement of time on batteries and active time?

Torxgewinde commented 1 year ago

The battery unit is in Volt. Active Time is the time the ESP32 is in active mode in milliseconds. BatteryRuntime is the time since power-up/reset in seconds (as float, "seconds.subseconds" ). BatteryRuntime does not have a very accurate clock source, so it is just a course time.

RTC_NOINIT_ATTR struct {
  uint8_t bssid[6];
  uint8_t channel;
  float BatteryVoltage;      //battery voltage in V
  uint64_t NumberOfRestarts; //number of restarts
  uint64_t ActiveTime;       //time being active in ms
  enum _state state;         //keep track of current state
  uint64_t counter;          //gasmeter-counter
} cache;
VoyteckPL commented 1 year ago

Screenshot_2023-01-18-06-14-44-405-edit_io homeassistant companion android Screenshot_2023-01-18-06-14-58-834-edit_io homeassistant companion android Here are some stats for period from 09.01-18.01. I didn't cut the low power connection yet.

Torxgewinde commented 1 year ago

Ok, about the Low-Power-Pad, there is a lot of power that is drawn by the RGB-LED even if it is not emitting light. For my particular reed-switch I adjusted the bounce-time down to 150 ms, but if 1000 ms works for your setup I would just keep it that way.

@imabot2 has an informative and detailed article in his blog about the low-power-pad: https://lucidar.me/en/esp32/power-consumption-of-esp32-firebeetle-dfr0654/