Christian Mueller e706b22f31 Add design specification for LoRaWAN Web Portal
Based on Pflichtenheft v2.2, documents the full system design
including daemon, API, frontend, and infrastructure with agreed
deviations (Axum 0.8, SQLx 0.8, Vite 6, Tailwind 4).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:27:49 +01:00

350 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <HCSR04.h>
// HC-SR04 Ultraschallsensor Pins
#define TRIG_PIN 12
#define ECHO_PIN 13
// Batterie-Messung (LilyGO T3 V1.6.1)
#define BATTERY_PIN 35 // ADC1_CH7 für Batterie-Messung
#define BATTERY_ADC_MULTIPLIER 2.0 // Spannungsteiler 1:1 (2× Faktor)
// Tank-Konfiguration (Füllstandsmessung)
#define TANK_HEIGHT 160.0 // Tank-Höhe in cm (innen)
#define SENSOR_MOUNT_OFFSET 14.0 // Montageabstand: Sensor über Tank-Oberkante in cm
#define SENSOR_MIN_DISTANCE 2.0 // HC-SR04 Mindestabstand (technische Grenze)
// Deep Sleep Konfiguration (Batteriebetrieb)
#define ENABLE_DEEP_SLEEP false // true = Deep Sleep aktiv, false = Deep Sleep deaktiviert (für Tests)
#define SLEEP_INTERVAL 300 // Sleep-Zeit in Sekunden (1800 = 30 Minuten)
#define DISPLAY_ON_TIME 10000 // Display-Anzeigedauer in ms (30000 = 30 Sekunden)
#define uS_TO_S_FACTOR 1000000 // Umrechnung Mikrosekunden zu Sekunden
HCSR04 distanceSensor(TRIG_PIN, ECHO_PIN);
// OLED-Display (I2C: SDA 21, SCL 22 beim LilyGO T3 V1.6.1)
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// LoRaWAN Keys (hier deine echten Werte eintragen)
static const u1_t PROGMEM APPEUI[8] = { 0x01, 0x00, 0x00, 0xD0, 0x7E, 0xD5, 0xB3, 0x70 };
static const u1_t PROGMEM DEVEUI[8] = { 0xB2, 0xA1, 0x05, 0xD0, 0x7E, 0xD5, 0xB3, 0x70 };
static const u1_t PROGMEM APPKEY[16] = { 0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x07, 0x18, 0x29, 0x38, 0x47, 0x56, 0xAB, 0xCD, 0xEF, 0x12 };
void os_getArtEui(u1_t *buf) { memcpy_P(buf, APPEUI, 8); }
void os_getDevEui(u1_t *buf) { memcpy_P(buf, DEVEUI, 8); }
void os_getDevKey(u1_t *buf) { memcpy_P(buf, APPKEY, 16); }
static osjob_t sendjob;
const unsigned TX_INTERVAL = 30; // Nicht verwendet bei Deep Sleep
// RTC Memory für Deep Sleep (bleibt beim Sleep erhalten)
RTC_DATA_ATTR int bootCount = 0;
RTC_DATA_ATTR bool joinedNetwork = false;
// Globale Variablen
unsigned long displayStartTime = 0;
bool dataSent = false;
bool userDataScheduled = false; // Flag: Nutzdaten (nicht nur MAC) wurden gesendet
// LoRa Pinout für LilyGO T3 V1.6.1
const lmic_pinmap lmic_pins = {
.nss = 18,
.rxtx = LMIC_UNUSED_PIN,
.rst = 23,
.dio = {26, 33, 32}
};
// Forward-Deklaration
void do_send(osjob_t* j);
// Deep Sleep aktivieren
void goToSleep() {
#if ENABLE_DEEP_SLEEP
Serial.println(F("=== Deep Sleep aktivieren ==="));
Serial.print(F("Schlafdauer: "));
Serial.print(SLEEP_INTERVAL);
Serial.print(F(" Sekunden ("));
Serial.print(SLEEP_INTERVAL / 60);
Serial.println(F(" Minuten)"));
// Display ausschalten
display.clearDisplay();
display.display();
display.ssd1306_command(0xAE); // Display OFF
// Deep Sleep konfigurieren
esp_sleep_enable_timer_wakeup(SLEEP_INTERVAL * uS_TO_S_FACTOR);
Serial.println(F("Gehe in Deep Sleep..."));
Serial.flush();
// Deep Sleep starten
esp_deep_sleep_start();
#else
Serial.println(F("=== Deep Sleep DEAKTIVIERT (Test-Modus) ==="));
Serial.print(F("Nächste Messung in "));
Serial.print(SLEEP_INTERVAL);
Serial.println(F(" Sekunden..."));
// Variablen zurücksetzen für nächste Messung
dataSent = false;
displayStartTime = 0;
userDataScheduled = false;
// Warte SLEEP_INTERVAL Sekunden, dann sende erneut
os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(SLEEP_INTERVAL), do_send);
#endif
}
void do_send(osjob_t* j) {
// Batteriespannung messen
uint16_t batteryRaw = analogRead(BATTERY_PIN);
float batteryVoltage = (batteryRaw / 4095.0) * 3.3 * BATTERY_ADC_MULTIPLIER;
// Batterie-Prozent berechnen (LiPo 3.0V = 0%, 4.2V = 100%)
float batteryPercent = ((batteryVoltage - 3.0) / (4.2 - 3.0)) * 100.0;
if (batteryPercent < 0) batteryPercent = 0;
if (batteryPercent > 100) batteryPercent = 100;
// Distanz messen (in cm)
float distance = distanceSensor.dist();
// Prüfen ob Messung gültig ist
if (distance < 0) {
Serial.println(F("Fehler beim Messen der Distanz!"));
distance = 0; // Fehlerfall: 0 senden
}
// Füllstand berechnen
// Gesamtabstand bei leerem Tank = Montageabstand + Tank-Höhe
float emptyDistance = SENSOR_MOUNT_OFFSET + TANK_HEIGHT;
// Füllhöhe = Leerer Tank - gemessene Distanz
float fillHeight = emptyDistance - distance;
// Korrektur: Negativ = leer, über Tank-Höhe = voll
if (fillHeight < 0) fillHeight = 0;
if (fillHeight > TANK_HEIGHT) fillHeight = TANK_HEIGHT;
// Füllstand in Prozent
float fillPercent = (fillHeight / TANK_HEIGHT) * 100.0;
// Warnung bei kritischem Abstand (zu nah am Sensor)
bool tooClose = (distance < (SENSOR_MOUNT_OFFSET + SENSOR_MIN_DISTANCE));
// Distanz in mm umrechnen und als uint16_t speichern (0-65535 mm = 0-65.5 m)
uint16_t distanceMm = (uint16_t)(distance * 10);
// Füllstand in mm für LoRaWAN
uint16_t fillHeightMm = (uint16_t)(fillHeight * 10);
Serial.println(F("--- Messung ---"));
Serial.print("Batterie: "); Serial.print(batteryVoltage, 2); Serial.print("V (");
Serial.print(batteryPercent, 0); Serial.println("%)");
Serial.print("Distanz: "); Serial.print(distance); Serial.println(" cm");
Serial.print("Füllhöhe: "); Serial.print(fillHeight); Serial.println(" cm");
Serial.print("Füllstand: "); Serial.print(fillPercent, 1); Serial.println(" %");
if (tooClose) {
Serial.println(F("WARNUNG: Flüssigkeit zu nah am Sensor!"));
}
// Display aktualisieren
display.clearDisplay();
// Header: iotwave.de
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("iotwave.de"));
display.drawLine(0, 10, 128, 10, SSD1306_WHITE);
// Füllstand groß anzeigen
display.setTextSize(3);
display.setCursor(0, 16);
display.print(fillPercent, 0);
display.println(F("%"));
// Füllhöhe klein anzeigen
display.setTextSize(1);
display.setCursor(0, 48);
display.print(F("Fuellhoehe: "));
display.print(fillHeight, 1);
display.println(F("cm"));
// Distanz anzeigen
display.setCursor(0, 56);
display.print(F("Distanz: "));
display.print(distance, 1);
display.println(F("cm"));
display.display();
// Display-Timer starten (nur beim ersten Mal)
if (displayStartTime == 0) {
displayStartTime = millis();
Serial.println(F("Display-Timer gestartet"));
}
// Prüfen ob LMIC bereit ist zum Senden
if (LMIC.opmode & OP_TXRXPEND) {
Serial.println(F("OP_TXRXPEND, nicht gesendet"));
} else {
// LoRaWAN Payload vorbereiten (8 Bytes):
// Byte 0-1: Distanz in mm (uint16_t)
// Byte 2-3: Füllhöhe in mm (uint16_t)
// Byte 4-5: Füllstand in % * 10 (z.B. 855 = 85.5%)
// Byte 6-7: Batteriespannung in mV (uint16_t)
uint16_t fillPercentInt = (uint16_t)(fillPercent * 10);
uint16_t batteryMv = (uint16_t)(batteryVoltage * 1000);
uint8_t payload[8] = {
highByte(distanceMm), // Distanz High Byte
lowByte(distanceMm), // Distanz Low Byte
highByte(fillHeightMm), // Füllhöhe High Byte
lowByte(fillHeightMm), // Füllhöhe Low Byte
highByte(fillPercentInt), // Füllstand % High Byte
lowByte(fillPercentInt), // Füllstand % Low Byte
highByte(batteryMv), // Batterie High Byte
lowByte(batteryMv) // Batterie Low Byte
};
// Debug: Payload anzeigen
Serial.print(F("LoRa Payload (Hex): "));
for (int i = 0; i < 8; i++) {
if (payload[i] < 16) Serial.print("0");
Serial.print(payload[i], HEX);
Serial.print(" ");
}
Serial.println();
LMIC_setTxData2(1, payload, sizeof(payload), 0);
Serial.println(F("LoRa Payload gesendet"));
userDataScheduled = true; // Markiere dass Nutzdaten gesendet wurden
}
}
void onEvent(ev_t ev) {
switch(ev) {
case EV_JOINING:
Serial.println(F("Beitritt läuft..."));
break;
case EV_JOINED:
Serial.println(F("Netzwerkbeitritt erfolgreich"));
joinedNetwork = true; // Merken für nächsten Boot
// Warte 5 Sekunden, damit MAC-Commands abgeschlossen werden
os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(5), do_send);
break;
case EV_TXCOMPLETE:
Serial.println(F("Nachricht übertragen."));
// Debug: Frame Counter
Serial.print(F("Frame Counter: "));
Serial.println(LMIC.seqnoUp - 1);
if (LMIC.txrxFlags & TXRX_ACK) {
Serial.println(F("ACK empfangen"));
}
if (LMIC.dataLen > 0) {
Serial.print(F("Downlink empfangen: "));
Serial.print(LMIC.dataLen);
Serial.println(F(" Bytes"));
}
// Prüfe ob echte Nutzdaten gesendet wurden
if (userDataScheduled) {
Serial.println(F("Nutzdaten erfolgreich gesendet"));
// Markiere Daten als gesendet
if (!dataSent) {
dataSent = true;
Serial.print(F("Übertragung abgeschlossen. Display bleibt noch "));
Serial.print((DISPLAY_ON_TIME - (millis() - displayStartTime)) / 1000);
Serial.println(F(" Sekunden an..."));
}
} else {
Serial.println(F("Nur MAC-Commands gesendet, warte auf nächstes TX..."));
// Sende Nutzdaten erneut nach 3 Sekunden
os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(3), do_send);
}
break;
default:
Serial.print(F("Ereignis: ")); Serial.println(ev); break;
}
}
void setup() {
Serial.begin(115200);
delay(1000);
// Boot-Zähler erhöhen
bootCount++;
Serial.println(F("\n=== ESP32 aufgewacht ==="));
Serial.print(F("Boot-Nummer: "));
Serial.println(bootCount);
// Wakeup-Grund anzeigen
esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
if (wakeup_reason == ESP_SLEEP_WAKEUP_TIMER) {
Serial.println(F("Aufgewacht durch Timer (5 Minuten vergangen)"));
} else {
Serial.println(F("Erster Start oder Reset"));
bootCount = 1;
joinedNetwork = false;
}
// OLED Display initialisieren
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
// Kurzer Startup-Screen (nur 1 Sekunde, Batterie sparen!)
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("iotwave.de"));
display.setCursor(0, 20);
display.print(F("Boot #"));
display.println(bootCount);
display.setCursor(0, 35);
display.println(F("Initialisiere..."));
display.display();
delay(1000);
// LMIC initialisieren
os_init();
LMIC_reset();
LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100);
// OTAA Join starten
LMIC_startJoining();
Serial.println(F("Starte LoRaWAN..."));
// Variablen zurücksetzen
dataSent = false;
displayStartTime = 0;
userDataScheduled = false;
}
void loop() {
os_runloop_once();
// Prüfe ob Display-Zeit abgelaufen ist und Daten gesendet wurden
if (dataSent && displayStartTime > 0) {
unsigned long currentTime = millis();
unsigned long elapsedTime = currentTime - displayStartTime;
// Nach DISPLAY_ON_TIME (30 Sekunden): Deep Sleep
if (elapsedTime >= DISPLAY_ON_TIME) {
Serial.println(F("Display-Zeit abgelaufen, gehe in Deep Sleep..."));
goToSleep();
}
}
}