Improve buffered temperature payload uploads
This commit is contained in:
parent
2bd1c11c45
commit
b3b59019a6
2 changed files with 488 additions and 109 deletions
|
|
@ -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",
|
||||
"batchId": 1,
|
||||
"samples": [
|
||||
{
|
||||
"measuredAtUnix": 1793025000,
|
||||
"temperatureC": 22.50
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,64 +1,217 @@
|
|||
#include <Arduino_LED_Matrix.h>
|
||||
#include <DallasTemperature.h>
|
||||
#include <OneWire.h>
|
||||
#include <WiFiS3.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#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();
|
||||
|
||||
// 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 lcdWriteNibble(uint8_t nibble, bool dataMode) {
|
||||
uint8_t value = (nibble & 0x0F) << 4;
|
||||
if (dataMode) value |= LCD_PIN_RS;
|
||||
lcdExpanderWrite(value);
|
||||
lcdPulseEnable(value);
|
||||
}
|
||||
|
||||
// Ten LEDs count down from 10 to 1 before the next payload.
|
||||
const uint8_t lit = min(secondsRemaining, static_cast<uint8_t>(10));
|
||||
for (uint8_t column = 0; column < lit; ++column) {
|
||||
setPixel(column + 1, 6);
|
||||
void lcdSend(uint8_t value, bool dataMode) {
|
||||
lcdWriteNibble(value >> 4, dataMode);
|
||||
lcdWriteNibble(value, dataMode);
|
||||
}
|
||||
matrix.renderBitmap(frame, ROWS, COLUMNS);
|
||||
|
||||
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<uint16_t>(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();
|
||||
}
|
||||
|
||||
if (displayEvent != DisplayEvent::NORMAL && displayEvent != DisplayEvent::UPLOADING &&
|
||||
static_cast<long>(millis() - displayEventUntil) >= 0) {
|
||||
displayEvent = DisplayEvent::NORMAL;
|
||||
}
|
||||
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<uint32_t>(packet[40]) << 24) |
|
||||
(static_cast<uint32_t>(packet[41]) << 16) |
|
||||
(static_cast<uint32_t>(packet[42]) << 8) |
|
||||
static_cast<uint32_t>(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<int16_t>(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<int16_t>(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<unsigned long>(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<uint16_t>(abs(centiDegrees));
|
||||
snprintf(fragment, sizeof(fragment),
|
||||
"%s{\"measuredAtUnix\":%lu,\"temperatureC\":%s%u.%02u}",
|
||||
offset == 0 ? "" : ",", static_cast<unsigned long>(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<const uint8_t *>(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."));
|
||||
const int statusCode = readHttpStatus(client);
|
||||
lastHttpStatus = statusCode;
|
||||
client.stop();
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
|
||||
const String statusLine = client.readStringUntil('\n');
|
||||
const bool accepted = statusLine.indexOf(" 201 ") >= 0;
|
||||
Serial.print(F("Serverens svar: "));
|
||||
Serial.println(statusLine);
|
||||
client.stop();
|
||||
return accepted;
|
||||
}
|
||||
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;
|
||||
|
||||
void sendPayload() {
|
||||
temperatureSensors.requestTemperatures();
|
||||
const float celsius = temperatureSensors.getTempCByIndex(0);
|
||||
|
||||
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<uint16_t>(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<uint8_t>((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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue