From b3b59019a61045213c3b17e3a6de36a62a736316 Mon Sep 17 00:00:00 2001 From: Christian Johannesen Date: Mon, 26 Oct 2026 11:04:19 +0100 Subject: [PATCH] Improve buffered temperature payload uploads --- .../ds18b20_payload/README.md | 46 +- .../ds18b20_payload/ds18b20_payload.ino | 551 ++++++++++++++---- 2 files changed, 488 insertions(+), 109 deletions(-) diff --git a/sketches/arduino-uno-r4-wifi/ds18b20_payload/README.md b/sketches/arduino-uno-r4-wifi/ds18b20_payload/README.md index dee70c1..f25cda0 100644 --- a/sketches/arduino-uno-r4-wifi/ds18b20_payload/README.md +++ b/sketches/arduino-uno-r4-wifi/ds18b20_payload/README.md @@ -1,11 +1,25 @@ # DS18B20 til payload-server -Sketchen måler temperaturen med et Keyestudio KS0023 DS18B20-modul og sender -en JSON-payload til `https://payloads.p20.dev/api/payloads` hvert 10. sekund. -Den indbyggede 12×8 LED-matrix viser samtidig: +Sketchen måler temperaturen med et Keyestudio KS0023 DS18B20-modul hvert 10. +sekund. Målingerne gemmes i en fast ringbuffer og sendes samlet til +`https://payloads.p20.dev/api/payloads` hvert 5. minut. -- et flueben, når Wi-Fi er forbundet, eller et kryds uden forbindelse; -- en stribe på 10 LED'er, som tæller ned med én LED pr. sekund. +Hver måling får sit eget UTC-tidsstempel. Boardet synkroniserer tiden over +Wi-Fi og opdaterer tidsreferencen hver 6. time. Bufferen har plads til 1 times +målinger. Efter et netværksudfald sendes de ældste batches først; kun et +vellykket HTTP 2xx-svar fjerner en batch fra bufferen. + +Ved uploadfejl prøver sketchen igen efter et minut. Wi-Fi-forbindelsen nulstilles +efter hver tredje sammenhængende fejl, og boardet genstarter efter den tolvte. +En batch bruger den ældste målings tidsstempel som stabilt `batchId`, så samme +batch kan genkendes, hvis et serversvar går tabt og uploaden gentages. + +Et HD44780-kompatibelt LCD1602 med PCF8574 I²C-backpack viser temperatur, +Wi-Fi (`W+`/`W-`), UTC-tid (`U+`/`U-`), heartbeat og antal målinger i bufferen. +Under upload viser det batchstatus og HTTP-status. Sensorfejl, uploadfejl og +automatisk genstart vises som tydelige tekstbeskeder. Sketchen scanner begge +almindelige PCF8574-adresseområder og opdager også et display, der tilsluttes +efter opstart. ## Tilslutning @@ -17,6 +31,16 @@ Den indbyggede 12×8 LED-matrix viser samtidig: KS0023-modulet har pull-up-modstanden til 1-Wire-datalinjen monteret på boardet. +| LCD1602 I²C | Arduino UNO R4 WiFi | +| --- | --- | +| `GND` | `GND` | +| `VCC` | `5V` | +| `SDA` | `SDA` (eller A4) | +| `SCL` | `SCL` (eller A5) | + +Hvis baggrundsbelysningen tænder, men teksten ikke kan ses, justeres det blå +kontrast-potentiometer på I²C-backpacket forsigtigt. + ## Secrets Kopiér `arduino_secrets.example.h` til `arduino_secrets.h`. Brug samme @@ -28,7 +52,7 @@ Den lokale secret-fil er allerede ignoreret af Git. ## Biblioteker og kompilering Installér `OneWire` og `DallasTemperature` i Arduino IDE's Library Manager. -`WiFiS3` og `Arduino_LED_Matrix` følger med UNO R4-boardpakken. +`WiFiS3` og `Wire` følger med UNO R4-boardpakken. ```powershell arduino-cli lib install OneWire DallasTemperature @@ -36,14 +60,20 @@ arduino-cli compile --fqbn arduino:renesas_uno:unor4wifi sketches/arduino-uno-r4 arduino-cli upload -p COM5 --fqbn arduino:renesas_uno:unor4wifi sketches/arduino-uno-r4-wifi/ds18b20_payload ``` -API-payloaden har formen: +API-payloaden indeholder op til 30 tidsstemplede målinger: ```json { "topic": "sensors/temperature", "payload": { "device": "uno-r4-wifi-ds18b20", - "temperatureC": 22.50 + "batchId": 1, + "samples": [ + { + "measuredAtUnix": 1793025000, + "temperatureC": 22.50 + } + ] } } ``` diff --git a/sketches/arduino-uno-r4-wifi/ds18b20_payload/ds18b20_payload.ino b/sketches/arduino-uno-r4-wifi/ds18b20_payload/ds18b20_payload.ino index 21c7e2f..ef507d4 100644 --- a/sketches/arduino-uno-r4-wifi/ds18b20_payload/ds18b20_payload.ino +++ b/sketches/arduino-uno-r4-wifi/ds18b20_payload/ds18b20_payload.ino @@ -1,64 +1,217 @@ -#include #include #include #include +#include #include "arduino_secrets.h" -constexpr uint8_t ROWS = 8; -constexpr uint8_t COLUMNS = 12; constexpr uint8_t ONE_WIRE_PIN = 2; -constexpr unsigned long PAYLOAD_INTERVAL_MS = 10000; -constexpr unsigned long WIFI_RETRY_MS = 3000; +constexpr uint8_t LCD_COLUMNS = 16; +constexpr uint8_t LCD_ROWS = 2; +constexpr uint8_t LCD_PIN_RS = 0x01; +constexpr uint8_t LCD_PIN_ENABLE = 0x04; +constexpr uint8_t LCD_PIN_BACKLIGHT = 0x08; +constexpr unsigned long MEASUREMENT_INTERVAL_MS = 10000; +constexpr unsigned long UPLOAD_INTERVAL_MS = 5UL * 60UL * 1000UL; +constexpr unsigned long UPLOAD_RETRY_MS = 60000; +constexpr unsigned long BACKLOG_UPLOAD_INTERVAL_MS = 10000; +constexpr unsigned long WIFI_RETRY_MS = 15000; +constexpr unsigned long TIME_RETRY_MS = 60000; +constexpr unsigned long TIME_SYNC_INTERVAL_MS = 6UL * 60UL * 60UL * 1000UL; constexpr unsigned long RESPONSE_TIMEOUT_MS = 5000; +constexpr uint16_t BUFFER_CAPACITY = 60U * 60U / 10U; +constexpr uint8_t BATCH_SIZE = 30; +constexpr size_t REQUEST_BODY_CAPACITY = 3072; +constexpr uint8_t WIFI_RECOVERY_AFTER_FAILURES = 3; +constexpr uint8_t BOARD_RESTART_AFTER_FAILURES = 12; constexpr char API_HOST[] = "payloads.p20.dev"; constexpr uint16_t API_PORT = 443; constexpr char API_PATH[] = "/api/payloads"; +constexpr char DEVICE_ID[] = "uno-r4-wifi-ds18b20"; -ArduinoLEDMatrix matrix; OneWire oneWire(ONE_WIRE_PIN); DallasTemperature temperatureSensors(&oneWire); -uint8_t frame[ROWS][COLUMNS] = {}; +WiFiUDP timeUdp; +uint8_t lcdAddress = 0; +uint8_t lcdBacklightState = LCD_PIN_BACKLIGHT; -unsigned long lastPayloadAt = 0; +// Separate fixed-size arrays avoid heap fragmentation and use 3,600 bytes. +uint32_t measuredAtUnix[BUFFER_CAPACITY] = {}; +uint32_t measuredAtMillis[BUFFER_CAPACITY] = {}; +int16_t temperatureCentiDegrees[BUFFER_CAPACITY] = {}; +char requestBody[REQUEST_BODY_CAPACITY] = {}; + +uint16_t bufferHead = 0; +uint16_t bufferCount = 0; +uint32_t droppedMeasurements = 0; +uint8_t consecutiveUploadFailures = 0; + +unsigned long lastMeasurementAt = 0; +unsigned long lastUploadAttemptAt = 0; unsigned long lastWiFiAttemptAt = 0; -int lastDisplayedSeconds = -1; -bool lastDisplayedConnected = false; +unsigned long lastTimeAttemptAt = 0; +unsigned long clockSyncedAtMillis = 0; +uint32_t clockSyncedAtUnix = 0; -void clearFrame() { - memset(frame, 0, sizeof(frame)); +enum class DisplayEvent : uint8_t { + NORMAL, UPLOADING, UPLOAD_SUCCESS, UPLOAD_ERROR, SENSOR_ERROR, RESTARTING +}; + +DisplayEvent displayEvent = DisplayEvent::NORMAL; +unsigned long displayEventUntil = 0; +unsigned long lastDisplayRefreshAt = 0; +unsigned long lastLcdScanAt = 0; +int16_t latestTemperatureCentiDegrees = 0; +bool hasTemperature = false; +int lastHttpStatus = 0; + +void lcdExpanderWrite(uint8_t value) { + Wire.beginTransmission(lcdAddress); + Wire.write(value | lcdBacklightState); + Wire.endTransmission(); } -void setPixel(int x, int y, bool on = true) { - if (x >= 0 && x < COLUMNS && y >= 0 && y < ROWS) { - frame[y][x] = on ? 1 : 0; - } +void lcdPulseEnable(uint8_t value) { + lcdExpanderWrite(value | LCD_PIN_ENABLE); + delayMicroseconds(1); + lcdExpanderWrite(value & ~LCD_PIN_ENABLE); + delayMicroseconds(50); } -void showCountdown(uint8_t secondsRemaining, bool connected) { - clearFrame(); +void lcdWriteNibble(uint8_t nibble, bool dataMode) { + uint8_t value = (nibble & 0x0F) << 4; + if (dataMode) value |= LCD_PIN_RS; + lcdExpanderWrite(value); + lcdPulseEnable(value); +} - // Wi-Fi status: a check mark when connected, an X otherwise. - if (connected) { - setPixel(0, 2); - setPixel(1, 3); - setPixel(2, 2); - setPixel(3, 1); - } else { - setPixel(0, 0); - setPixel(1, 1); - setPixel(2, 2); - setPixel(2, 0); - setPixel(0, 2); +void lcdSend(uint8_t value, bool dataMode) { + lcdWriteNibble(value >> 4, dataMode); + lcdWriteNibble(value, dataMode); +} + +void lcdCommand(uint8_t command) { + lcdSend(command, false); + if (command == 0x01 || command == 0x02) delayMicroseconds(2000); +} + +void lcdWrite(uint8_t value) { + lcdSend(value, true); +} + +void lcdSetCursor(uint8_t column, uint8_t row) { + static const uint8_t rowOffset[] = {0x00, 0x40}; + lcdCommand(0x80 | (column + rowOffset[row % LCD_ROWS])); +} + +void lcdPrintLine(uint8_t row, const char *text) { + lcdSetCursor(0, row); + uint8_t written = 0; + while (*text && written < LCD_COLUMNS) { + lcdWrite(*text++); + ++written; + } + while (written++ < LCD_COLUMNS) lcdWrite(' '); +} + +uint8_t findLcdDisplay() { + const uint8_t ranges[][2] = {{0x20, 0x27}, {0x38, 0x3F}}; + for (const auto &range : ranges) { + for (uint8_t address = range[0]; address <= range[1]; ++address) { + Wire.beginTransmission(address); + if (Wire.endTransmission() == 0) return address; + } + } + return 0; +} + +void lcdInit() { + delay(50); + lcdWriteNibble(0x03, false); delayMicroseconds(4500); + lcdWriteNibble(0x03, false); delayMicroseconds(4500); + lcdWriteNibble(0x03, false); delayMicroseconds(150); + lcdWriteNibble(0x02, false); + lcdCommand(0x28); + lcdCommand(0x08); + lcdCommand(0x01); + lcdCommand(0x06); + lcdCommand(0x0C); +} + +void formatTemperature(char *output, size_t outputSize) { + if (!hasTemperature) { + snprintf(output, outputSize, "--.--"); + return; + } + const uint16_t absolute = static_cast(abs(latestTemperatureCentiDegrees)); + snprintf(output, outputSize, "%s%u.%02u", latestTemperatureCentiDegrees < 0 ? "-" : "", + absolute / 100, absolute % 100); +} + +void renderLcdStatus() { + char line1[LCD_COLUMNS + 1]; + char line2[LCD_COLUMNS + 1]; + char temperature[8]; + formatTemperature(temperature, sizeof(temperature)); + + switch (displayEvent) { + case DisplayEvent::NORMAL: + snprintf(line1, sizeof(line1), "T:%s W%c U%c", temperature, + WiFi.status() == WL_CONNECTED ? '+' : '-', clockSyncedAtUnix != 0 ? '+' : '-'); + snprintf(line2, sizeof(line2), "Buf:%03u/%03u %c", bufferCount, BUFFER_CAPACITY, + (millis() / 500UL) % 2 == 0 ? '*' : ' '); + break; + case DisplayEvent::UPLOADING: + snprintf(line1, sizeof(line1), "Uploader batch"); + snprintf(line2, sizeof(line2), "30 stk Buf:%03u", bufferCount); + break; + case DisplayEvent::UPLOAD_SUCCESS: + snprintf(line1, sizeof(line1), "Upload OK HTTP%d", lastHttpStatus); + snprintf(line2, sizeof(line2), "Buffer:%03u", bufferCount); + break; + case DisplayEvent::UPLOAD_ERROR: + snprintf(line1, sizeof(line1), "UPLOAD FEJL %d", lastHttpStatus); + snprintf(line2, sizeof(line2), "Forsoeg:%u/12", consecutiveUploadFailures); + break; + case DisplayEvent::SENSOR_ERROR: + snprintf(line1, sizeof(line1), "SENSORFEJL"); + snprintf(line2, sizeof(line2), "Kontroller DS18B20"); + break; + case DisplayEvent::RESTARTING: + snprintf(line1, sizeof(line1), "GENSTARTER..."); + snprintf(line2, sizeof(line2), "Uploadfejl:%u", consecutiveUploadFailures); + break; + } + lcdPrintLine(0, line1); + lcdPrintLine(1, line2); +} + +void updateDisplay(bool force = false) { + if (!force && millis() - lastDisplayRefreshAt < 500) return; + lastDisplayRefreshAt = millis(); + + if (lcdAddress == 0) { + if (millis() - lastLcdScanAt < 5000) return; + lastLcdScanAt = millis(); + lcdAddress = findLcdDisplay(); + if (lcdAddress == 0) return; + Serial.print(F("LCD1602 fundet paa I2C-adresse 0x")); + Serial.println(lcdAddress, HEX); + lcdInit(); } - // Ten LEDs count down from 10 to 1 before the next payload. - const uint8_t lit = min(secondsRemaining, static_cast(10)); - for (uint8_t column = 0; column < lit; ++column) { - setPixel(column + 1, 6); + if (displayEvent != DisplayEvent::NORMAL && displayEvent != DisplayEvent::UPLOADING && + static_cast(millis() - displayEventUntil) >= 0) { + displayEvent = DisplayEvent::NORMAL; } - matrix.renderBitmap(frame, ROWS, COLUMNS); + renderLcdStatus(); +} + +void setDisplayEvent(DisplayEvent event, unsigned long durationMs = 0) { + displayEvent = event; + displayEventUntil = durationMs == 0 ? 0 : millis() + durationMs; + updateDisplay(true); } void connectWiFi() { @@ -66,110 +219,306 @@ void connectWiFi() { if (millis() - lastWiFiAttemptAt < WIFI_RETRY_MS) return; lastWiFiAttemptAt = millis(); + WiFi.disconnect(); Serial.print(F("Forbinder til Wi-Fi: ")); Serial.println(SECRET_SSID); WiFi.begin(SECRET_SSID, SECRET_PASS); } -bool postTemperature(float celsius) { +uint32_t currentUnixTime() { + if (clockSyncedAtUnix == 0) return 0; + return clockSyncedAtUnix + (millis() - clockSyncedAtMillis) / 1000UL; +} + +uint32_t requestNtpTime() { + constexpr uint16_t NTP_LOCAL_PORT = 2390; + constexpr uint16_t NTP_SERVER_PORT = 123; + constexpr uint8_t NTP_PACKET_SIZE = 48; + constexpr uint32_t NTP_TO_UNIX_OFFSET = 2208988800UL; + const IPAddress timeServer(162, 159, 200, 123); + uint8_t packet[NTP_PACKET_SIZE] = {}; + + // Values used by the WiFiS3 board package's official NTP example. + packet[0] = 0b11100011; + packet[1] = 0; + packet[2] = 6; + packet[3] = 0xEC; + packet[12] = 49; + packet[13] = 0x4E; + packet[14] = 49; + packet[15] = 52; + + timeUdp.stop(); + if (!timeUdp.begin(NTP_LOCAL_PORT)) return 0; + if (!timeUdp.beginPacket(timeServer, NTP_SERVER_PORT)) return 0; + timeUdp.write(packet, sizeof(packet)); + if (!timeUdp.endPacket()) return 0; + + const unsigned long startedAt = millis(); + while (millis() - startedAt < 2000) { + const int packetSize = timeUdp.parsePacket(); + if (packetSize >= NTP_PACKET_SIZE) { + timeUdp.read(packet, sizeof(packet)); + const uint32_t secondsSince1900 = + (static_cast(packet[40]) << 24) | + (static_cast(packet[41]) << 16) | + (static_cast(packet[42]) << 8) | + static_cast(packet[43]); + timeUdp.stop(); + if (secondsSince1900 <= NTP_TO_UNIX_OFFSET) return 0; + return secondsSince1900 - NTP_TO_UNIX_OFFSET; + } + updateDisplay(); + delay(10); + } + + timeUdp.stop(); + return 0; +} + +void backfillUnsyncedTimestamps() { + for (uint16_t offset = 0; offset < bufferCount; ++offset) { + const uint16_t index = (bufferHead + offset) % BUFFER_CAPACITY; + if (measuredAtUnix[index] == 0) { + measuredAtUnix[index] = clockSyncedAtUnix - + (clockSyncedAtMillis - measuredAtMillis[index]) / 1000UL; + } + } +} + +void syncClock() { + if (WiFi.status() != WL_CONNECTED) return; + const bool clockIsDue = clockSyncedAtUnix == 0 || + millis() - clockSyncedAtMillis >= TIME_SYNC_INTERVAL_MS; + if (!clockIsDue || millis() - lastTimeAttemptAt < TIME_RETRY_MS) return; + + lastTimeAttemptAt = millis(); + const unsigned long unixTime = requestNtpTime(); + if (unixTime < 1700000000UL) { + Serial.println(F("Kunne ikke hente gyldig UTC-tid; proever igen senere.")); + return; + } + + clockSyncedAtUnix = unixTime; + clockSyncedAtMillis = millis(); + backfillUnsyncedTimestamps(); + Serial.print(F("UTC-tid synkroniseret: ")); + Serial.println(clockSyncedAtUnix); +} + +void storeMeasurement(float celsius) { + if (bufferCount == BUFFER_CAPACITY) { + bufferHead = (bufferHead + 1) % BUFFER_CAPACITY; + --bufferCount; + ++droppedMeasurements; + Serial.println(F("Maelebufferen er fuld; aeldste maaling blev overskrevet.")); + } + + const uint16_t index = (bufferHead + bufferCount) % BUFFER_CAPACITY; + measuredAtUnix[index] = currentUnixTime(); + measuredAtMillis[index] = millis(); + temperatureCentiDegrees[index] = static_cast(roundf(celsius * 100.0f)); + ++bufferCount; +} + +void takeMeasurement() { + temperatureSensors.requestTemperatures(); + const float celsius = temperatureSensors.getTempCByIndex(0); + if (celsius == DEVICE_DISCONNECTED_C) { + Serial.println(F("DS18B20 blev ikke fundet paa 1-Wire-bussen.")); + setDisplayEvent(DisplayEvent::SENSOR_ERROR, 2000); + return; + } + + storeMeasurement(celsius); + latestTemperatureCentiDegrees = static_cast(roundf(celsius * 100.0f)); + hasTemperature = true; + Serial.print(F("Temperatur gemt: ")); + Serial.print(celsius, 2); + Serial.print(F(" C; maalinger i buffer: ")); + Serial.println(bufferCount); +} + +bool appendText(size_t &length, const char *text) { + const size_t textLength = strlen(text); + if (length + textLength >= REQUEST_BODY_CAPACITY) return false; + memcpy(requestBody + length, text, textLength); + length += textLength; + requestBody[length] = '\0'; + return true; +} + +bool buildBatchBody(uint8_t sampleCount, uint32_t batchId, size_t &bodyLength) { + bodyLength = 0; + char fragment[128]; + snprintf(fragment, sizeof(fragment), + "{\"topic\":\"sensors/temperature\",\"payload\":{\"device\":\"%s\"," + "\"batchId\":%lu,\"samples\":[", + DEVICE_ID, static_cast(batchId)); + if (!appendText(bodyLength, fragment)) return false; + + for (uint8_t offset = 0; offset < sampleCount; ++offset) { + const uint16_t index = (bufferHead + offset) % BUFFER_CAPACITY; + const int16_t centiDegrees = temperatureCentiDegrees[index]; + const uint16_t absoluteTemperature = static_cast(abs(centiDegrees)); + snprintf(fragment, sizeof(fragment), + "%s{\"measuredAtUnix\":%lu,\"temperatureC\":%s%u.%02u}", + offset == 0 ? "" : ",", static_cast(measuredAtUnix[index]), + centiDegrees < 0 ? "-" : "", absoluteTemperature / 100, + absoluteTemperature % 100); + if (!appendText(bodyLength, fragment)) return false; + } + + return appendText(bodyLength, "]}}\n"); +} + +int readHttpStatus(WiFiSSLClient &client) { + unsigned long lastActivityAt = millis(); + while (!client.available() && client.connected() && + millis() - lastActivityAt < RESPONSE_TIMEOUT_MS) { + updateDisplay(); + delay(10); + } + if (!client.available()) return 0; + + char statusLine[64] = {}; + const size_t statusLength = client.readBytesUntil('\n', statusLine, sizeof(statusLine) - 1); + statusLine[statusLength] = '\0'; + int statusCode = 0; + sscanf(statusLine, "HTTP/%*s %d", &statusCode); + + // Drain headers and response body before releasing the socket. + lastActivityAt = millis(); + while ((client.connected() || client.available()) && + millis() - lastActivityAt < RESPONSE_TIMEOUT_MS) { + while (client.available()) { + client.read(); + lastActivityAt = millis(); + } + updateDisplay(); + delay(1); + } + return statusCode; +} + +bool postOldestBatch(uint8_t sampleCount) { + if (clockSyncedAtUnix == 0) { + Serial.println(F("Upload udskudt: UTC-tiden er endnu ikke synkroniseret.")); + return false; + } + + size_t bodyLength = 0; + // The oldest sample's timestamp remains stable across retries and board restarts. + const uint32_t batchId = measuredAtUnix[bufferHead]; + if (!buildBatchBody(sampleCount, batchId, bodyLength)) { + Serial.println(F("Batchen kunne ikke vaere i request-bufferen.")); + return false; + } + WiFiSSLClient client; if (!client.connect(API_HOST, API_PORT)) { Serial.println(F("Kunne ikke oprette HTTPS-forbindelse til payload-serveren.")); return false; } - String body = F("{\"topic\":\"sensors/temperature\",\"payload\":{"); - body += F("\"device\":\"uno-r4-wifi-ds18b20\",\"temperatureC\":"); - body += String(celsius, 2); - body += F("}}"); - - client.print(F("POST ")); - client.print(API_PATH); - client.println(F(" HTTP/1.1")); - client.print(F("Host: ")); - client.println(API_HOST); - client.print(F("Authorization: Bearer ")); - client.println(SECRET_API_TOKEN); + client.print(F("POST ")); client.print(API_PATH); client.println(F(" HTTP/1.1")); + client.print(F("Host: ")); client.println(API_HOST); + client.print(F("Authorization: Bearer ")); client.println(SECRET_API_TOKEN); client.println(F("Content-Type: application/json")); - client.println(F("X-Device-ID: uno-r4-wifi-ds18b20")); + client.print(F("X-Device-ID: ")); client.println(DEVICE_ID); client.println(F("X-Client-Name: ds18b20-payload-sketch")); - client.print(F("Content-Length: ")); - client.println(body.length()); + client.print(F("Content-Length: ")); client.println(bodyLength); client.println(F("Connection: close")); client.println(); - client.print(body); + client.write(reinterpret_cast(requestBody), bodyLength); - const unsigned long startedAt = millis(); - while (!client.available() && client.connected() && - millis() - startedAt < RESPONSE_TIMEOUT_MS) { - delay(10); - } - - if (!client.available()) { - Serial.println(F("Payload-serveren svarede ikke inden timeout.")); - client.stop(); - return false; - } - - const String statusLine = client.readStringUntil('\n'); - const bool accepted = statusLine.indexOf(" 201 ") >= 0; - Serial.print(F("Serverens svar: ")); - Serial.println(statusLine); + const int statusCode = readHttpStatus(client); + lastHttpStatus = statusCode; client.stop(); - return accepted; + Serial.print(F("Serverens HTTP-status: ")); + Serial.println(statusCode); + if (statusCode < 200 || statusCode >= 300) return false; + + bufferHead = (bufferHead + sampleCount) % BUFFER_CAPACITY; + bufferCount -= sampleCount; + consecutiveUploadFailures = 0; + Serial.print(F("Batch sendt; maalinger tilbage i buffer: ")); + Serial.println(bufferCount); + return true; } -void sendPayload() { - temperatureSensors.requestTemperatures(); - const float celsius = temperatureSensors.getTempCByIndex(0); +void uploadWhenDue() { + if (bufferCount < BATCH_SIZE || WiFi.status() != WL_CONNECTED || + clockSyncedAtUnix == 0) return; + const unsigned long regularInterval = consecutiveUploadFailures == 0 + ? UPLOAD_INTERVAL_MS + : UPLOAD_RETRY_MS; + const bool regularBatchDue = bufferCount >= BATCH_SIZE && + millis() - lastUploadAttemptAt >= regularInterval; + const bool backlogDue = consecutiveUploadFailures == 0 && + bufferCount >= BATCH_SIZE * 2U && + millis() - lastUploadAttemptAt >= BACKLOG_UPLOAD_INTERVAL_MS; + if (!regularBatchDue && !backlogDue) return; - if (celsius == DEVICE_DISCONNECTED_C) { - Serial.println(F("DS18B20 blev ikke fundet paa 1-Wire-bussen.")); + lastUploadAttemptAt = millis(); + const uint8_t sampleCount = min(static_cast(BATCH_SIZE), bufferCount); + setDisplayEvent(DisplayEvent::UPLOADING); + if (postOldestBatch(sampleCount)) { + setDisplayEvent(DisplayEvent::UPLOAD_SUCCESS, 1000); return; } + setDisplayEvent(DisplayEvent::UPLOAD_ERROR, 2000); - Serial.print(F("Temperatur: ")); - Serial.print(celsius, 2); - Serial.println(F(" C")); - Serial.println(postTemperature(celsius) ? F("Payload sendt.") : F("Payload fejlede.")); + ++consecutiveUploadFailures; + Serial.print(F("Sammenhaengende uploadfejl: ")); + Serial.println(consecutiveUploadFailures); + + if (consecutiveUploadFailures % WIFI_RECOVERY_AFTER_FAILURES == 0) { + Serial.println(F("Nulstiller Wi-Fi-forbindelsen efter uploadfejl.")); + WiFi.disconnect(); + lastWiFiAttemptAt = millis() - WIFI_RETRY_MS; + } + + if (consecutiveUploadFailures >= BOARD_RESTART_AFTER_FAILURES) { + Serial.println(F("Genstarter boardet efter vedvarende uploadfejl.")); + Serial.flush(); + setDisplayEvent(DisplayEvent::RESTARTING); + const unsigned long restartAnimationStartedAt = millis(); + while (millis() - restartAnimationStartedAt < 800) { + updateDisplay(); + delay(10); + } + NVIC_SystemReset(); + } } void setup() { Serial.begin(115200); - matrix.begin(); + Wire.begin(); + Wire.setClock(100000); temperatureSensors.begin(); temperatureSensors.setResolution(10); - - showCountdown(10, false); + lastLcdScanAt = millis() - 5000; + setDisplayEvent(DisplayEvent::NORMAL); lastWiFiAttemptAt = millis() - WIFI_RETRY_MS; + lastTimeAttemptAt = millis() - TIME_RETRY_MS; + lastMeasurementAt = millis(); + lastUploadAttemptAt = millis(); connectWiFi(); - lastPayloadAt = millis(); } void loop() { connectWiFi(); + syncClock(); - const unsigned long elapsed = millis() - lastPayloadAt; - const uint8_t secondsRemaining = elapsed >= PAYLOAD_INTERVAL_MS - ? 0 - : static_cast((PAYLOAD_INTERVAL_MS - elapsed + 999) / 1000); + const unsigned long elapsed = millis() - lastMeasurementAt; - const bool connected = WiFi.status() == WL_CONNECTED; - if (secondsRemaining != lastDisplayedSeconds || connected != lastDisplayedConnected) { - lastDisplayedSeconds = secondsRemaining; - lastDisplayedConnected = connected; - showCountdown(secondsRemaining, connected); - } - - if (elapsed >= PAYLOAD_INTERVAL_MS) { - if (WiFi.status() == WL_CONNECTED) { - sendPayload(); - } else { - Serial.println(F("Payload sprunget over: Wi-Fi er ikke forbundet.")); - } - lastPayloadAt = millis(); - lastDisplayedSeconds = -1; + if (elapsed >= MEASUREMENT_INTERVAL_MS) { + takeMeasurement(); + lastMeasurementAt += MEASUREMENT_INTERVAL_MS; } + uploadWhenDue(); + updateDisplay(); delay(20); }