Friday, November 19, 2021

Make an ESP8266 WiFi Temperature Sensor & Python Flask Backend - Part 3

In the previous article, I created a rudimentary Flask backend to receive example data from our ESP8266. Here's a further decomposed system diagram to give you a better idea of where we're going. The orange stars indicate our focus for this article.

In a moment, I'll incorporate the HttpPostClient code into the temperature sensor example, send temperature data instead of example data, and refactor the backend API to receive temperature data. The GET API, index.html, and plot.js for plotting the data will be the focus in a future blog post.

So without further ado, I'll combine our example sketches into one...

Combining Our Examples

To keep the sketch from becoming a mess of code, it's time to make what we've done so far more modular with good decoupling and cohesion.

We want our modules to correspond to a single logical entity or function. We want to minimize dependencies between modules.

Coupling, Cohesion, Modules

Let's list the functionality of our application:

  • Setup everything
  • Read the temperature sensor
  • Connect to wifi
  • Post data to an API 
  • Repeat the above actions (except Setup)

Starting with the DS18B20 example, I'm going create a new tab for all the DS18B20 stuff, and simplify the main tab and rename it to WiFiTempSensor and save the new sketch with that name.

I'll put in some function calls representing the functionality above. Here's an initial stab at the main tab:

void setup(void) {
  // start serial port
  Serial.begin(9600);
  Serial.println("WiFi Temp Sensor");
  setupDS18B20();
  setupHttpClient();
}

void loop(void) {
  float tempC = readDS18B20C();
  // convert tempC to JSON
  postJsonData(json);
}

That code is a lot cleaner than what we had before. Coupling between setup() and the various setup functions is very low because each of the functions "hides" the details of implementation. Same deal with loop().

Next, I copied over all the old temperature example code into a tab "DS18B20" and all the previous HttpClientPost code into a tab "HttpClient". 

HTTP Client Code

In HttpClient, I refactored SERVER_IP to SERVER_URL and specified the URL, changing the API endpoint from /postplain to /temp.

Here's what I came up with:

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>

#define SERVER_URL "http://192.168.1.15:5000/temp"

// Assumes Serial has been set up
void setupHttpClient()
{
  WiFi.beginSmartConfig();

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected! IP address: ");
  Serial.println(WiFi.localIP());
}
 
void postJsonData(char *data)
{
  if ((WiFi.status() == WL_CONNECTED)) {

    WiFiClient client;
    HTTPClient http;

    Serial.print("[HTTP] begin...\n");
    http.begin(client, SERVER_URL); //HTTP
    http.addHeader("Content-Type", "application/json");

    Serial.print("[HTTP] POST...\n");

    int httpCode = http.POST(data);

    if (httpCode > 0) {
      
      Serial.printf("[HTTP] POST... code: %d\n", httpCode);

      if (httpCode == HTTP_CODE_OK) {
        const String& payload = http.getString();
        Serial.println("received payload:\n<<");
        Serial.println(payload);
        Serial.println(">>");
      }

    } else {
      Serial.printf("[HTTP] POST... failed, error: %s\n", http.errorToString(httpCode).c_str());
    }

    http.end();
  }
}

Notice that the two functions here deal with posting data via HTTP. They're logically related. Hence, this module has decent cohesion. Also, notice that I changed the API endpoint from /postplain to /temp. We'll have to change the backend in the near future.

DS18B20 Code

I did the same sort of thing with the DS18B20 code.

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

#define ONE_WIRE_BUS 2

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

/*
 * The setup function. We only start the sensors here
 */
void setupDS18B20(void)
{
  sensors.begin();
}

/*
 * Main function, get and show the temperature
 */
float readCelciusDS18B20()
{
  Serial.print("Requesting temperatures...");
  sensors.requestTemperatures(); // Send the command to get temperatures
  Serial.println("DONE");
  float tempC = sensors.getTempCByIndex(0);

  if(tempC != DEVICE_DISCONNECTED_C) {
    Serial.print("Temperature for the device 1 (index 0) is: ");
    Serial.println(tempC);
  } else {
    Serial.println("Error: Could not read temperature data");
  }
  return tempC;
}

Main Tab

Then I went back to the main tab and made some minor modifications to convert from the floating point temperature to json format.

void setup(void) {
  // start serial port
  Serial.begin(9600);
  Serial.println("WiFi Temp Sensor");
  setupDS18B20();
  setupHttpClient();
}

char json[256];

void loop(void) {
  float tempC = readCelciusDS18B20();
  tempCelciusToJson(json, tempC);
  postJsonData(json);
  delay(10000);
}

void tempCelciusToJson(char *json, float t) {
  if (json)
    sprintf(json, "{ \"tempC\": \"%4.2f\" }", t);
}

The reason I didn't embed the conversion of temperature data into postJsonData() has to do with coupling. I wanted to decouple that routine from any knowledge of temperature or the JSON format I am chosing, making it more general purpose. If I can use it without any modifications to send any other kind of sensor data or using any kind of JSON format.

Now all that's left is to refactor the backend to accept the temperature data.

Refactoring the API Backend

Last time we created a route handler for the example code. We'll just refactor it by changing the API endpoint to /temp and then adding some code to receive the JSON data and print it out. I ran into some issues and so I went ahead and added  try-except blocks and printed out request headers for my own debugging.

@app.route("/temp", methods=["POST"])
def temp_post():
print(request.headers)
try:
data = request.json
tc = data['tempC']
tf = float(tc) * 9.0 / 5.0 + 32.0
print(tc, tf)
return "{'status': 'success'}"
except Exception as e:
print("Exception: {}".format(e))
return "{'status': 'exception'}"

After issuing the python ./app.py command, I get the following:

192.168.1.76 - - [13/Nov/2021 12:38:59] "POST /temp HTTP/1.1" 200 -
Host: 192.168.1.15:5000
User-Agent: ESP8266HTTPClient
Accept-Encoding: identity;q=1,chunked;q=0.1,*;q=0
Connection: keep-alive
Content-Type: application/json
Content-Length: 20


21.56 70.80799999999999

So that's successful! Over in Serial Monitor, I also see signs of success:

Requesting temperatures...DONE
Temperature for the device 1 (index 0) is: 21.56
[HTTP] begin...
[HTTP] POST...
[HTTP] POST... code: 200
received payload:
<<
{'status': 'success'}
>> 

As promised, I combined the two example/prototype sketches into a single sketch that implements the most important functionality: measuring temperature and posting it to a URL. I refactored the backend to accept the data and print it.

Revisiting the requirements, we're making good progress:

  • Periodically measure the temperature
  • Sensor probe must be waterproof
  • Sensor operates wirelessly (almost; we'll figure out power later)
  • Store the data centrally
  • Plot the data
  • At least +/- 1 Degree F accuracy
  • Alert the user if temperature is above the threshold 
Here is all the code so far

Next time, I'll implement basic chart plotting and maybe add a few improvements.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.