diff --git a/.gitattributes b/.gitattributes index dfe0770..e3c240d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ # Auto detect text files and perform LF normalization * text=auto +reference/** linguist-vendored +noisemeter-device/certs.py linguist-vendored diff --git a/noisemeter-device/access-point.h b/noisemeter-device/access-point.h index faa574f..7891f4c 100644 --- a/noisemeter-device/access-point.h +++ b/noisemeter-device/access-point.h @@ -11,7 +11,7 @@ class AccessPoint public: AccessPoint(): - server(80) {} + server(80), funcOnCredentialsReceived(nullptr) {} // Configure the WiFi radio to be an access point. void begin(); diff --git a/noisemeter-device/board.h b/noisemeter-device/board.h index f10eafa..886d5ef 100644 --- a/noisemeter-device/board.h +++ b/noisemeter-device/board.h @@ -1,6 +1,8 @@ #ifndef BOARD_H #define BOARD_H +#include "config.h" + #undef SERIAL #if defined(BOARD_ESP32_PCB) @@ -46,4 +48,3 @@ extern HWCDC USBSerial; #endif #endif // BOARD_H - diff --git a/noisemeter-device/data-packet.h b/noisemeter-device/data-packet.h new file mode 100644 index 0000000..c801071 --- /dev/null +++ b/noisemeter-device/data-packet.h @@ -0,0 +1,27 @@ +#ifndef DATAPACKET_H +#define DATAPACKET_H + +#include "timestamp.h" + +#include + +struct DataPacket +{ + constexpr DataPacket() = default; + + void add(float sample) noexcept { + count++; + minimum = std::min(minimum, sample); + maximum = std::max(maximum, sample); + average += (sample - average) / count; + } + + int count = 0; + float minimum = 999.f; + float maximum = 0.f; + float average = 0.f; + Timestamp timestamp = Timestamp::invalidTimestamp(); +}; + +#endif // DATAPACKET_H + diff --git a/noisemeter-device/noisemeter-device.ino b/noisemeter-device/noisemeter-device.ino index 9711a20..a595b81 100644 --- a/noisemeter-device/noisemeter-device.ino +++ b/noisemeter-device/noisemeter-device.ino @@ -9,8 +9,6 @@ * - Add second step to Access Point flow - to gather users email, generate a UUID and upload them to the cloud. UUID to be saved in EEPROM * - Add functionality to reset the device periodically (eg every 24 hours) */ -#include "config.h" - #include // https://arduinojson.org/ #include #include @@ -18,25 +16,26 @@ #include // https://github.com/RobTillaart/UUID #include #include -#include #include // ESP32 core #include // ESP32 core #include "access-point.h" #include "board.h" +#include "data-packet.h" #include "sos-iir-filter.h" #include "certs.h" #include "secret.h" #include "storage.h" #include +#include +#include #if defined(BUILD_PLATFORMIO) && defined(BOARD_ESP32_PCB) HWCDC USBSerial; #endif static Storage Creds; -static WiFiMulti WiFiMulti; // Uncomment these to disable WiFi and/or data upload //#define UPLOAD_DISABLED @@ -45,9 +44,8 @@ constexpr auto UpdateEndpoint = "https://www.bitgloo.com/files/noisemeter-updat constexpr auto VersionEndpoint = "https://www.bitgloo.com/files/noisemeter-update.txt"; constexpr auto VersionNumber = "v0.2"; -const unsigned long UPLOAD_INTERVAL_MS = 60000 * 5; // Upload every 5 mins -// const unsigned long UPLOAD_INTERVAL_MS = 30000; // Upload every 30 secs -const unsigned long MIN_READINGS_BEFORE_UPLOAD = 20; +const unsigned long UPLOAD_INTERVAL_SEC = 60 * 5; // Upload every 5 mins +// const unsigned long UPLOAD_INTERVAL_SEC = 30; // Upload every 30 secs // // Constants & Config @@ -93,47 +91,19 @@ struct sum_queue_t { }; // Static buffer for block of samples -float samples[SAMPLES_SHORT] __attribute__((aligned(4))); - -// Not sure if WiFiClientSecure checks the validity date of the certificate. -// Setting clock just to be sure... -void setClock() { - configTime(0, 0, "pool.ntp.org"); - - SERIAL.print(F("Waiting for NTP time sync: ")); - time_t nowSecs = time(nullptr); - while (nowSecs < 8 * 3600 * 2) { - delay(500); - SERIAL.print(F(".")); - yield(); - nowSecs = time(nullptr); - } - - SERIAL.println(); - struct tm timeinfo; - gmtime_r(&nowSecs, &timeinfo); - SERIAL.print(F("Current time: ")); - SERIAL.print(asctime(&timeinfo)); -} - -/** - * Returns true if the "reset" button is pressed, meaning the user wants to input new credentials. - */ -bool isCredsResetPressed(); +static_assert(sizeof(float) == sizeof(int32_t)); +using SampleBuffer alignas(4) = float[SAMPLES_SHORT]; +static SampleBuffer samples; // Sampling Buffers & accumulators sum_queue_t q; uint32_t Leq_samples = 0; double Leq_sum_sqr = 0; double Leq_dB = 0; -size_t bytes_read = 0; // Noise Level Readings -int numberOfReadings = 0; -float minReading = MIC_OVERLOAD_DB; -float maxReading = MIC_NOISE_DB; -float sumReadings = 0; -unsigned long lastUploadMillis = 0; +static std::forward_list packets; +static Timestamp lastUpload = Timestamp::invalidTimestamp(); /** * Initialization routine. @@ -166,9 +136,12 @@ void setup() { initMicrophone(); + packets.emplace_front(); + #ifndef UPLOAD_DISABLED // Run the access point if it is requested or if there are no valid credentials. - if (isCredsResetPressed() || !Creds.valid()) { + bool resetPressed = !digitalRead(PIN_BUTTON); + if (resetPressed || !Creds.valid()) { AccessPoint ap; SERIAL.println("Erasing stored credentials..."); @@ -188,11 +161,11 @@ void setup() { SERIAL.println(ssid); WiFi.mode(WIFI_STA); - WiFiMulti.addAP(ssid.c_str(), Creds.get(Storage::Entry::Passkey).c_str()); + WiFi.begin(ssid.c_str(), Creds.get(Storage::Entry::Passkey).c_str()); // wait for WiFi connection SERIAL.print("Waiting for WiFi to connect..."); - while ((WiFiMulti.run() != WL_CONNECTED)) { + while (WiFi.status() != WL_CONNECTED) { SERIAL.print("."); delay(500); } @@ -200,7 +173,11 @@ void setup() { SERIAL.print("Local ESP32 IP: "); SERIAL.println(WiFi.localIP()); - setClock(); + SERIAL.println("Waiting for NTP time sync..."); + Timestamp::synchronize(); + lastUpload = Timestamp(); + SERIAL.print("Current time: "); + SERIAL.println(lastUpload); #endif // !UPLOAD_DISABLED digitalWrite(PIN_LED1, HIGH); @@ -332,12 +309,34 @@ void loop() { } #ifndef UPLOAD_DISABLED - if (canUploadData()) { - WiFiClientSecure* client = new WiFiClientSecure; - float average = sumReadings / numberOfReadings; - String payload = createJSONPayload(DEVICE_ID, minReading, maxReading, average); - uploadData(client, payload); - delete client; + // Has it been at least the upload interval since we uploaded data? + const auto now = Timestamp(); + if (lastUpload.secondsBetween(now) >= UPLOAD_INTERVAL_SEC) { + lastUpload = now; + packets.front().timestamp = now; + + if (WiFi.status() != WL_CONNECTED) { + SERIAL.println("Attempting WiFi reconnect..."); + WiFi.reconnect(); + delay(5000); + } + + if (WiFi.status() == WL_CONNECTED) { + packets.remove_if([](const auto& pkt) { + const auto payload = createJSONPayload(DEVICE_ID, pkt); + + WiFiClientSecure client; + return uploadData(&client, payload) == 0; + }); + } + + if (!packets.empty()) { + SERIAL.print(std::distance(packets.cbegin(), packets.cend())); + SERIAL.println(" packets still need to be sent!"); + } + + // Create new packet for next measurements + packets.emplace_front(); } #endif // !UPLOAD_DISABLED } @@ -356,28 +355,20 @@ void printReadingToConsole(double reading) { String output = ""; output += reading; output += "dB"; - if (numberOfReadings > 1) { - output += " [+" + String(numberOfReadings - 1) + " more]"; + + const auto currentCount = packets.front().count; + if (currentCount > 1) { + output += " [+" + String(currentCount - 1) + " more]"; } SERIAL.println(output); } -bool isCredsResetPressed() { - bool pressed = !digitalRead(PIN_BUTTON); - - SERIAL.println(); - SERIAL.print("Is reset detected: "); - SERIAL.println(pressed); - return pressed; -} - void saveNetworkCreds(WebServer& httpServer) { // Confirm that the form was actually submitted. if (httpServer.hasArg("ssid") && httpServer.hasArg("psk")) { const auto ssid = httpServer.arg("ssid"); const auto psk = httpServer.arg("psk"); UUID uuid; // generates random UUID - SERIAL.println(uuid.toCharArray()); // Confirm that the given credentials will fit in the allocated EEPROM space. if (Creds.canStore(ssid) && Creds.canStore(psk)) { @@ -399,20 +390,23 @@ void saveNetworkCreds(WebServer& httpServer) { SERIAL.println("Error: Invalid network credentials!"); } -String createJSONPayload(String deviceId, float min, float max, float average) { +String createJSONPayload(String deviceId, 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"] = min; - doc["data"]["contents"][0]["Max"] = max; - doc["data"]["contents"][0]["Mean"] = average; + doc["data"]["contents"][0]["Min"] = dp.minimum; + doc["data"]["contents"][0]["Max"] = dp.maximum; + doc["data"]["contents"][0]["Mean"] = dp.average; doc["data"]["contents"][0]["DeviceID"] = deviceId; // TODO + doc["data"]["contents"][0]["Timestamp"] = String(dp.timestamp); // Serialize JSON document String json; @@ -420,18 +414,9 @@ String createJSONPayload(String deviceId, float min, float max, float average) { return json; } -bool canUploadData() { - // Has it been at least the upload interval since we uploaded data? - long now = millis(); - long msSinceLastUpload = now - lastUploadMillis; - if (msSinceLastUpload < UPLOAD_INTERVAL_MS) return false; - - // Do we have the minimum number of readings stored to form a resonable average? - if (numberOfReadings < MIN_READINGS_BEFORE_UPLOAD) return false; - return true; -} - -void uploadData(WiFiClientSecure* client, String json) { +// Given a serialized JSON payload, upload the data to webcomand +int uploadData(WiFiClientSecure* client, String json) +{ if (client) { client->setCACert(cert_ISRG_Root_X1); { @@ -464,14 +449,19 @@ void uploadData(WiFiClientSecure* client, String json) { 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; } // End extra scoping block @@ -480,21 +470,11 @@ void uploadData(WiFiClientSecure* client, String json) { // delete client; } else { SERIAL.println("Unable to create client"); + return -1; } - long now = millis(); - lastUploadMillis = now; - resetReading(); -}; // Given a serialized JSON payload, upload the data to webcomand - -void resetReading() { - SERIAL.println("Resetting readings cache..."); - numberOfReadings = 0; - minReading = MIC_OVERLOAD_DB; - maxReading = MIC_NOISE_DB; - sumReadings = 0; - SERIAL.println("Reset complete"); -}; + return 0; +} // // I2S Microphone sampling setup @@ -514,7 +494,9 @@ void initMicrophone() { dma_buf_len: DMA_BANK_SIZE, use_apll: true, tx_desc_auto_clear: false, - fixed_mclk: 0 + fixed_mclk: 0, + mclk_multiple: I2S_MCLK_MULTIPLE_DEFAULT, + bits_per_chan: I2S_BITS_PER_CHAN_DEFAULT, }; // I2S pin mapping @@ -530,7 +512,8 @@ void initMicrophone() { i2s_set_pin(I2S_PORT, &pin_config); // Discard first block, microphone may need time to startup and settle. - i2s_read(I2S_PORT, &samples, SAMPLES_SHORT * sizeof(int32_t), &bytes_read, portMAX_DELAY); + size_t bytes_read; + i2s_read(I2S_PORT, samples, sizeof(samples), &bytes_read, portMAX_DELAY); } void readMicrophoneData() { @@ -541,12 +524,13 @@ void readMicrophoneData() { // // Note: i2s_read does not care it is writing in float[] buffer, it will write // integer values to the given address, as received from the hardware peripheral. - i2s_read(I2S_PORT, &samples, SAMPLES_SHORT * sizeof(SAMPLE_T), &bytes_read, portMAX_DELAY); + size_t bytes_read; + i2s_read(I2S_PORT, samples, sizeof(samples), &bytes_read, portMAX_DELAY); // Convert (including shifting) integer microphone values to floats, // using the same buffer (assumed sample size is same as size of float), // to save a bit of memory - SAMPLE_T* int_samples = (SAMPLE_T*)&samples; + auto int_samples = reinterpret_cast(samples); for (int i = 0; i < SAMPLES_SHORT; i++) samples[i] = MIC_CONVERT(int_samples[i]); @@ -579,13 +563,8 @@ void readMicrophoneData() { Leq_sum_sqr = 0; Leq_samples = 0; - // SERIAL.printf("Calculated dB: %.1fdB\n", Leq_dB); printReadingToConsole(Leq_dB); - - if (Leq_dB < minReading) minReading = Leq_dB; - if (Leq_dB > maxReading) maxReading = Leq_dB; - sumReadings += Leq_dB; - numberOfReadings++; + packets.front().add(Leq_dB); digitalWrite(PIN_LED2, LOW); delay(30); diff --git a/noisemeter-device/timestamp.h b/noisemeter-device/timestamp.h new file mode 100644 index 0000000..fa5f835 --- /dev/null +++ b/noisemeter-device/timestamp.h @@ -0,0 +1,46 @@ +#ifndef TIMESTAMP_H +#define TIMESTAMP_H + +#include +#include + +class Timestamp +{ +public: + Timestamp(std::time_t tm_ = std::time(nullptr)): + tm(tm_) {} + + bool valid() const noexcept { + return tm >= 8 * 3600 * 2; + } + + operator String() const noexcept { + char tsbuf[32]; + const auto timeinfo = std::gmtime(&tm); + const auto success = std::strftime(tsbuf, sizeof(tsbuf), "%c", timeinfo) > 0; + + return success ? tsbuf : "(error)"; + } + + auto secondsBetween(Timestamp ts) const noexcept { + return std::difftime(ts.tm, tm); + } + + static void synchronize() { + configTime(0, 0, "pool.ntp.org"); + + do { + delay(1000); + } while (!Timestamp().valid()); + } + + static Timestamp invalidTimestamp() { + return Timestamp(0); + } + +private: + std::time_t tm; +}; + +#endif // TIMESTAMP_H + diff --git a/platformio.ini b/platformio.ini index 6571716..3a6962b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,6 +26,7 @@ build_flags = -std=gnu++17 -DBUILD_PLATFORMIO -DNO_GLOBAL_EEPROM + -Wall -Wextra [env:esp32-pcb] board = esp32-c3-devkitm-1