NodeMCU Freezer Monitor Webserver Hanging

I built a monitor for an old freezer in the garage out of an old NodeMCU I had and a DS18B20 temp sensor. It worked nice but I wanted to be able to request the temperature so I added a super simple web server to it. It just responds with a text string when the root page is requested.

It works, but only for a little while. After a little while the webserver and OTA both stop responding. The temperature probe still works and the heartbeat still blinks so the board isn't locking up. It also still sends an email if I warm up the probe, so it does have the network.

But going to the IP address with a browser or attempting an OTA upload fails for no response. If I reset the board then everything will work as normal for a little while.

Can anyone see where I messed up? I'm not terribly experienced using the network from these parts so I might have done something dumb.

/*

FreezerMonitor.ino

     Copyright (C) 2023  David C.

     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, either version 3 of the License, or
     (at your option) any later version.

     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, see <http://www.gnu.org/licenses/>.

     */
#include "Arduino.h"
#include <ESP8266WiFi.h>
#include <ArduinoOTA.h>

#include <ESP8266WebServer.h>
#include <ESP_Mail_Client.h>
#include "SecureDefine.h"

#include <OneWire.h>
#include <DallasTemperature.h>

/*****************************
******** WiFi Vars ***********
*****************************/

const char* ssid = WIFI_SSID;
const char* password = WIFI_PASSWORD;

const uint8_t HEART_PIN = D4;
uint32_t heartDelay = 250;

const uint8_t ONE_WIRE_BUS = D5;
uint32_t sensorReadInterval = 30000;

IPAddress ipa(192, 168, 1, 72);
IPAddress gate(192, 168, 1, 1);
IPAddress sub(255, 255, 255, 0);
IPAddress dns1(8, 8, 8, 8);
IPAddress dns2(8, 8, 4, 4);

/*****************************
******* Sensor Vars **********
*****************************/

OneWire oneWire(ONE_WIRE_BUS);

DallasTemperature sensor(&oneWire);

uint8_t sensorAddress[8] = { 0x28, 0xFF, 0xCE, 0x3A, 0x7E, 0x60, 0x1F, 0x06 };

float tempC;
float tempLimit = 0.0;
bool excursion = false;

/*****************************
******** Email Vars **********
*****************************/
SMTPSession smtp;

// Minimum time between mails.
unsigned long mailDelay = 3600000;
unsigned long lastMailTime = -mailDelay;

/* Callback function to get the Email sending status */
void smtpCallback(SMTP_Status status);

/* Declare the Session_Config for user defined session credentials */
Session_Config config;

/*****************************
******* Server Vars **********
*****************************/

ESP8266WebServer server(80);

void handleRoot();
void handleNotFound();


/*****************************
******** Functions ***********
*****************************/


void setup() {

  pinMode(HEART_PIN, OUTPUT);

  Serial.begin(115200);
  Serial.println("Booting");
  WiFi.mode(WIFI_STA);
  WiFi.config(ipa, gate, sub, dns1, dns2);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);

  setupOTA();

  sensor.begin();

  setupMail();

  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot);
  server.onNotFound(handleNotFound);

  server.begin();
  Serial.println("Server Started");
}

void loop() {
  heartbeat();
  handleSensor();
  server.handleClient();

  while(WiFi.status() != WL_CONNECTED){
    heartDelay = 100;
    heartbeat();
  }

  if (tempC >= tempLimit) {
    if (!excursion) {
      // If this is a new thing
      Serial.println("Excursion");
      excursion = true;
      heartDelay = 500;
      if (millis() - lastMailTime >= mailDelay) {
        Serial.println("Sending Mail");
        sendMessage();
        lastMailTime = millis();
      }
    }
  } else {
    heartDelay = 2000;
    excursion = false;
  }

  ArduinoOTA.handle();
}

/********************
  Webserver Callbacks
********************/

void handleRoot() {
  char response[40];
  char tempbuf[10];
  dtostrf(tempC, 3, 1, tempbuf);
  sprintf(response, "Freezer Temp is %s C", tempbuf);
  server.send(200, "text/plain", response);
}

void handleNotFound() {
  server.send(404, "text/plain", "404: Page Not Found!");
}


/*
Gets temperture from sensor to global tempC variable
*/
void handleSensor() {
  //  Set previous millis to past so sensor will read immediately at startup
  //  to prevent a spurrious report at startup.
  static uint32_t pm = 0 - sensorReadInterval;
  uint32_t cm = millis();
  if (cm - pm >= sensorReadInterval) {
    sensor.requestTemperatures();
    Serial.print("Temp Is : ");
    tempC = sensor.getTempC(sensorAddress);
    Serial.println(tempC);
    pm = cm;
  }
}

/*
Blinks onboard LED to show status
0.25Hz = Normal
1Hz = Alarm
5Hz = Lost Connection
*/
void heartbeat() {
  static uint32_t pm = millis();
  uint32_t cm = millis();
  static boolean state = true;
  if (cm - pm >= heartDelay) {
    pm = cm;
    state = !state;
    digitalWrite(HEART_PIN, state ? HIGH : LOW);
  }
}

/*
Sends email message on freezer alarm
*/
void sendMessage() {
  /* Declare the message class */
  SMTP_Message message;

  /* Set the message headers */
  message.sender.name = F("ESP Mail");
  message.sender.email = AUTHOR_EMAIL;

  message.subject = F("Freezer Alarm!");
  message.addRecipient(F("Someone"), RECIPIENT_EMAIL);
  message.addRecipient("S", EMAIL_S);
  message.addRecipient("D", EMAIL_D);

  char textMsg[40];
  char tempbuf[10];
  dtostrf(tempC, 3, 1, tempbuf);
  sprintf(textMsg, "Freezer is over temperature %s C", tempbuf);
  message.text.content = textMsg;
  message.text.charSet = F("us-ascii");
  message.text.transfer_encoding = Content_Transfer_Encoding::enc_7bit;
  message.priority = esp_mail_smtp_priority::esp_mail_smtp_priority_low;

  message.addHeader(F("Message-ID: <abcde.fghij@gmail.com>"));

  /* Connect to the server */
  if (!smtp.connect(&config)) {
    MailClient.printf("Connection error, Status Code: %d, Error Code: %d, Reason: %s\n", smtp.statusCode(), smtp.errorCode(), smtp.errorReason().c_str());
    return;
  }

  if (!smtp.isLoggedIn()) {
    Serial.println("Not yet logged in.");
  } else {
    if (smtp.isAuthenticated())
      Serial.println("Successfully logged in.");
    else
      Serial.println("Connected with no Auth.");
  }

  /* Start sending Email and close the session */
  if (!MailClient.sendMail(&smtp, &message))
    MailClient.printf("Error, Status Code: %d, Error Code: %d, Reason: %s\n", smtp.statusCode(), smtp.errorCode(), smtp.errorReason().c_str());
}

/*  
Call from setup to start email 
*/
void setupMail() {
  /*  Set the network reconnection option */
  MailClient.networkReconnect(true);

  /** Enable the debug via Serial port
   * 0 for no debugging
   * 1 for basic level debugging
   *
   * Debug port can be changed via ESP_MAIL_DEFAULT_DEBUG_PORT in ESP_Mail_FS.h
   */
  smtp.debug(0);

  /* Set the callback function to get the sending results */
  smtp.callback(smtpCallback);

  /* Set the session config */
  config.server.host_name = SMTP_HOST;
  config.server.port = SMTP_PORT;
  config.login.email = AUTHOR_EMAIL;
  config.login.password = AUTHOR_PASSWORD;
  config.login.user_domain = F("127.0.0.1");
  config.time.ntp_server = F("time.nist.gov");
  config.time.gmt_offset = 3;
  config.time.day_light_offset = 0;
}

/* 
Callback function to get the Email sending status 
*/
void smtpCallback(SMTP_Status status) {
  /* Print the current status */
  Serial.println(status.info());

  /* Print the sending result */
  if (status.success()) {

    Serial.println("----------------");
    MailClient.printf("Message sent success: %d\n", status.completedCount());
    MailClient.printf("Message sent failed: %d\n", status.failedCount());
    Serial.println("----------------\n");

    for (size_t i = 0; i < smtp.sendingResult.size(); i++) {
      /* Get the result item */
      SMTP_Result result = smtp.sendingResult.getItem(i);

      MailClient.printf("Message No: %d\n", i + 1);
      MailClient.printf("Status: %s\n", result.completed ? "success" : "failed");
      MailClient.printf("Date/Time: %s\n", MailClient.Time.getDateTimeString(result.timestamp, "%B %d, %Y %H:%M:%S").c_str());
      MailClient.printf("Recipient: %s\n", result.recipients.c_str());
      MailClient.printf("Subject: %s\n", result.subject.c_str());
    }
    Serial.println("----------------\n");

    // You need to clear sending result as the memory usage will grow up.
    smtp.sendingResult.clear();
  }
}

/*
OTA setup
*/
void setupOTA() {
  ArduinoOTA.setPort(8266);
  ArduinoOTA.onStart([]() {
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH) {
      type = "sketch";
    } else {  // U_FS
      type = "filesystem";
    }

    // NOTE: if updating FS this would be the place to unmount FS using FS.end()
    Serial.println("Start updating " + type);
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) {
      Serial.println("Auth Failed");
    } else if (error == OTA_BEGIN_ERROR) {
      Serial.println("Begin Failed");
    } else if (error == OTA_CONNECT_ERROR) {
      Serial.println("Connect Failed");
    } else if (error == OTA_RECEIVE_ERROR) {
      Serial.println("Receive Failed");
    } else if (error == OTA_END_ERROR) {
      Serial.println("End Failed");
    }
  });
  ArduinoOTA.begin();
}

A shot in the dark... Maybe ESP8266WebServer does not gracefully handle dropped Wifi. Try setting a bool if WiFi.status() != WL_CONNECTED then blinking differently when the bool is true. If there appears to be a correlation between the new blink and the web server not responding then the theory has some merit.

You may be able to perform the test on a bench. You would just need something WiFi blocking. Aluminum foil? Connected to ground?

That was my first thought exactly. So I put a catch in loop that should freeze things if it drops.

Maybe I should add a flag there so I'll know if it dropped and came back on or something.

Done. Will return shortly with results.

That's not it. I added a boolean hasDropped and used in loop:

void loop() {
  heartbeat();
  handleSensor();
  server.handleClient();

  while(WiFi.status() != WL_CONNECTED){
    hasDropped = true;
    heartDelay = 100;
    heartbeat();
  }

  if (tempC >= tempLimit) {
    if (!excursion) {
      // If this is a new thing
      Serial.println("Excursion");
      excursion = true;
      heartDelay = 500;
      if (millis() - lastMailTime >= mailDelay) {
        Serial.println("Sending Mail");
        sendMessage();
        lastMailTime = millis();
      }
    }
  } else {
    heartDelay = hasDropped? 200 : 2000;
    excursion = false;
  }

  ArduinoOTA.handle();
}

It should blink fast if it's dropped a connection at any point. But it's in the failure mode and blinking at 2000ms intervals still. So that never got tripped.

I know the DS18B20 can block for a bit while it requests temperatures. I saw that when I was testing just the sensor and a blinking led with nothing else. Could that be a problem for the web server? It's less than a second, but I know ESP8266 doesn't like blocking code.

I like that theory.

Make sensorReadInterval ridiculously large then test.

I'm interested to know what you discover.

I've had several projects with ESP8266s + DS18B20 temp sensors, including one that ran 24/7 for a year with no problems, and a new one that has been going for a week or so now.

My code is similar to yours, except I usually query the DS18B20 at a slower pace (every 5 or 10 minutes), but IIRC I've done much faster in testing. I also post to Thingspeak right after getting a temperature, which adds another second or so to the loop at that point.

So I have a hard time believing that pinging the DS18B20 every 30 seconds would be a problem for OTA or the server, but maybe 'tis.

Also, my wifi setup is simpler (no .config), I don't have the email code, and my OTA setup is merely ArduinoOTA.begin().

If you strip out all DS18B20 and email code, leaving just...

void loop() {  
  ArduinoOTA.handle();
  server.handleClient();
}

...do you get the same problem on the bench?

I think that I see what you are trying to do but are you sure this could not go wrong at some stage ?: if (millis() - lastMailTime >= mailDelay) . . .

OK. I now see you are quite fluently using the trick of rolling back an unsigned integer to control the timing of the first invocation of a block of code. It is not a programming idiom I've used so I was not familiar with it but, providing there is no mixing of data types, there can be nothing wrong with it so my last comment does not apply.

Here, I guess you need a yield() statement because otherwise it appears to be a blocking loop if there is connection failure.

 while(WiFi.status() != WL_CONNECTED){
    hasDropped = true;
    heartDelay = 100;
    heartbeat();
  }

. . . 
. . .

void heartbeat() {
  static uint32_t pm = millis();
  uint32_t cm = millis();
  static boolean state = true;
  if (cm - pm >= heartDelay) {
    pm = cm;
    state = !state;
    digitalWrite(HEART_PIN, state ? HIGH : LOW);
  }
}

That is the intention. That code is there to try to figure out if I am temporarily losing connection. If so it should hang there and I will know. That will change to an attempt to reconnect or a reset once I figure out this bug.

Yup. It's just a so I'm not prevented from getting an alarm in the first hour. It only works if everything is the same type.

It also creates a magic hour every 49 days where the alarm will not send. I'll fix that later. I want to add a bit to send an email at least once a week to let me know it's still alive. I can add a line there to bring that time up to the recent past.

Was a little while before I had a chance to really test this. But it has no effect. It reads the sensor once at the beginning of the code and dutifully reports that temperature for the first few minutes. It works as many times as I want to open the browser and type in 192.168.1.72. But after some time when I put that address the browser just hangs and it doesn't work again until I reset the board. Neither the server nor OTA work.

But the email still sends just fine if the temp gets up. I think I've ruled out a loss of connection.

I believe that places the problem squarely on the ESP library's shoulders.

Are you using the most up-to-date version of that library?

Does connecting from a fresh run of the browser make a difference?

Does connecting from a different computer make a difference?

AFAIK

No difference.

It also doesn't react when I try to ping it. It doesn't time out. Just hangs.

If I ping an address that doesn't exist I get back pretty quick a message that the destination host is unreachable. I don't get that in this case. Or a time-out. It just hangs forever.

But if I warm that probe up it sends the email no problem.

I was really hoping that someone who knew these types of programs would come tell me I have two things that don't go together and I have to use a different library or something.

On an unrelated topic, you're probably better served by using NTP Pool...
https://www.ntppool.org/en/

There's like three orders of magnitude more servers available from the pool then from NIST.

Well, until someone with more experience with NodeMCU chimes in... Try to divide-and-conquer. Comment out the email stuff. I think you can just comment out setupMail and sendMessage. Does the web server still have problems?

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.