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>
350 lines
11 KiB
C++
350 lines
11 KiB
C++
#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();
|
||
}
|
||
}
|
||
} |