Skip to content
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

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

Closed
VoyteckPL opened this issue Dec 19, 2022 · 70 comments
Closed
Assignees
Labels
enhancement New feature or request

Comments

@VoyteckPL
Copy link

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

@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
Copy link
Author

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
Copy link
Owner

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

@VoyteckPL
Copy link
Author

Nice! My firebeetle should arrive tomorrow 🙂

@Torxgewinde Torxgewinde changed the title Connection of pir sensor REED switch input, reading out gas-meter (was: Connection of pir sensor) Dec 27, 2022
@VoyteckPL
Copy link
Author

VoyteckPL commented Dec 28, 2022

image

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

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

#include "esp_adc_cal.h"

I have FireBeetle 2 board.

@Torxgewinde
Copy link
Owner

Torxgewinde commented Dec 30, 2022

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
Copy link
Author

Ok I will check and report back

@Torxgewinde
Copy link
Owner

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
Copy link
Author

Fantastic. Thanks. I will do some testing today.

@VoyteckPL
Copy link
Author

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
Copy link
Author

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
Copy link
Owner

Torxgewinde commented Dec 30, 2022

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

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

@Torxgewinde
Copy link
Owner

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
Copy link
Author

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

@VoyteckPL
Copy link
Author

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

@Torxgewinde
Copy link
Owner

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
Copy link
Author

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
Copy link
Author

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
Copy link
Owner

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:

  • Start ESP32 (first time or after reset pressed) --> obtain the counter from MQTT-Broker.
  • Either the value of "counter" is still valid from last time, or the user set it manually by mosquitto_pub -t "Keller/Gaszaehler/counter" -h server.lan -u "username" -P "password" -m "12345" -r. Regardless who put the last value, the value as in the MQTT-broker will be incremented. from then on. The ESP32 will only check for this value after a reset or when power-cycling it.

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
Copy link
Author

VoyteckPL commented Jan 7, 2023

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
Copy link
Author

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
Copy link
Owner

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

  • Always send the counter and the converted value in m³ with a precision of two digits
  • If gasmeter does not trigger ESP32 for a LONG_TIME (=1h), send full status
  • If counter ends on 0, send a full status

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
Copy link
Author

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
Copy link
Owner

Yes, to start from a certain value:

  • Power down the ESP32
  • Send a retained message with the meter-value in counts. If the meter reads for example 123.45 m³ send: mosquitto_pub -t "Keller/Gaszaehler/counter" -h server.lan -u "username" -P "password" -m "12345" -r
  • Power on the ESP32, it will now learn the value from the MQTT-Broker. It does only look for the broker-value after power-on or when reset is pressed.

@Torxgewinde
Copy link
Owner

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
Copy link
Author

Mine is in sync so far

@Torxgewinde
Copy link
Owner

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
Copy link
Author

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

@Torxgewinde
Copy link
Owner

Torxgewinde commented Jan 10, 2023

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
Copy link
Author

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
Copy link
Owner

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/

@VoyteckPL
Copy link
Author

So it takes ~500 microampers without cut and ~11 microampers with cut?

@Torxgewinde
Copy link
Owner

Yepp, it should be in the ballpark.
The pull-up resistor used for the reed-switch will increase the sleep-current to a bit above 11 µA, but I do not have a device for measuring such low currents at home to accurately give a figure.

BTW: "Cznas na bateriach" is in seconds, in your screenshot 830440 should translate to 9 days, 14 hours, 40 minutes and 40 seconds. That is the approximate time since last reset or power-up (the clock has a huge drift because its time-base is an oscillator instead of a quartz-cristal, so do not trust this value fully, but coarsely it should give the time since start). The other figures and units seem correct.

@VoyteckPL
Copy link
Author

Ok so after 2 weeks:
IMG_20230121_083257
IMG_20230121_083515

Unfortunately there is difference. I still have 1000 ms debounce setting.

@Torxgewinde
Copy link
Owner

Torxgewinde commented Jan 21, 2023

Well, at least it is quite close. The current sketch is improved and could address your error-margin:

  • If WiFi and/or MQTT connection takes very long, it will just give up sooner. This helps to not miss a revolution of the gasmeter while it was trying to submit a new value. I observed this error with my gasmeter and the default timings are for a high gas-consumption-rate (like when showering). I prefer to miss a transmission, instead of having a missed count. With the next transmission it will just use the correct value then, so it might be increased by more than one step-count.
  • The counter start value can be placed as a retained value into the subtopic MQTTRootTopic+"/config/counter", it will be picked up by pressing reset or by powering up the Firebeetle.
  • The debounce time can be placed into as a reatined value into the subtopic MQTTRootTopic+"/config/debounceTime", it will be picked up by pressing reset or by powering up the Firebeetle.
  • I decided against reading the counter from the MQTT server to save battery and to avoid picking up an outdated value. In the discussion above I gave a hint where this can be added to the sketch if this is needed for your use case, but I'd like to not have it that way for my sketch.
  • Mechanically I had to readjust/move my reed-switch a couple of times as it was somewhat insensitive. It took me a couple of tries to really mount it at a good spot.

TL;DR: Please try the new sketch, WiFi timeouts and a too large debounce time might be responsible for the error you observed.

@VoyteckPL
Copy link
Author

Will do! Thanks for explaining.

@Torxgewinde
Copy link
Owner

Torxgewinde commented Jan 21, 2023

I just noticed, your software-counter is higher than the gasmeter-counter. That is something I did not see with my setup and might be due to the reed-switch and perhaps bouncing. This might also hint why your setup required such a high debounce time.

You can make sure the reed-switch is working well with the very minimal sketch:

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

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

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

That sketch just lights up the LED, when the reed switch is pulled to ground. It helps to check the reed-switch.

@VoyteckPL
Copy link
Author

The reed switch is definately fine. I tested it on Tasmota for over 2 months and it was 1:1 all the time. Never lost a single impulse😬

@Torxgewinde
Copy link
Owner

Please try the most current version of the sketch and adjust the debounceTime. 1000ms for debounce is surprisingly large, but perhaps you need to even increase it further in your setup (strange indeed, but who knows).

Here, with my setup, it counts correctly since last week (about ~8 to 12 m³ per day) without error. The only thing I observed, that indeed it sometimes cannot transmit immediately and aborts the connection due to timeout, but still counts correctly. As soon as the transmissions go through again to the MQTT-broker, the counter is correct. So, I think the sketch is not too far of.

The energy consumption is OK for transmitting that often, but in my setup I will decrease the number of transmissions at some stage to make the battery last longer. I am now at about ~60% capacity since start of development with a 2000mAh LiPo battery.

@VoyteckPL
Copy link
Author

VoyteckPL commented Jan 30, 2023

This doesn't seem to work anymore. I tried multiple times :(

Yes, to start from a certain value:

  • Power down the ESP32
  • Send a retained message with the meter-value in counts. If the meter reads for example 123.45 m³ send: mosquitto_pub -t "Keller/Gaszaehler/counter" -h server.lan -u "username" -P "password" -m "12345" -r
  • Power on the ESP32, it will now learn the value from the MQTT-Broker. It does only look for the broker-value after power-on or when reset is pressed.

What I would also suggest (if possible) to abb configurable debounce time via MQTT so it is possible to change without updating firmware :)

@VoyteckPL
Copy link
Author

image

So here is V1 case ;)

@VoyteckPL

This comment was marked as off-topic.

@Torxgewinde
Copy link
Owner

Hi,
The most recent sketch has a slightly changed topic for the initial counter, that is learned when resetting or powering on the ESP32:

  • The counter start value can be placed as a retained value into the subtopic MQTTRootTopic+"/config/counter", it will be picked up by pressing reset or by powering up the Firebeetle.
  • The debounce time can be placed into as a reatined value into the subtopic MQTTRootTopic+"/config/debounceTime", it will be picked up by pressing reset or by powering up the Firebeetle.

This means:

  • Power down the ESP32
  • Send a retained message with the meter-value in counts. If the meter reads for example 123.45 m³ send: mosquitto_pub -t "Keller/Gaszaehler/config/counter" -h server.lan -u "username" -P "password" -m "12345" -r and for a debounce time of 1111 ms mosquitto_pub -t "Keller/Gaszaehler/config/debounceTime" -h server.lan -u "username" -P "password" -m "1111" -r
  • Power on the ESP32, it will now read the value sfrom the MQTT-Broker. It does only look for the broker-value after power-on or when reset is pressed.

The hardware in the case looks good, well done.

About not being able to flash: The sketch itself has nothing that can "brick" an ESP32. The flashing procedure is part of the hardware, the ESP32 and bootloader. Please try to load a minimal sketch, the Arduino IDE has a few examples that just blink a LED or print something to the serial port. Those should always work, or otherwise your hardware, cabling or software has an issue. When handling electronic devices, short circuits, broken connections and even ESD (=electrostatic static discharge) can damage the electronics. I just hope this did not happen to your board. Loading a very basic sketch is useful to narrow down the issue. HTH.

@Torxgewinde Torxgewinde self-assigned this Feb 3, 2023
@Torxgewinde Torxgewinde added the enhancement New feature or request label Feb 3, 2023
@VoyteckPL

This comment was marked as off-topic.

@Torxgewinde

This comment was marked as off-topic.

@VoyteckPL
Copy link
Author

Ok. So far few days testing counter is 1:1. I will mount it outside soon. Great job

@VoyteckPL
Copy link
Author

Last 3 weeks tests: device didn't loose a single pulse. It is 1:1 all the time.

@Torxgewinde
Copy link
Owner

Very nice, same for my setup.

I will close the issue for now as it seems we have reached an acceptable revision. There is potential to save more power, but I think it is quite OK for now.

@VoyteckPL
Copy link
Author

Thanks. Great job. I will test in longer period and see battery life.

@Torxgewinde
Copy link
Owner

Sure, just add it to this issue.

Actually, I am quite happy to hear it is also working for you. I myself am now also happy, because finally I can monitor my gasmeter and without the "nudge" to start coding, I would probably still procrastinate it. I guess we all have busy lives...

Pozdrawiam serdecznie!

@VoyteckPL
Copy link
Author

From 01.03 till today 24.09 I went from 4.2 to 3.8v which is a great success. Charging batteries now for the winter. We will see how it goes. Never lost a single unit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants