diff --git a/noisemeter-device/api.cpp b/noisemeter-device/api.cpp
new file mode 100644
index 0000000..db3f624
--- /dev/null
+++ b/noisemeter-device/api.cpp
@@ -0,0 +1,206 @@
+/* noisemeter-device - Firmware for CivicTechTO's Noisemeter Device
+ * Copyright (C) 2024 Clyne Sullivan
+ *
+ * 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 .
+ */
+#include "api.h"
+#include "board.h"
+#include "certs.h"
+#include "url-encode.h"
+
+#include
+#include
+
+#include
+
+API::Request::Request(const char endpoint[])
+{
+ url.reserve(256);
+ url.concat(Base);
+ url.concat(endpoint);
+ url.concat('?');
+}
+
+API::Request& API::Request::addParam(const char param[], String value)
+{
+ url.concat(param);
+ url.concat('=');
+ url.concat(urlEncode(value));
+ url.concat('&');
+ return *this;
+}
+
+std::optional API::sendAuthorizedRequest(const API::Request& req)
+{
+ WiFiClientSecure client;
+ client.setCACert(rootCertificate());
+
+#ifdef API_VERBOSE
+ SERIAL.print("[api] Authorized request: ");
+ SERIAL.println(req.url);
+#endif
+
+ HTTPClient https;
+ if (https.begin(client, req.url)) {
+ https.addHeader("Authorization", String("Token ") + token);
+
+ if (const auto code = https.GET(); code > 0) {
+ if (code == HTTP_CODE_OK || code == HTTP_CODE_MOVED_PERMANENTLY) {
+ const auto response = https.getString();
+#ifdef API_VERBOSE
+ SERIAL.print("[api] Response: ");
+ SERIAL.println(response);
+#endif
+ https.end();
+
+ return responseToJson(response);
+#ifdef API_VERBOSE
+ } else {
+ SERIAL.print("[api] HTTP error: ");
+ SERIAL.println(code);
+#endif
+ }
+#ifdef API_VERBOSE
+ } else {
+ SERIAL.print("[api] HTTP error: ");
+ SERIAL.println(code);
+#endif
+ }
+#ifdef API_VERBOSE
+ } else {
+ SERIAL.println("[api] Failed to https.begin()");
+#endif
+ }
+
+ return {};
+}
+
+std::optional API::sendUnauthorizedRequest(const API::Request& req)
+{
+ WiFiClientSecure client;
+ client.setCACert(rootCertificate());
+
+#ifdef API_VERBOSE
+ SERIAL.print("[api] Unauthorized request: ");
+ SERIAL.println(req.url);
+#endif
+
+ HTTPClient https;
+ if (https.begin(client, req.url)) {
+ if (const auto code = https.GET(); code > 0) {
+ if (code == HTTP_CODE_OK || code == HTTP_CODE_MOVED_PERMANENTLY) {
+ const auto response = https.getString();
+#ifdef API_VERBOSE
+ SERIAL.print("[api] Response: ");
+ SERIAL.println(response);
+#endif
+ https.end();
+
+ return responseToJson(response);
+#ifdef API_VERBOSE
+ } else {
+ SERIAL.print("[api] HTTP error: ");
+ SERIAL.println(code);
+#endif
+ }
+#ifdef API_VERBOSE
+ } else {
+ SERIAL.print("[api] HTTP error: ");
+ SERIAL.println(code);
+#endif
+ }
+#ifdef API_VERBOSE
+ } else {
+ SERIAL.println("[api] Failed to https.begin()");
+#endif
+ }
+
+ return {};
+}
+
+std::optional API::responseToJson(const String& response)
+{
+ JsonDocument doc;
+ const auto error = deserializeJson(doc, response);
+
+ if (error) {
+ SERIAL.println(error.f_str());
+ return {};
+ } else {
+ return doc;
+ }
+}
+
+API::API(UUID id_, String token_):
+ id(id_), token(token_) {}
+
+bool API::sendMeasurement(const DataPacket& packet)
+{
+ const auto request = Request("measurement")
+ .addParam("device", id)
+ .addParam("timestamp", packet.timestamp)
+ .addParam("min", String(std::lround(packet.minimum)))
+ .addParam("max", String(std::lround(packet.maximum)))
+ .addParam("mean", String(std::lround(packet.average)));
+
+ const auto resp = sendAuthorizedRequest(request);
+ return resp && (*resp)["result"] == "ok";
+}
+
+bool API::sendDiagnostics(String version, String boottime)
+{
+ const auto request = Request("measurement")
+ .addParam("device", id)
+ .addParam("version", version)
+ .addParam("boottime", boottime);
+
+ const auto resp = sendAuthorizedRequest(request);
+ return resp && (*resp)["result"] == "ok";
+}
+
+std::optional API::sendRegister(String email)
+{
+ const auto request = Request("device/register")
+ .addParam("device", id)
+ .addParam("email", email);
+
+ const auto resp = sendUnauthorizedRequest(request);
+ if (resp && (*resp)["result"] == "ok")
+ return (*resp)["token"];
+ else
+ return {};
+}
+
+std::optional API::getLatestSoftware()
+{
+ const auto request = Request("software/latest");
+
+ const auto resp = sendUnauthorizedRequest(request);
+ if (resp && (*resp)["result"] == "ok") {
+ LatestSoftware ls = {
+ (*resp)["version"],
+ (*resp)["url"]
+ };
+
+ return ls;
+ } else {
+ return {};
+ }
+}
+
+const char *API::rootCertificate()
+{
+ return cert_ISRG_Root_X1;
+}
+
diff --git a/noisemeter-device/api.h b/noisemeter-device/api.h
new file mode 100644
index 0000000..4724b75
--- /dev/null
+++ b/noisemeter-device/api.h
@@ -0,0 +1,67 @@
+/// @file
+/// @brief API implementation for communication with server
+/* noisemeter-device - Firmware for CivicTechTO's Noisemeter Device
+ * Copyright (C) 2024 Clyne Sullivan
+ *
+ * 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 .
+ */
+#ifndef API_H
+#define API_H
+
+#include "data-packet.h"
+#include "UUID/UUID.h"
+
+#include
+#include
+
+#include
+
+class API
+{
+ constexpr static const char Base[] = "https://noisemeter.webcomand.com/api/v1/";
+
+ struct Request {
+ Request(const char endpoint[]);
+
+ Request& addParam(const char param[], String value);
+
+ String url;
+ };
+
+public:
+ struct LatestSoftware {
+ String version;
+ String url;
+ };
+
+ API(UUID id_, String token_ = {});
+
+ bool sendMeasurement(const DataPacket& packet);
+ bool sendDiagnostics(String version, String boottime);
+ std::optional sendRegister(String email);
+ std::optional getLatestSoftware();
+
+ static const char *rootCertificate();
+
+private:
+ UUID id;
+ String token;
+
+ std::optional responseToJson(const String& response);
+ std::optional sendAuthorizedRequest(const Request& req);
+ std::optional sendUnauthorizedRequest(const Request& req);
+};
+
+#endif // API_H
+
diff --git a/noisemeter-device/certs.py b/noisemeter-device/certs.py
old mode 100644
new mode 100755
diff --git a/noisemeter-device/config.h.example b/noisemeter-device/config.h.example
index 4584af3..cf2528d 100644
--- a/noisemeter-device/config.h.example
+++ b/noisemeter-device/config.h.example
@@ -20,6 +20,9 @@
// Uncomment to print credentials over serial (for debugging).
//#define STORAGE_SHOW_CREDENTIALS
+// Uncomment to print verbose API logging over serial (for debugging).
+//#define API_VERBOSE
+
// Define only *one* of the follwoing board options.
// If using PlatformIO, the selected 'env' will override this selection.
#if !defined(BOARD_ESP32_PCB) && \
diff --git a/noisemeter-device/noisemeter-device.ino b/noisemeter-device/noisemeter-device.ino
index eaad012..7626a8d 100644
--- a/noisemeter-device/noisemeter-device.ino
+++ b/noisemeter-device/noisemeter-device.ino
@@ -16,29 +16,22 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-#include // https://arduinojson.org/
-#include
-#include
#include
-#include
#include
#include
#include "access-point.h"
+#include "api.h"
#include "blinker.h"
#include "board.h"
#include "data-packet.h"
-#include "certs.h"
-#include "secret.h"
#include "spl-meter.h"
#include "storage.h"
#include "ota-update.h"
#include "UUID/UUID.h"
-#include
#include
#include
-#include
#include
#if defined(BUILD_PLATFORMIO) && defined(BOARD_ESP32_PCB)
@@ -69,14 +62,6 @@ static Timestamp lastUpload = Timestamp::invalidTimestamp();
/** Tracks when the last OTA update check occurred. */
static Timestamp lastOTACheck = Timestamp::invalidTimestamp();
-/**
- * Outputs an array of floating-point values over serial.
- * @deprecated Unused and unlikely to be used in the future
- * @param arr The array to output
- * @param length Number of values from arr to display
- */
-void printArray(float arr[], unsigned long length);
-
/**
* Outputs the given decibel reading over serial.
* @param reading The decibel reading to display
@@ -102,31 +87,6 @@ UUID buildDeviceId();
*/
int tryWifiConnection();
-/**
- * Creates a serialized JSON payload containing the given data.
- * @param dp DataPacket containing the data to serialize
- * @return String containing the resulting JSON payload
- */
-String createJSONPayload(const DataPacket& dp);
-
-/**
- * Upload a serialized JSON payload to our server.
- * @param json JSON payload to be sent
- * @return Zero on success or a negative number on failure
- */
-int uploadData(String json);
-
-/**
- * Prepares the I2S peripheral for reading microphone data.
- */
-void initMicrophone();
-
-/**
- * Reads a set number of samples from the microphone and factors them into
- * our decibel calculations and statistics.
- */
-void readMicrophoneData();
-
/**
* Firmware entry point and initialization routine.
*/
@@ -138,10 +98,6 @@ void setup() {
// Useful as a "reset" button to overwrite currently saved credentials.
pinMode(PIN_BUTTON, INPUT_PULLUP);
- // If needed, now you can actually lower the CPU frquency,
- // i.e. if you want to (slightly) reduce ESP32 power consumption
- // setCpuFrequencyMhz(80); // It should run as low as 80MHz
-
SERIAL.begin(115200);
delay(2000);
SERIAL.println();
@@ -180,6 +136,25 @@ void setup() {
ap.run(); // does not return
}
+ if (const auto email = Creds.get(Storage::Entry::Email); email.length() > 0) {
+ API api (buildDeviceId());
+ const auto registration = api.sendRegister(email);
+
+ if (registration) {
+ Creds.set(Storage::Entry::Email, {});
+ Creds.set(Storage::Entry::Token, *registration);
+ Creds.commit();
+
+ SERIAL.print("Registered! ");
+ SERIAL.println(*registration);
+ } else {
+ SERIAL.println("Failed to register!");
+ Creds.clear();
+ delay(2000);
+ ESP.restart();
+ }
+ }
+
Timestamp now;
lastUpload = now;
lastOTACheck = now;
@@ -217,6 +192,8 @@ void loop() {
}
if (WiFi.status() == WL_CONNECTED) {
+ API api (buildDeviceId(), Creds.get(Storage::Entry::Token));
+
{
std::optional bl;
@@ -224,13 +201,8 @@ void loop() {
if (++packets.cbegin() != packets.cend())
bl.emplace(300);
- packets.remove_if([](const auto& pkt) {
- if (pkt.count > 0) {
- const auto payload = createJSONPayload(pkt);
- return uploadData(payload) == 0;
- } else {
- return true; // Discard empty packets
- }
+ packets.remove_if([&api](const auto& pkt) {
+ return pkt.count <= 0 || api.sendMeasurement(pkt);
});
}
@@ -240,20 +212,24 @@ void loop() {
lastOTACheck = now;
SERIAL.println("Checking for updates...");
- OTAUpdate ota (cert_ISRG_Root_X1);
- if (ota.available()) {
- SERIAL.print(ota.version);
- SERIAL.println(" available!");
-
- if (ota.download()) {
- SERIAL.println("Download success! Restarting...");
- delay(1000);
- ESP.restart();
+ const auto ota = api.getLatestSoftware();
+ if (ota) {
+ if (ota->version.compareTo(NOISEMETER_VERSION) > 0) {
+ SERIAL.print(ota->version);
+ SERIAL.println(" available!");
+
+ if (downloadOTAUpdate(ota->url, api.rootCertificate())) {
+ SERIAL.println("Download success! Restarting...");
+ delay(1000);
+ ESP.restart();
+ } else {
+ SERIAL.println("Update download failed.");
+ }
} else {
- SERIAL.println("Update download failed.");
+ SERIAL.println("No new updates.");
}
} else {
- SERIAL.println("No update available.");
+ SERIAL.println("Failed to reach update server!");
}
}
#endif // BOARD_ESP32_PCB
@@ -277,16 +253,6 @@ void loop() {
#endif // !UPLOAD_DISABLED
}
-void printArray(float arr[], unsigned long length) {
- SERIAL.print(length);
- SERIAL.print(" {");
- for (int i = 0; i < length; i++) {
- SERIAL.print(arr[i]);
- if (i < length - 1) SERIAL.print(", ");
- }
- SERIAL.println("}");
-}
-
void printReadingToConsole(double reading) {
String output = "";
output += std::lround(reading);
@@ -304,13 +270,15 @@ bool saveNetworkCreds(WebServer& httpServer) {
if (httpServer.hasArg("ssid") && httpServer.hasArg("psk")) {
const auto ssid = httpServer.arg("ssid");
const auto psk = httpServer.arg("psk");
+ const auto email = httpServer.arg("email");
// Confirm that the given credentials will fit in the allocated EEPROM space.
- if (!ssid.isEmpty() && Creds.canStore(ssid) && Creds.canStore(psk)) {
+ if (!ssid.isEmpty() && Creds.canStore(ssid) && Creds.canStore(psk) && Creds.canStore(email)) {
Creds.set(Storage::Entry::SSID, ssid);
Creds.set(Storage::Entry::Passkey, psk);
- Creds.set(Storage::Entry::Token, API_TOKEN);
+ Creds.set(Storage::Entry::Email, email);
Creds.commit();
+
return true;
}
}
@@ -347,74 +315,3 @@ int tryWifiConnection()
return connected ? 0 : -1;
}
-String createJSONPayload(const DataPacket& dp)
-{
-#ifdef BUILD_PLATFORMIO
- JsonDocument doc;
-#else
- DynamicJsonDocument doc (2048);
-#endif
-
- doc["parent"] = "/Bases/nm1";
- doc["data"]["type"] = "comand";
- doc["data"]["version"] = "1.0";
- doc["data"]["contents"][0]["Type"] = "Noise";
- doc["data"]["contents"][0]["Min"] = std::lround(dp.minimum);
- doc["data"]["contents"][0]["Max"] = std::lround(dp.maximum);
- doc["data"]["contents"][0]["Mean"] = std::lround(dp.average);
- doc["data"]["contents"][0]["DeviceID"] = String(buildDeviceId());
- doc["data"]["contents"][0]["Timestamp"] = String(dp.timestamp);
-
- // Serialize JSON document
- String json;
- serializeJson(doc, json);
- return json;
-}
-
-// Given a serialized JSON payload, upload the data to webcomand
-int uploadData(String json)
-{
- WiFiClientSecure client;
- HTTPClient https;
-
- client.setCACert(cert_ISRG_Root_X1);
-
- SERIAL.print("[HTTPS] begin...\n");
- if (https.begin(client, "https://noisemeter.webcomand.com/ws/put")) {
- SERIAL.print("[HTTPS] POST...\n");
-
- // start connection and send HTTP header
- https.addHeader("Authorization", String("Token ") + API_TOKEN);
- https.addHeader("Content-Type", "application/json");
- https.addHeader("Content-Length", String(json.length()));
- https.addHeader("User-Agent", "ESP32");
-
- int httpCode = https.POST(json);
-
- // httpCode will be negative on error
- if (httpCode > 0) {
- // HTTP header has been send and Server response header has been handled
- SERIAL.printf("[HTTPS] POST... code: %d\n", httpCode);
-
- // file found at server
- if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
- String payload = https.getString();
- SERIAL.println(payload);
- } else {
- SERIAL.printf("[HTTPS] POST... failed, error: %s\n", https.errorToString(httpCode).c_str());
- return -1;
- }
- } else {
- SERIAL.printf("[HTTPS] POST... failed, error: %s\n", https.errorToString(httpCode).c_str());
- return -1;
- }
-
- https.end();
- } else {
- SERIAL.printf("[HTTPS] Unable to connect\n");
- return -1;
- }
-
- return 0;
-}
-
diff --git a/noisemeter-device/ota-update.cpp b/noisemeter-device/ota-update.cpp
index 6a2e276..5bb3070 100644
--- a/noisemeter-device/ota-update.cpp
+++ b/noisemeter-device/ota-update.cpp
@@ -21,56 +21,19 @@
#include
#include
#include
+#include
#include "board.h"
-constexpr auto OTA_JSON_URL = "https://live.noisemeter.webcomand.com/updates/latest.json";
+static bool applyUpdate(WiFiClientSecure& client, int totalSize);
-bool OTAUpdate::available()
-{
- WiFiClientSecure client;
- client.setCACert(rootCA);
-
- HTTPClient https;
- if (https.begin(client, OTA_JSON_URL)) {
- const auto code = https.GET();
-
- if (code == HTTP_CODE_OK || code == HTTP_CODE_MOVED_PERMANENTLY) {
- const auto response = client.readString();
-
- JsonDocument doc;
- const auto error = deserializeJson(doc, response);
- if (!error) {
- version = doc["Version"].as();
- if (version.compareTo(NOISEMETER_VERSION) > 0) {
- url = doc["URL"].as();
- return !url.isEmpty();
- } else {
- SERIAL.print("Server version: ");
- SERIAL.println(version);
- }
- } else {
- SERIAL.print("json failed: ");
- SERIAL.println(error.f_str());
- }
- } else {
- SERIAL.print("Bad HTTP response: ");
- SERIAL.println(code);
- }
- } else {
- SERIAL.println("Unable to connect.");
- }
-
- return false;
-}
-
-bool OTAUpdate::download()
+bool downloadOTAUpdate(String url, String rootCA)
{
if (url.isEmpty())
return false;
WiFiClientSecure client;
- client.setCACert(rootCA);
+ client.setCACert(rootCA.c_str());
HTTPClient https;
if (https.begin(client, url)) {
@@ -89,7 +52,7 @@ bool OTAUpdate::download()
return false;
}
-bool OTAUpdate::applyUpdate(WiFiClientSecure& client, int totalSize)
+bool applyUpdate(WiFiClientSecure& client, int totalSize)
{
std::array buffer;
diff --git a/noisemeter-device/ota-update.h b/noisemeter-device/ota-update.h
index b5f8de0..c1f9713 100644
--- a/noisemeter-device/ota-update.h
+++ b/noisemeter-device/ota-update.h
@@ -20,49 +20,12 @@
#define OTA_UPDATE_H
#include
-#include
/**
- * Manages fetching and installing of OTA software updates.
+ * Downloads and applies the latest OTA update.
+ * @return True if update is successfully downloaded and installed.
*/
-struct OTAUpdate
-{
- /** Stores the latest version string from the server (for logging). */
- String version;
-
- /**
- * Creates an OTAUpdate object.
- * @param rootCA_ Root certificate to use for HTTPS requests.
- */
- OTAUpdate(const char *rootCA_):
- rootCA(rootCA_) {}
-
- /**
- * Checks if a new OTA update is available.
- * @return True if available.
- */
- bool available();
-
- /**
- * Downloads and applies the latest OTA update.
- * @return True if update is successfully downloaded and installed.
- */
- bool download();
-
-private:
- /** Stores fetched URL for the latest update. */
- String url;
- /** Stores the given root certificate for HTTPS. */
- const char *rootCA;
-
- /**
- * Writes the received OTA update to flash memory.
- * @param client An active client for the update download.
- * @param totalSize The total size of the update if known.
- * @return True if update is successfully installed.
- */
- bool applyUpdate(WiFiClientSecure& client, int totalSize);
-};
+bool downloadOTAUpdate(String url, String rootCA);
#endif // OTA_UPDATE_H
diff --git a/noisemeter-device/storage.h b/noisemeter-device/storage.h
index 668e555..a1ad587 100644
--- a/noisemeter-device/storage.h
+++ b/noisemeter-device/storage.h
@@ -41,7 +41,8 @@ class Storage : protected EEPROMClass
SSID = Checksum + sizeof(uint32_t), /** User's WiFi SSID */
Passkey = SSID + StringSize, /** User's WiFi passkey */
Token = Passkey + StringSize, /** Device API token */
- TotalSize = Token + StringSize /** Marks storage end address */
+ Email = Token + StringSize, /** Temporary storage of user's email */
+ TotalSize = Email + StringSize /** Marks storage end address */
};
/**
diff --git a/noisemeter-device/url-encode.h b/noisemeter-device/url-encode.h
new file mode 100644
index 0000000..ee09e9d
--- /dev/null
+++ b/noisemeter-device/url-encode.h
@@ -0,0 +1,51 @@
+/* MIT License
+ *
+ * Copyright (c) 2022 Masayuki Sugahara
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef URL_ENCODE_H
+#define URL_ENCODE_H
+
+#include
+
+String urlEncode(const char *msg) {
+ const char *hex = "0123456789ABCDEF";
+ String encodedMsg = "";
+
+ while (*msg != '\0') {
+ if (
+ ('a' <= *msg && *msg <= 'z') || ('A' <= *msg && *msg <= 'Z') || ('0' <= *msg && *msg <= '9') || *msg == '-' || *msg == '_' || *msg == '.' || *msg == '~') {
+ encodedMsg += *msg;
+ } else {
+ encodedMsg += '%';
+ encodedMsg += hex[(unsigned char)*msg >> 4];
+ encodedMsg += hex[*msg & 0xf];
+ }
+ msg++;
+ }
+ return encodedMsg;
+}
+
+String urlEncode(String msg) {
+ return urlEncode(msg.c_str());
+}
+
+#endif // URL_ENCODE_H
+
diff --git a/platformio.ini b/platformio.ini
index d41ecbc..993d5e9 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -26,7 +26,7 @@ build_flags =
-std=gnu++17
-DBUILD_PLATFORMIO
-DNO_GLOBAL_EEPROM
- -DNOISEMETER_VERSION=\"0.0.4\"
+ -DNOISEMETER_VERSION=\"0.1.0\"
-Wall -Wextra
[env:esp32-pcb]