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>
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
#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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user