Avatar

Making a weather indicator with Arduino

← Back to list
Posted on 03.04.2023
Image by AI on Midjourney
Refill!

Previously I have made a firmware for controlling a LED driver. This time I'll wire it up with a weather API, so it could actually bring some meaningful value.

I know, making a weather indicator is like a "Hello world" in a world of electronics. But you got to start somewhere, right? Also, I always wanted to do such device as a kid. But back in the days since there were no API available, I would have to literary drill the window frame through to place a sensor outside. My mom would not approve that.

Anyway, these days we have a lot of amazing services to get the temperature readings from. I am gonna go with weatherapi.com, as they allow a generous free tier of 1 million requests per a month.

The act() functions

Most of the time when I was reading Arduino tutorials and articles, I could not help noticing the fact the code in there was blocking the loop() cycle. What I wanted to achieve was some kind of non-blocking logic in every component of the firmware. That is why, there is an act() method everywhere. The CPU enters them one by one, and the methods check if the time has come to process something. If not, they just move on until the next iteration. By doing so I could achieve some sort of multitasking, allowing, for instance, run animation on the LED driver and at the same time wait for incoming network connections.

File structure

In many tutorials that I could find out there, engineers had all the firmware code in one single ino file. However, I believe a better way would be having different parts of the firmware to be kept in distinct files.

Flags & secrets

The file with flags contain two settings:

👉 📃  flags.h
#pragma once
#define DEV_MODE 0
#define USE_SERIAL 0
The code is licensed under the MIT license

The ATmega328 chip Arduino is equipped with does not have any hardware protection against problems with the firmware. That is, the board simply gets frozen and goes unresponsive in case if it encounters a memory overflow, runtime error or some illegal instruction.

This is the reason why I personally think ATmega-based boards, such as Arduino or similar, are not suitable for building any kind of critical systems, such as life support systems, parts of a self-driving vehicles, etc. I would also think twice before equipping a flying drone with Arduino-based brains.

Should an error happen immediately after the board starts, it also becomes difficult to catch the moment to re-wire Arduino in order to mitigate the problem. That is why, when the DEV_MODE is on, it adds a timeout of 2 seconds before the code starts, so I have time to intervene.

I must be careful writing C++ code. If I am not sure about what I am doing, I can verify a minimal code snippet with an online playground.

The USE_SERIAL flag tells the board to initialise a serial connection with an IDE for debugging. If the IDE is not-plugged, the board can't proceed with the rest of the firmware. That is why I can't use the device when it is detached from the computer. Dropping USE_SERIAL to 0 tells the board to not use the serial connection and be on its own.

Debug output

I made a wrapper around Serial in order to be able to disable the debug output by dropping the flag. That wasn't probably the smartest choice from the perspective of the resource preservation, but it worked fine for now.

There is a polymorphic implementation of a debug() function. Maybe there was a better way to do it, but again, worked fine for this case.

👉 📃  debug.h
#pragma once
#include <Arduino.h>
#include <cstdint>
#include <memory>
void debugInit();
void debug(const arduino::String data);
void debug(const char *data);
void debug(const uint8_t data);
void debug(const int16_t data);
The code is licensed under the MIT license

And the implementation:

👉 📃  debug.cpp
#include <Arduino.h>
#include <cstdint>
#include "flags.h"
#include "debug.h"
void debugInit() {
#if USE_SERIAL==1
Serial.begin(9600);
while (!Serial);
#endif
}
void debug(const arduino::String data) {
#if USE_SERIAL==1
Serial.println(data);
#endif
}
void debug(const char *data) {
#if USE_SERIAL==1
Serial.println(data);
#endif
}
void debug(const uint8_t data) {
#if USE_SERIAL==1
Serial.println(data);
#endif
}
void debug(const int16_t data) {
#if USE_SERIAL==1
Serial.println(data);
#endif
}
void debug(const unsigned long data) {
#if USE_SERIAL==1
Serial.println(data);
#endif
}
The code is licensed under the MIT license

Secrets

The secrets I don't commit to the repo, that is why, by the definition, they should be placed separately. I believe the names of the variable are quite self-explanatory.

👉 📃  secrets.h
#pragma once
constexpr char WIFI_SECRET_SSID[] = "wifi-name";
constexpr char WIFI_SECRET_PASS[] = "";
constexpr char API_TOKEN[] = "";
constexpr char WEATHER_API_TOKEN[] = "";
constexpr char WEATHER_API_LOCATION[] = ""; # GPS coordinates
constexpr char LOGGLY_TOKEN[] = ""; # I use Loggly to store the logs from the device
The code is licensed under the MIT license

Networking

Okay, here comes the best part. To implement networking, I have the logic split onto three parts: the networking, the client and the server.

#pragma once
#include <Arduino.h>
#include <cstdint>
#include <map>
#define RECONNECTION_RETRY_DELAY 1000
#define CLIENT_CONNECTION_TIMEOUT 2000
using Callback = std::function<void()>;
class NetworkRequest {
public:
arduino::String method;
arduino::String target = "";
arduino::String path = "";
void appendLine(String line);
arduino::String getHeaderValue(String name);
arduino::String getAPIToken();
private:
std::map<arduino::String, arduino::String> headers;
};
class NetworkResponse {
public:
uint8_t status = 0;
void appendLine(String line);
void addHeader(String name, String value);
arduino::String getHeaderValue(String name);
arduino::String getHTTPStringStatus();
private:
std::map<arduino::String, arduino::String> headers;
};
class Networking {
public:
Networking(const char *ssid_, const char *password_): ssid(ssid_), password(password_) {};
void onConnect(Callback callback);
void onDisconnect(Callback callback);
void checkHardware();
arduino::String getIP();
bool isConnected();
void act();
private:
const char *ssid;
const char *password;
unsigned long currentTime = millis();
unsigned long lastConnectionAttemptTime;
bool malfunctionReported = false;
bool connectionReported = false;
Callback onConnectCallback;
Callback onDisconnectCallback;
};
The code is licensed under the MIT license

So, in the networking, in turn, I have three classes defined: Request, Response and Networking. I wanted to make it ideologically close to how Express of NodeJS is designed.

However, it was only to a certain degree possible. The thing is, Arduino Nano has only 2kb of RAM. That means, I can't buffer pretty much anything. About 15% of RAM will be taken over by global variables, plus I need some space to allocate dynamic local memory. So, 2kb does not feel much at all. The fact that the amount of memory is so limited, leads to two major restrictions:

  1. An HTTP body must only be streamed, not stored beforehand.
  2. A JSON string coming as a request/response HTTP body should not be parsed in case if I need only a small fracture of it.

The implementation:

👉 📃  networking.cpp
#include <Arduino.h>
#include <map>
#include <functional>
#include <WiFi.h>
#include <WiFiNINA.h>
#include "networking.h"
#include "debug.h"
#include "util.h"
void NetworkRequest::appendLine(String line) {
const char delim = ' ';
auto lineParts = splitString(line, delim);
if (method == "") {
// first line always contains the method and the query: GET /foo, POST /bar
method = lineParts[0];
target = lineParts[1];
path = lineParts[1]; // todo: need to properly parse the path out of the target
} else if (lineParts.size() >= 2) {
// then we receive headers
auto header = lineParts[0].substring(0, lineParts[0].length() - 1);
headers[header] = lineParts[1];
}
}
arduino::String NetworkRequest::getHeaderValue(String name) {
if (auto search = headers.find(name); search != headers.end()) {
return search->second;
}
return "";
}
arduino::String NetworkRequest::getAPIToken() {
return getHeaderValue("x-auth-token");
}
// ///////////////////////
// ///////////////////////
// ///////////////////////
void NetworkResponse::addHeader(String name, String value) {
headers[name] = value;
}
arduino::String NetworkResponse::getHeaderValue(String name) {
if (auto search = headers.find(name); search != headers.end()) {
return search->second;
}
return "";
}
arduino::String NetworkResponse::getHTTPStringStatus() {
std::map<unsigned short int, arduino::String> httpStringCodes = {
{ 200, F("OK") },
{ 400, F("Bad Request") },
{ 401, F("Unauthorized") },
{ 403, F("Forbidden") },
{ 404, F("Not Found") },
{ 500, F("Internal Server Error") },
};
auto search_result = httpStringCodes.find(status);
if (auto search = httpStringCodes.find(status); search != httpStringCodes.end()) {
return arduino::String(status)+" "+search->second;
}
return "";
}
void NetworkResponse::appendLine(String line) {
const char delim = ' ';
auto lineParts = splitString(line, delim);
if (status == 0) {
// first line always contains the method and the query: HTTP/version status
status = static_cast<unsigned short int>(lineParts[1].toInt());
} else if (lineParts.size() >= 2) {
// then we receive headers
auto header = lineParts[0].substring(0, lineParts[0].length() - 1);
headers[header] = lineParts[1];
}
}
// ///////////////////////
// ///////////////////////
// ///////////////////////
void Networking::checkHardware() {
if (WiFi.status() == WL_NO_MODULE) {
if (!malfunctionReported) {
debug(F("Communication with WiFi module failed"));
malfunctionReported = true;
}
}
}
arduino::String Networking::getIP() {
auto ip = WiFi.localIP();
auto byte0 = arduino::String(ip[0]);
auto byte1 = arduino::String(ip[0]);
auto byte2 = arduino::String(ip[0]);
auto byte3 = arduino::String(ip[0]);
return byte0+"."+byte1+"."+byte2+"."+byte3;
}
bool Networking::isConnected() {
return WiFi.status() == WL_CONNECTED;
}
void Networking::act() {
if (malfunctionReported) {
return;
}
currentTime = millis();
auto connected = isConnected();
if (!connected && currentTime - lastConnectionAttemptTime > RECONNECTION_RETRY_DELAY) {
debug("Connecting to the wifi network");
WiFi.begin(ssid, password);
lastConnectionAttemptTime = currentTime;
}
if (connected && !connectionReported) {
debug("You're now connected to the wifi network");
if (onConnectCallback) {
onConnectCallback();
}
connectionReported = true;
}
if (!connected) {
connectionReported = false;
}
}
void Networking::onConnect(Callback callback) {
onConnectCallback = callback;
}
void Networking::onDisconnect(Callback callback) {
onDisconnectCallback = callback;
}
The code is licensed under the MIT license

Server

One more important thing regarding networking. Arduino obviously has no operation system whatsoever, that is why any kind of abstractions like network interfaces, routing, etc. we are used to are not available. There is only a library that implements some portion of the TCP/IP stack, but that library is tightly coupled with the physical way the device does the communicating: either through WiFi or Ethernet.

The server here allows public or protected routes to be created.

👉 📃  nserver.h
#include <Arduino.h>
#include <functional>
#include <WiFi.h>
#include <WiFiNINA.h>
#include "networking.h"
struct NServerRoute {
arduino::String method;
arduino::String path;
std::function<void(NetworkRequest, NetworkResponse)> prepareHeaders;
std::function<void(NetworkRequest, NetworkResponse, WiFiClient client)> streamBody;
bool isProtected;
};
class NServer {
public:
NServer(const char *token_): token(token_) {};
void addRoute(
arduino::String method,
arduino::String path,
std::function<void(NetworkRequest, NetworkResponse)> prepareHeaders,
std::function<void(NetworkRequest, NetworkResponse, WiFiClient client)> streamBody
);
void addProtectedRoute(
arduino::String method,
arduino::String path,
std::function<void(NetworkRequest, NetworkResponse)> prepareHeaders,
std::function<void(NetworkRequest, NetworkResponse, WiFiClient client)> streamBody
);
void sendResponse(NetworkRequest request, WiFiClient client);
void act();
bool isConnected();
private:
const char *token;
std::vector<NServerRoute> routes;
std::pair<NServerRoute, bool> getRoute(NetworkRequest &request);
unsigned long currentTime = millis();
unsigned long previousTime = 0;
bool serverCreated = false;
bool verifyRequestAuthorization(NetworkRequest &request, NetworkResponse &response);
arduino::String printRequest(NetworkRequest request);
};
The code is licensed under the MIT license

The implementation. One note here: I had to make the server variable global, otherwise the networking does not function at all. This is certainly a library bug, and there is not much I could do about it.

#include <Arduino.h>
#include <functional>
#include <vector>
#include <WiFi.h>
#include <WiFiNINA.h>
#include "networking.h"
#include "nserver.h"
#include "debug.h"
WiFiServer server(80);
void NServer::addRoute(
arduino::String method,
arduino::String path,
std::function<void(NetworkRequest, NetworkResponse)> prepareHeaders,
std::function<void(NetworkRequest, NetworkResponse, WiFiClient client)> streamBody
) {
NServerRoute middleware = {
method, path, prepareHeaders, streamBody, false
};
routes.push_back(middleware);
}
void NServer::addProtectedRoute(
arduino::String method,
arduino::String path,
std::function<void(NetworkRequest, NetworkResponse)> prepareHeaders,
std::function<void(NetworkRequest, NetworkResponse, WiFiClient client)> streamBody
) {
NServerRoute middleware = {
method, path, prepareHeaders, streamBody, true
};
routes.push_back(middleware);
}
std::pair<NServerRoute, bool> NServer::getRoute(NetworkRequest &request) {
std::pair <NServerRoute, bool> result;
result.second = false;
for (auto route : routes) {
if (route.method == request.method && route.path == request.path) {
result.first = route;
result.second = true;
}
}
return result;
}
bool NServer::verifyRequestAuthorization(NetworkRequest &request, NetworkResponse &response) {
auto requestToken = request.getAPIToken();
if (requestToken.length() == 0 || requestToken != arduino::String(token)) {
response.status = 401;
return false;
}
return true;
}
void NServer::sendResponse(NetworkRequest req, WiFiClient client) {
bool isAccessGranted = true;
NetworkResponse response;
response.status = 404;
response.addHeader(F("Content-Type"), F("application/json"));
auto search = getRoute(req);
if (search.second) {
response.status = 200;
if (search.first.isProtected) {
isAccessGranted = verifyRequestAuthorization(req, response);
}
if (isAccessGranted) {
search.first.prepareHeaders(req, response);
}
}
client.println("HTTP/1.1 "+response.getHTTPStringStatus());
client.println("Content-Type: "+response.getHeaderValue("Content-Type"));
client.println(F("Connection: close"));
client.println();
if (search.second) {
if (isAccessGranted) {
search.first.streamBody(req, response, client);
} else {
client.println(F("{\"error\": \"ACCESS_DENIED\"}"));
}
}
}
void NServer::act() {
if (!isConnected()) {
return;
}
if (!serverCreated) {
server.begin();
serverCreated = true;
}
WiFiClient client = server.available();
if (client) {
currentTime = millis();
previousTime = currentTime;
String currentLine = "";
NetworkRequest req;
while (client.connected() && currentTime - previousTime <= CLIENT_CONNECTION_TIMEOUT) {
currentTime = millis();
if (client.available()) {
char next_char = client.read();
if (next_char != '\r' && next_char != '\n') {
currentLine += next_char;
}
if (next_char == '\n') {
req.appendLine(currentLine);
if (currentLine.length() == 0) {
debug("Incoming request: "+printRequest(req));
sendResponse(req, client);
client.stop();
break;
}
currentLine = "";
}
}
}
}
}
bool NServer::isConnected() {
return WiFi.status() == WL_CONNECTED;
}
arduino::String NServer::printRequest(NetworkRequest req) {
return req.method+" "+req.path;
}
The code is licensed under the MIT license

Client

It gets a bit more tricky with the client. When a request is made, I can't just wait for the response, because the server on the other side needs time to answer, and therefore the operation becomes a blocking one. That is why I have to make the request, and put it into a ledger of pending requests. Then, when the act() function is called, I check if some responses are ready to be processed. When I read the response data, I execute a callback stored along with the request.

If a server does not respond in 5 seconds, I cancel the request and remove it from the list.

👉 📃  nclient.h
#pragma once
#include <WiFi.h>
#include <WiFiNINA.h>
#include <functional>
#include <unordered_set>
#include <Arduino.h>
#include "networking.h"
#define NCLIENT_CONNECTION_TIMEOUT 5000
using OnSuccessCallback = std::function<void(NetworkResponse, WiFiClient)>;
struct PendingRequest {
WiFiClient client;
OnSuccessCallback callback;
char *host;
arduino::String path;
arduino::String method;
unsigned long time;
};
struct PendingRequestHash {
std::size_t operator()(const PendingRequest &r) const {
return std::hash<unsigned long>()(r.time);
}
};
struct PendingRequestEqual {
bool operator()(const PendingRequest& lhs, const PendingRequest& rhs) const {
return lhs.time == rhs.time;
}
};
class NClient {
public:
void request(arduino::String method, char *host, arduino::String path, arduino::String body, OnSuccessCallback callback);
void act();
private:
std::unordered_set<PendingRequest, PendingRequestHash, PendingRequestEqual> pendingRequests;
bool isConnected();
arduino::String printRequest(PendingRequest request);
};
The code is licensed under the MIT license

And the implementation:

👉 📃  nclient.cpp
#include <WiFi.h>
#include <WiFiNINA.h>
#include <Arduino.h>
#include <functional>
#include <cstdint>
#include "networking.h"
#include "nclient.h"
#include "debug.h"
void NClient::request(arduino::String method, char *host, arduino::String path, arduino::String body, OnSuccessCallback callback) {
if(!isConnected()) {
return;
}
WiFiClient client;
if(client.connect(host, 80)) {
client.println(method+" "+path+" HTTP/1.1");
client.println("Host: "+arduino::String(host));
client.println(F("Accept: application/json"));
client.println(F("Content-Type: application/json"));
client.println(F("Connection: close"));
if (body.length() != 0) {
client.println("Content-Length: "+arduino::String(body.length()));
}
client.println();
if (body.length() != 0) {
client.println(body);
}
PendingRequest pendingRequest;
pendingRequest.client = client;
pendingRequest.callback = callback;
pendingRequest.time = millis();
pendingRequest.host = host;
pendingRequest.path = path;
pendingRequest.method = method;
pendingRequests.insert(pendingRequest);
debug("Request sent: "+printRequest(pendingRequest));
}
}
void NClient::act() {
if(!isConnected()) {
return;
}
// for (auto it = pendingRequests.begin(); it != pendingRequests.end(); ++it) {
for (auto it = pendingRequests.begin(); it != pendingRequests.end(); ++it) {
auto currentTime = millis();
auto req = *it;
auto client = req.client;
auto time = req.time;
if (currentTime - time > NCLIENT_CONNECTION_TIMEOUT) {
debug("Request timed out: "+printRequest(req));
client.stop();
pendingRequests.erase(it);
return;
}
if (client.available()) {
String currentLine = "";
NetworkResponse response;
while(client.available()) {
char nextChar = client.read();
if (nextChar != '\r' && nextChar != '\n') {
currentLine += nextChar;
}
if (nextChar == '\n') {
response.appendLine(currentLine);
if (currentLine.length() == 0) {
debug("Request served: "+printRequest(req));
req.callback(response, client);
break;
}
currentLine = "";
}
}
while(client.available()) {
client.read();
}
client.stop();
pendingRequests.erase(it);
break;
}
}
}
bool NClient::isConnected() {
return WiFi.status() == WL_CONNECTED;
}
arduino::String NClient::printRequest(PendingRequest req) {
return req.method+" http://"+arduino::String(req.host)+":80"+req.path;
}
The code is licensed under the MIT license

Weather API client

The weather indicator uses the network client to get the readings, so I made use of dependency injection via references to pass an instance of one class as an argument for a constructor of another.

I had some troubles reading the JSON body of the response. First, I tried to parse it using several of the many libraries out there. I could not do it, as it simply did not work out well with Arduino.

Then, I gave regexes a shot. The very moment I plugged the regex extension in, the memory consumption went from 15% to somewhat around 38%. "Holy monkeys!" - I immediately thought, and put this option aside.

And then I thought: "Why would I need all that, if out of that huge JSON I only need the value of one field?" So I ended up using a simple indexOf() and it worked like a charm.

👉 📃  weather.h
#pragma once
#include <cstdint>
#include <Arduino.h>
#include "nclient.h"
using GetTemperatureCallback = std::function<void(int16_t temperature)>;
using OnErrorCallback = std::function<void(arduino::String errorMessage)>;
class Weather {
public:
Weather(NClient &networkClient_, const char* token_, const char* location_): networkClient(networkClient_), token(token_), location(location_) {};
void GetTemperature(GetTemperatureCallback callback, OnErrorCallback onErrorCallback);
private:
NClient &networkClient;
const char* token;
const char* location;
};
The code is licensed under the MIT license

And the implementation:

👉 📃  weather.cpp
#include <Arduino.h>
#include <cstdint>
#include "nclient.h"
#include "weather.h"
#include "util.h"
#include "debug.h"
void Weather::GetTemperature(GetTemperatureCallback callback, OnErrorCallback onErrorCallback) {
networkClient.request(
"GET",
"api.weatherapi.com",
"/v1/current.json?key="+arduino::String(token)+"&q="+arduino::String(location),
"",
[callback, onErrorCallback](NetworkResponse clientResponse, WiFiClient requestClient) {
if (clientResponse.status != 200) {
auto message = "Could not process the response, status: "+arduino::String(clientResponse.status);
debug(message);
if (onErrorCallback) {
onErrorCallback(message);
}
return;
}
auto transferEncoding = clientResponse.getHeaderValue("Transfer-Encoding");
if (transferEncoding == "chunked") {
String currentLine = "";
int16_t chunkSize = -1;
while(requestClient.available()) {
char nextChar = requestClient.read();
if (!isEOL(nextChar)) {
currentLine += nextChar;
}
if (nextChar == '\n') {
if (chunkSize == -1) {
chunkSize = hexStringToInt16(currentLine);
} else {
if (chunkSize > 0) {
arduino::String tempKey = F("\"temp_c\":");
auto indexOfTempKey = currentLine.indexOf(tempKey);
if (indexOfTempKey >= 0) {
arduino::String value = "";
for (uint16_t i = indexOfTempKey + tempKey.length(); i < chunkSize; i++) {
auto charAt = currentLine[i];
if (charAt == ',') {
break;
}
value += charAt;
}
auto temperature = int16_t(atof(value.c_str()));
callback(temperature);
} else {
auto message = F("The temperature key was not found in the JSON!");
debug(message);
if (onErrorCallback) {
onErrorCallback(message);
}
}
chunkSize = -1;
break;
}
}
currentLine = "";
}
}
}
}
);
}
The code is licensed under the MIT license

Planner

The last of the many things was the planner. It is just a simple class that takes a callback and pulls it every N microseconds.

👉 📃  planner.h
#pragma once
#include <functional>
#include <Arduino.h>
#define PLANNER_INTERVAL 900000 // 15 minutes
using PlannerCallback = std::function<void()>;
class Planner {
public:
void onTick(PlannerCallback callbackParam);
void act();
private:
PlannerCallback callback;
unsigned long previousTime = 0;
};
The code is licensed under the MIT license

And the implementation:

👉 📃  planner.cpp
#include "planner.h"
#include "debug.h"
void Planner::onTick(PlannerCallback callbackParam) {
callback = callbackParam;
}
void Planner::act() {
auto currentTime = millis();
if (currentTime - previousTime > PLANNER_INTERVAL) {
callback();
previousTime = currentTime;
}
}
The code is licensed under the MIT license

Util

There are just some small util function I reused between the parts of the sketch:

👉 📃  util.h
#pragma once
#include <string>
#include <vector>
#include <Arduino.h>
#include <cstdint>
std::vector<arduino::String> splitString(arduino::String const str, const char delim);
bool isEOL(const char byte);
int16_t hexStringToInt16(const arduino::String str);
The code is licensed under the MIT license

And the implementation:

👉 📃  util.cpp
#include <string>
#include <vector>
#include <Arduino.h>
std::vector<arduino::String> splitString(arduino::String const str, const char delim) {
std::vector<arduino::String> result;
unsigned int lastFoundIndex = 0;
unsigned int stringLen = str.length();
unsigned int i = 0;
for (; i < stringLen; i++) {
if (str[i] == delim) {
result.push_back(str.substring(lastFoundIndex > 0 ? lastFoundIndex + 1 : lastFoundIndex, i));
lastFoundIndex = i;
}
}
if (lastFoundIndex != i) {
result.push_back(str.substring(lastFoundIndex + 1, i));
}
return result;
}
bool isEOL(const char byte) {
return byte == '\r' || byte == '\n';
}
int16_t hexStringToInt16(const arduino::String str) {
int strLen = str.length() + 1;
char stringChar[strLen];
str.toCharArray(stringChar, strLen);
char* endptr;
return static_cast<int16_t>(strtol(stringChar, &endptr, 16));
}
The code is licensed under the MIT license

Logging

The debugging data is not available after the device is unplugged from the IDE, so I needed some observability in the wild. Loggly by SolarWinds gives a free trial for 1 month.

Upd: I just saw an Ad of another service, that was, like, completely free: Logtail. So next time I might want to give that a try.

Nevertheless, with Loggly the client looks like this:

👉 📃  logger.h
#pragma once
#include <Arduino.h>
#include <cstdint>
#include "nclient.h"
class Logger {
public:
Logger(NClient &networkClient_, const char *logglyToken_): networkClient(networkClient_), logglyToken(logglyToken_) {};
void log(const arduino::String data);
void log(const char *data);
void log(const uint8_t data);
void log(const int16_t data);
private:
NClient &networkClient;
const char *logglyToken;
void logToLoggly(arduino::String data);
};
The code is licensed under the MIT license

And the implementation:

👉 📃  logger.cpp
#include <Arduino.h>
#include <cstdint>
#include <WiFi.h>
#include <WiFiNINA.h>
#include "logger.h"
#include "nclient.h"
#include "networking.h"
#include "debug.h"
void Logger::log(arduino::String data) {
logToLoggly(data);
}
void Logger::log(const char *data) {
logToLoggly(arduino::String(data));
}
void Logger::log(const uint8_t data) {
logToLoggly(arduino::String(data));
}
void Logger::log(const int16_t data) {
logToLoggly(arduino::String(data));
}
void Logger::logToLoggly(arduino::String data) {
networkClient.request(
"POST",
"logs-01.loggly.com",
"/inputs/"+arduino::String(logglyToken)+"/tag/http/",
data,
[](NetworkResponse clientResponse, WiFiClient requestClient) {}
);
}
The code is licensed under the MIT license

FINALLY, putting everything together

Right, so the sketch itself looks pretty nifty, since it just creates instances and put them into work.

Here, I have three endpoints defined:

  1. GET /info to return the device type, so we can later identify it in the network.
  2. GET /reload to ask the device to re-read the temperature from the API.
  3. GET /readings to get the current readings kept by the device.

Also, besides establishing the connection, there is a weather client wrapped into a callback, which is, in turn, fed to a planner job.

As I already mentioned, I re-used the code of a LED indicator I made before.

👉 📃  sketch.ino
#include <functional>
#include <cstdint>
#include "flags.h"
#include "debug.h"
#include "display.h"
#include "secrets.h"
#include "networking.h"
#include "nserver.h"
#include "nclient.h"
#include "weather.h"
#include "planner.h"
#include "logger.h"
#define CLK 7
#define DAT 8
Display display(CLK, DAT);
Networking networking(WIFI_SECRET_SSID, WIFI_SECRET_PASS);
NServer nServer(API_TOKEN);
NClient nClient;
Weather weather(nClient, WEATHER_API_TOKEN, WEATHER_API_LOCATION);
Logger logger(nClient, LOGGLY_TOKEN);
Planner planner;
int16_t readings = 0;
void setup() {
#if DEV_MODE==1
delay(2000);
#endif
debugInit();
display.startRollingAnimation();
networking.checkHardware();
PlannerCallback getWeatherIndication = [weather, display, logger](){
logger.log("{\"action\": \"getWeatherIndication\", \"result\": \"info\"}");
weather.GetTemperature([](int16_t temperature){
readings = temperature;
display.clearAnimation();
display.displayNumber(temperature);
logger.log("{\"temperature\": "+arduino::String(temperature)+", \"result\": \"success\"}");
}, [](arduino::String errorMessage){
logger.log("{\"error\": "+errorMessage+", \"result\": \"error\"}");
});
};
networking.onConnect([getWeatherIndication](){
getWeatherIndication();
});
planner.onTick(getWeatherIndication);
nServer.addRoute(
"GET",
"/info",
[](NetworkRequest request, NetworkResponse response) {},
[](NetworkRequest request, NetworkResponse response, WiFiClient client) {
client.println(F("{\"kind\": \"WEATHER_INDICATOR\", \"name\": \"Weather Indicator\"}"));
}
);
nServer.addRoute(
"GET",
"/reload",
[](NetworkRequest request, NetworkResponse response) {},
[getWeatherIndication](NetworkRequest request, NetworkResponse response, WiFiClient client) {
getWeatherIndication();
client.println(F("{\"ok\": 1}"));
}
);
nServer.addRoute(
"GET",
"/readings",
[](NetworkRequest request, NetworkResponse response) {},
[](NetworkRequest request, NetworkResponse response, WiFiClient client) {
client.println("{\"readings\": "+arduino::String(readings)+"}");
}
);
}
void loop(){
networking.act();
display.act();
nServer.act();
nClient.act();
planner.act();
}
The code is licensed under the MIT license

At the moment the uptime of the device is already something close to 10 days, and it does not freeze or show any false readings. That means hopefully I have done everything right, and there are no memory leaks or anything.

I can see the logs coming:

In the next article I am gonna make a UI for it!


Avatar

Sergei Gannochenko

Business-oriented fullstack engineer, in ❤️ with Tech.
Golang, React, TypeScript, Docker, AWS, Jamstack.
15+ years in dev.