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
|
# DS18B20 til payload-server
|
||||||
|
|
||||||
Sketchen måler temperaturen med et Keyestudio KS0023 DS18B20-modul og sender
|
Sketchen måler temperaturen med et Keyestudio KS0023 DS18B20-modul hvert 10.
|
||||||
en JSON-payload til `https://payloads.p20.dev/api/payloads` hvert 10. sekund.
|
sekund. Målingerne gemmes i en fast ringbuffer og sendes samlet til
|
||||||
Den indbyggede 12×8 LED-matrix viser samtidig:
|
`https://payloads.p20.dev/api/payloads` hvert 5. minut.
|
||||||
|
|
||||||
- et flueben, når Wi-Fi er forbundet, eller et kryds uden forbindelse;
|
Hver måling får sit eget UTC-tidsstempel. Boardet synkroniserer tiden over
|
||||||
- en stribe på 10 LED'er, som tæller ned med én LED pr. sekund.
|
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
|
## 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.
|
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
|
## Secrets
|
||||||
|
|
||||||
Kopiér `arduino_secrets.example.h` til `arduino_secrets.h`. Brug samme
|
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
|
## Biblioteker og kompilering
|
||||||
|
|
||||||
Installér `OneWire` og `DallasTemperature` i Arduino IDE's Library Manager.
|
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
|
```powershell
|
||||||
arduino-cli lib install OneWire DallasTemperature
|
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
|
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
|
```json
|
||||||
{
|
{
|
||||||
"topic": "sensors/temperature",
|
"topic": "sensors/temperature",
|
||||||
"payload": {
|
"payload": {
|
||||||
"device": "uno-r4-wifi-ds18b20",
|
"device": "uno-r4-wifi-ds18b20",
|
||||||
|
"batchId": 1,
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"measuredAtUnix": 1793025000,
|
||||||
"temperatureC": 22.50
|
"temperatureC": 22.50
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,217 @@
|
||||||
#include <Arduino_LED_Matrix.h>
|
|
||||||
#include <DallasTemperature.h>
|
#include <DallasTemperature.h>
|
||||||
#include <OneWire.h>
|
#include <OneWire.h>
|
||||||
#include <WiFiS3.h>
|
#include <WiFiS3.h>
|
||||||
|
#include <Wire.h>
|
||||||
|
|
||||||
#include "arduino_secrets.h"
|
#include "arduino_secrets.h"
|
||||||
|
|
||||||
constexpr uint8_t ROWS = 8;
|
|
||||||
constexpr uint8_t COLUMNS = 12;
|
|
||||||
constexpr uint8_t ONE_WIRE_PIN = 2;
|
constexpr uint8_t ONE_WIRE_PIN = 2;
|
||||||
constexpr unsigned long PAYLOAD_INTERVAL_MS = 10000;
|
constexpr uint8_t LCD_COLUMNS = 16;
|
||||||
constexpr unsigned long WIFI_RETRY_MS = 3000;
|
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 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 char API_HOST[] = "payloads.p20.dev";
|
||||||
constexpr uint16_t API_PORT = 443;
|
constexpr uint16_t API_PORT = 443;
|
||||||
constexpr char API_PATH[] = "/api/payloads";
|
constexpr char API_PATH[] = "/api/payloads";
|
||||||
|
constexpr char DEVICE_ID[] = "uno-r4-wifi-ds18b20";
|
||||||
|
|
||||||
ArduinoLEDMatrix matrix;
|
|
||||||
OneWire oneWire(ONE_WIRE_PIN);
|
OneWire oneWire(ONE_WIRE_PIN);
|
||||||
DallasTemperature temperatureSensors(&oneWire);
|
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;
|
unsigned long lastWiFiAttemptAt = 0;
|
||||||
int lastDisplayedSeconds = -1;
|
unsigned long lastTimeAttemptAt = 0;
|
||||||
bool lastDisplayedConnected = false;
|
unsigned long clockSyncedAtMillis = 0;
|
||||||
|
uint32_t clockSyncedAtUnix = 0;
|
||||||
|
|
||||||
void clearFrame() {
|
enum class DisplayEvent : uint8_t {
|
||||||
memset(frame, 0, sizeof(frame));
|
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) {
|
void lcdPulseEnable(uint8_t value) {
|
||||||
if (x >= 0 && x < COLUMNS && y >= 0 && y < ROWS) {
|
lcdExpanderWrite(value | LCD_PIN_ENABLE);
|
||||||
frame[y][x] = on ? 1 : 0;
|
delayMicroseconds(1);
|
||||||
}
|
lcdExpanderWrite(value & ~LCD_PIN_ENABLE);
|
||||||
|
delayMicroseconds(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showCountdown(uint8_t secondsRemaining, bool connected) {
|
void lcdWriteNibble(uint8_t nibble, bool dataMode) {
|
||||||
clearFrame();
|
uint8_t value = (nibble & 0x0F) << 4;
|
||||||
|
if (dataMode) value |= LCD_PIN_RS;
|
||||||
// Wi-Fi status: a check mark when connected, an X otherwise.
|
lcdExpanderWrite(value);
|
||||||
if (connected) {
|
lcdPulseEnable(value);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ten LEDs count down from 10 to 1 before the next payload.
|
void lcdSend(uint8_t value, bool dataMode) {
|
||||||
const uint8_t lit = min(secondsRemaining, static_cast<uint8_t>(10));
|
lcdWriteNibble(value >> 4, dataMode);
|
||||||
for (uint8_t column = 0; column < lit; ++column) {
|
lcdWriteNibble(value, dataMode);
|
||||||
setPixel(column + 1, 6);
|
|
||||||
}
|
}
|
||||||
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() {
|
void connectWiFi() {
|
||||||
|
|
@ -66,110 +219,306 @@ void connectWiFi() {
|
||||||
if (millis() - lastWiFiAttemptAt < WIFI_RETRY_MS) return;
|
if (millis() - lastWiFiAttemptAt < WIFI_RETRY_MS) return;
|
||||||
|
|
||||||
lastWiFiAttemptAt = millis();
|
lastWiFiAttemptAt = millis();
|
||||||
|
WiFi.disconnect();
|
||||||
Serial.print(F("Forbinder til Wi-Fi: "));
|
Serial.print(F("Forbinder til Wi-Fi: "));
|
||||||
Serial.println(SECRET_SSID);
|
Serial.println(SECRET_SSID);
|
||||||
WiFi.begin(SECRET_SSID, SECRET_PASS);
|
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;
|
WiFiSSLClient client;
|
||||||
if (!client.connect(API_HOST, API_PORT)) {
|
if (!client.connect(API_HOST, API_PORT)) {
|
||||||
Serial.println(F("Kunne ikke oprette HTTPS-forbindelse til payload-serveren."));
|
Serial.println(F("Kunne ikke oprette HTTPS-forbindelse til payload-serveren."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String body = F("{\"topic\":\"sensors/temperature\",\"payload\":{");
|
client.print(F("POST ")); client.print(API_PATH); client.println(F(" HTTP/1.1"));
|
||||||
body += F("\"device\":\"uno-r4-wifi-ds18b20\",\"temperatureC\":");
|
client.print(F("Host: ")); client.println(API_HOST);
|
||||||
body += String(celsius, 2);
|
client.print(F("Authorization: Bearer ")); client.println(SECRET_API_TOKEN);
|
||||||
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.println(F("Content-Type: application/json"));
|
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.println(F("X-Client-Name: ds18b20-payload-sketch"));
|
||||||
client.print(F("Content-Length: "));
|
client.print(F("Content-Length: ")); client.println(bodyLength);
|
||||||
client.println(body.length());
|
|
||||||
client.println(F("Connection: close"));
|
client.println(F("Connection: close"));
|
||||||
client.println();
|
client.println();
|
||||||
client.print(body);
|
client.write(reinterpret_cast<const uint8_t *>(requestBody), bodyLength);
|
||||||
|
|
||||||
const unsigned long startedAt = millis();
|
const int statusCode = readHttpStatus(client);
|
||||||
while (!client.available() && client.connected() &&
|
lastHttpStatus = statusCode;
|
||||||
millis() - startedAt < RESPONSE_TIMEOUT_MS) {
|
|
||||||
delay(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!client.available()) {
|
|
||||||
Serial.println(F("Payload-serveren svarede ikke inden timeout."));
|
|
||||||
client.stop();
|
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');
|
void uploadWhenDue() {
|
||||||
const bool accepted = statusLine.indexOf(" 201 ") >= 0;
|
if (bufferCount < BATCH_SIZE || WiFi.status() != WL_CONNECTED ||
|
||||||
Serial.print(F("Serverens svar: "));
|
clockSyncedAtUnix == 0) return;
|
||||||
Serial.println(statusLine);
|
const unsigned long regularInterval = consecutiveUploadFailures == 0
|
||||||
client.stop();
|
? UPLOAD_INTERVAL_MS
|
||||||
return accepted;
|
: 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() {
|
lastUploadAttemptAt = millis();
|
||||||
temperatureSensors.requestTemperatures();
|
const uint8_t sampleCount = min(static_cast<uint16_t>(BATCH_SIZE), bufferCount);
|
||||||
const float celsius = temperatureSensors.getTempCByIndex(0);
|
setDisplayEvent(DisplayEvent::UPLOADING);
|
||||||
|
if (postOldestBatch(sampleCount)) {
|
||||||
if (celsius == DEVICE_DISCONNECTED_C) {
|
setDisplayEvent(DisplayEvent::UPLOAD_SUCCESS, 1000);
|
||||||
Serial.println(F("DS18B20 blev ikke fundet paa 1-Wire-bussen."));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setDisplayEvent(DisplayEvent::UPLOAD_ERROR, 2000);
|
||||||
|
|
||||||
Serial.print(F("Temperatur: "));
|
++consecutiveUploadFailures;
|
||||||
Serial.print(celsius, 2);
|
Serial.print(F("Sammenhaengende uploadfejl: "));
|
||||||
Serial.println(F(" C"));
|
Serial.println(consecutiveUploadFailures);
|
||||||
Serial.println(postTemperature(celsius) ? F("Payload sendt.") : F("Payload fejlede."));
|
|
||||||
|
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() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
matrix.begin();
|
Wire.begin();
|
||||||
|
Wire.setClock(100000);
|
||||||
temperatureSensors.begin();
|
temperatureSensors.begin();
|
||||||
temperatureSensors.setResolution(10);
|
temperatureSensors.setResolution(10);
|
||||||
|
lastLcdScanAt = millis() - 5000;
|
||||||
showCountdown(10, false);
|
setDisplayEvent(DisplayEvent::NORMAL);
|
||||||
lastWiFiAttemptAt = millis() - WIFI_RETRY_MS;
|
lastWiFiAttemptAt = millis() - WIFI_RETRY_MS;
|
||||||
|
lastTimeAttemptAt = millis() - TIME_RETRY_MS;
|
||||||
|
lastMeasurementAt = millis();
|
||||||
|
lastUploadAttemptAt = millis();
|
||||||
connectWiFi();
|
connectWiFi();
|
||||||
lastPayloadAt = millis();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
connectWiFi();
|
connectWiFi();
|
||||||
|
syncClock();
|
||||||
|
|
||||||
const unsigned long elapsed = millis() - lastPayloadAt;
|
const unsigned long elapsed = millis() - lastMeasurementAt;
|
||||||
const uint8_t secondsRemaining = elapsed >= PAYLOAD_INTERVAL_MS
|
|
||||||
? 0
|
|
||||||
: static_cast<uint8_t>((PAYLOAD_INTERVAL_MS - elapsed + 999) / 1000);
|
|
||||||
|
|
||||||
const bool connected = WiFi.status() == WL_CONNECTED;
|
if (elapsed >= MEASUREMENT_INTERVAL_MS) {
|
||||||
if (secondsRemaining != lastDisplayedSeconds || connected != lastDisplayedConnected) {
|
takeMeasurement();
|
||||||
lastDisplayedSeconds = secondsRemaining;
|
lastMeasurementAt += MEASUREMENT_INTERVAL_MS;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadWhenDue();
|
||||||
|
updateDisplay();
|
||||||
delay(20);
|
delay(20);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue