From 3960b4db6e33ac748ffc71741705d9c057f5f5ba Mon Sep 17 00:00:00 2001 From: Christian Johannesen Date: Sat, 24 Oct 2026 21:44:02 +0200 Subject: [PATCH] Add DS18B20 payload sketch --- .env.example | 2 +- README.md | 2 + .../keyestudio-ks0023-ds18b20/README.md | 6 +- server/scripts/configure-env.js | 33 +++- .../ds18b20_payload/README.md | 49 +++++ .../ds18b20_payload/arduino_secrets.example.h | 7 + .../ds18b20_payload/ds18b20_payload.ino | 175 ++++++++++++++++++ 7 files changed, 267 insertions(+), 7 deletions(-) create mode 100644 sketches/arduino-uno-r4-wifi/ds18b20_payload/README.md create mode 100644 sketches/arduino-uno-r4-wifi/ds18b20_payload/arduino_secrets.example.h create mode 100644 sketches/arduino-uno-r4-wifi/ds18b20_payload/ds18b20_payload.ino diff --git a/.env.example b/.env.example index 6997a15..99b9214 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ HOST_PORT=8152 -PUBLIC_DOMAIN=http://localhost +PUBLIC_DOMAIN=https://payloads.p20.dev API_BEARER_TOKEN=replace-with-a-long-random-token WIFI_SSID=replace-with-your-ssid WIFI_PASSWORD=replace-with-your-password diff --git a/README.md b/README.md index a26e19c..2210772 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,8 @@ Det samlede komponentindeks findes i [`components/`](components/README.md). – browserstyret løbetekst via Wi-Fi - [`wifi_matrix_status`](sketches/arduino-uno-r4-wifi/wifi_matrix_status) – forbinder til et hotspot og viser forbindelse samt signalstyrke på LED-matrixen +- [`ds18b20_payload`](sketches/arduino-uno-r4-wifi/ds18b20_payload) – måler en + KS0023 DS18B20 og sender temperaturen til payload-serveren hvert 10. sekund - [`lcd1602_i2c_demo`](sketches/arduino-uno-r4-wifi/lcd1602_i2c_demo) – effekt-demo til et eksternt 16×2 HD44780-display med PCF8574 I²C-backpack diff --git a/components/keyestudio-ks0023-ds18b20/README.md b/components/keyestudio-ks0023-ds18b20/README.md index 8d846c1..5f7fbeb 100644 --- a/components/keyestudio-ks0023-ds18b20/README.md +++ b/components/keyestudio-ks0023-ds18b20/README.md @@ -16,7 +16,11 @@ `DAT` forbindes til en digital pin. Modulet indeholder den nødvendige pull-up-komponent; flere DS18B20-enheder kan dele samme 1-Wire-bus. Typiske -biblioteker er `OneWire` og `DallasTemperature`. Ingen sketches endnu. +biblioteker er `OneWire` og `DallasTemperature`. + +## Sketches + +- Arduino UNO R4 WiFi: [`ds18b20_payload`](../../sketches/arduino-uno-r4-wifi/ds18b20_payload) – måler hvert 10. sekund og sender temperaturen til payload-serveren. ## Kilde diff --git a/server/scripts/configure-env.js b/server/scripts/configure-env.js index ad65965..799160c 100644 --- a/server/scripts/configure-env.js +++ b/server/scripts/configure-env.js @@ -12,6 +12,13 @@ const secretsPath = path.join( "wifi_matrix_status", "arduino_secrets.h", ); +const payloadSecretsPath = path.join( + repositoryRoot, + "sketches", + "arduino-uno-r4-wifi", + "ds18b20_payload", + "arduino_secrets.h", +); const secrets = await fs.readFile(secretsPath, "utf8"); const readDefine = (name) => { @@ -20,14 +27,30 @@ const readDefine = (name) => { return JSON.parse(`"${match[1]}"`); }; +const ssid = readDefine("SECRET_SSID"); +const password = readDefine("SECRET_PASS"); +const existingEnv = await fs.readFile(path.join(repositoryRoot, ".env"), "utf8").catch(() => ""); +const existingToken = existingEnv.match(/^API_BEARER_TOKEN=(.+)$/m)?.[1]?.trim(); +const apiToken = existingToken || crypto.randomBytes(32).toString("base64url"); const env = [ "HOST_PORT=8152", - "PUBLIC_DOMAIN=http://localhost", - `API_BEARER_TOKEN=${crypto.randomBytes(32).toString("base64url")}`, - `WIFI_SSID=${JSON.stringify(readDefine("SECRET_SSID"))}`, - `WIFI_PASSWORD=${JSON.stringify(readDefine("SECRET_PASS"))}`, + "PUBLIC_DOMAIN=https://payloads.p20.dev", + `API_BEARER_TOKEN=${apiToken}`, + `WIFI_SSID=${JSON.stringify(ssid)}`, + `WIFI_PASSWORD=${JSON.stringify(password)}`, + "", +].join("\n"); + +const payloadSecrets = [ + "#pragma once", + "", + "// Generated by npm --prefix server run configure. Do not commit.", + `#define SECRET_SSID ${JSON.stringify(ssid)}`, + `#define SECRET_PASS ${JSON.stringify(password)}`, + `#define SECRET_API_TOKEN ${JSON.stringify(apiToken)}`, "", ].join("\n"); await fs.writeFile(path.join(repositoryRoot, ".env"), env, { mode: 0o600 }); -console.log("Created .env with HOST_PORT=8152, PUBLIC_DOMAIN=http://localhost and local secrets."); +await fs.writeFile(payloadSecretsPath, payloadSecrets, { mode: 0o600 }); +console.log("Created .env and the DS18B20 payload sketch's local secret file."); diff --git a/sketches/arduino-uno-r4-wifi/ds18b20_payload/README.md b/sketches/arduino-uno-r4-wifi/ds18b20_payload/README.md new file mode 100644 index 0000000..dee70c1 --- /dev/null +++ b/sketches/arduino-uno-r4-wifi/ds18b20_payload/README.md @@ -0,0 +1,49 @@ +# 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: + +- 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. + +## Tilslutning + +| KS0023 | Arduino UNO R4 WiFi | +| --- | --- | +| `DAT` | D2 | +| `VCC` | 5V | +| `GND` | GND | + +KS0023-modulet har pull-up-modstanden til 1-Wire-datalinjen monteret på boardet. + +## Secrets + +Kopiér `arduino_secrets.example.h` til `arduino_secrets.h`. Brug samme +`SECRET_SSID` og `SECRET_PASS` som i `wifi_matrix_status`, og indsæt værdien af +`API_BEARER_TOKEN` fra repoets lokale `.env` som `SECRET_API_TOKEN`. + +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. + +```powershell +arduino-cli lib install OneWire DallasTemperature +arduino-cli compile --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: + +```json +{ + "topic": "sensors/temperature", + "payload": { + "device": "uno-r4-wifi-ds18b20", + "temperatureC": 22.50 + } +} +``` diff --git a/sketches/arduino-uno-r4-wifi/ds18b20_payload/arduino_secrets.example.h b/sketches/arduino-uno-r4-wifi/ds18b20_payload/arduino_secrets.example.h new file mode 100644 index 0000000..c7f977b --- /dev/null +++ b/sketches/arduino-uno-r4-wifi/ds18b20_payload/arduino_secrets.example.h @@ -0,0 +1,7 @@ +#pragma once + +// Copy this file to arduino_secrets.h and fill in the local values. +// arduino_secrets.h is ignored by Git and must never be committed. +#define SECRET_SSID "your-network-name" +#define SECRET_PASS "your-network-password" +#define SECRET_API_TOKEN "your-payload-api-bearer-token" diff --git a/sketches/arduino-uno-r4-wifi/ds18b20_payload/ds18b20_payload.ino b/sketches/arduino-uno-r4-wifi/ds18b20_payload/ds18b20_payload.ino new file mode 100644 index 0000000..21c7e2f --- /dev/null +++ b/sketches/arduino-uno-r4-wifi/ds18b20_payload/ds18b20_payload.ino @@ -0,0 +1,175 @@ +#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 unsigned long RESPONSE_TIMEOUT_MS = 5000; + +constexpr char API_HOST[] = "payloads.p20.dev"; +constexpr uint16_t API_PORT = 443; +constexpr char API_PATH[] = "/api/payloads"; + +ArduinoLEDMatrix matrix; +OneWire oneWire(ONE_WIRE_PIN); +DallasTemperature temperatureSensors(&oneWire); +uint8_t frame[ROWS][COLUMNS] = {}; + +unsigned long lastPayloadAt = 0; +unsigned long lastWiFiAttemptAt = 0; +int lastDisplayedSeconds = -1; +bool lastDisplayedConnected = false; + +void clearFrame() { + memset(frame, 0, sizeof(frame)); +} + +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 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); + } + + // 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); + } + matrix.renderBitmap(frame, ROWS, COLUMNS); +} + +void connectWiFi() { + if (WiFi.status() == WL_CONNECTED) return; + if (millis() - lastWiFiAttemptAt < WIFI_RETRY_MS) return; + + lastWiFiAttemptAt = millis(); + Serial.print(F("Forbinder til Wi-Fi: ")); + Serial.println(SECRET_SSID); + WiFi.begin(SECRET_SSID, SECRET_PASS); +} + +bool postTemperature(float celsius) { + 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.println(F("Content-Type: application/json")); + client.println(F("X-Device-ID: uno-r4-wifi-ds18b20")); + client.println(F("X-Client-Name: ds18b20-payload-sketch")); + client.print(F("Content-Length: ")); + client.println(body.length()); + client.println(F("Connection: close")); + client.println(); + client.print(body); + + 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); + client.stop(); + return accepted; +} + +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.")); + return; + } + + Serial.print(F("Temperatur: ")); + Serial.print(celsius, 2); + Serial.println(F(" C")); + Serial.println(postTemperature(celsius) ? F("Payload sendt.") : F("Payload fejlede.")); +} + +void setup() { + Serial.begin(115200); + matrix.begin(); + temperatureSensors.begin(); + temperatureSensors.setResolution(10); + + showCountdown(10, false); + lastWiFiAttemptAt = millis() - WIFI_RETRY_MS; + connectWiFi(); + lastPayloadAt = millis(); +} + +void loop() { + connectWiFi(); + + const unsigned long elapsed = millis() - lastPayloadAt; + const uint8_t secondsRemaining = elapsed >= PAYLOAD_INTERVAL_MS + ? 0 + : static_cast((PAYLOAD_INTERVAL_MS - elapsed + 999) / 1000); + + 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; + } + + delay(20); +}