ESP 32: Smarthome Beet über MQTT: FAST fertig

OP #7658162
Lesenswert?

Ich grüße Euch,

ich programmiere gerade meinen ersten µc. Ich habe ein Hochbeet, welches automatisch gewässert, gelüftet und beleuchtet werden soll. Die Messdaten dafür kommen von einem Xiaomi MiFlora Sensor. Die Auswertung läuft über einen smarthomeng-Server, der mit dem ESP32 im Beet über MQTT kommuniziert. Ausgangsbasis war dieser Code: https://github.com/e6on/ESP32_MiFlora_MQTT Jetzt funktioniert fast alles, für die verbliebenen Probleme erhoffe ich mir hier Hilfe :)

Den gesamten Code habe ich als Textfile beigefügt.

Problem 1: Der Eingang für den Schwimmer des Wassertanks soll sofort übertragen werden. Der Interrupt macht aber nichts, heißt er wird einfach ignoriert. Es könnte auch ein physischen Problem mit dem Schalter sein, das überprüfe ich am WE

1
void setupPins() {
2
  for (int i = 0; i < sizeof(ausgangspins) / sizeof(ausgangspins[0]); i++) {
3
    pinMode(ausgangspins[i], OUTPUT);
4
    digitalWrite(ausgangspins[i], LOW);
5
  }
6

7
  pinMode(waterSensorPin, INPUT); // Eingangspin für Wasserstandssensor
8
  attachInterrupt(digitalPinToInterrupt(waterSensorPin), sendWaterLevel, CHANGE);
9
}
1
void sendWaterLevel() {
2
  // Überprüfe den Wasserstand
3
  Serial.println ("-- read Water Level");
4
  bool wasserstand = digitalRead(waterSensorPin);
5
  Serial.print("Level is ");
6
  if (wasserstand) {
7
    Serial.println("HIGH");
8
  } else {
9
    Serial.println("LOW");
10
  }
11
  client.publish((MQTT_BASE_TOPIC + "/wasser").c_str(), wasserstand ? "1" : "0");
12
  Serial.println(" >> published");
13

14
}

Problem 2: Was kann ich in meinem loop optimieren hinsichtlich:

  • mehr Energie-Effizienz, also weniger idle Rechenleistung während ich auf MQTT Nachrichten oder Interrupts vom Input warte
  • Selbstüberwachung: Der µc soll im Fehlerfall das Problem erstmal versuchen durch einen Neustart zu lösen. Frage: Sind regelmäßige geplante Reboots sinnig und wie würdet ihr sie umsetzen?
1
void loop() {
2
  // check MQTT connection
3
  if (!client.connected()) {
4
    Serial.println("!Not connected >> reconnect");
5
    connectMqtt();
6
    subscribePins();
7
  } else {
8
    client.loop();
9
    if (bootCount >200000) {
10
      bootCount = 0;
11
      Serial.println("-- connection alive, connect.loop sent");
12
    } else {
13
      bootCount ++;
14
    }
15
  }
16
  //sendWaterLevel();
17

18
}

Problem 3: Prinzipiell kommen die MQTT Nachrichten an. Nur bei "pumpe" passiert nichts. Wahrscheinlich nur ein Tippfehler, aber ich finde ihn partout nicht :(

Serial Monitor:

1
23:16:17.147 -> --Pin set HIGH; returning to main programm
2
23:16:17.147 -> Running loop
3
23:16:17.147 -> Running loop
4
23:16:22.355 -> -- connection alive, connect.loop sent
5
23:16:29.428 -> Message arrived [beet0/motor] false
6
23:16:29.428 -> topicTail: motor
7
23:16:29.428 -> Running loop
8
23:16:29.428 -> Running loop
9
23:16:29.428 -> Running loop
10
23:16:29.428 -> --Pin set LOW; returning to main programm
11
23:16:29.428 -> Running loop
12
23:16:29.428 -> Running loop
13
23:16:37.627 -> Message arrived [beet0/dir] true
14
23:16:37.627 -> topicTail: dir
15
23:16:37.627 -> Running loop
16
23:16:37.627 -> Running loop
17
23:16:37.627 -> --Pin set HIGH; returning to main programm
18
23:16:37.627 -> Running loop
19
23:16:37.627 -> Running loop
20
23:16:37.627 -> Running loop
21
23:16:38.940 -> Message arrived [beet0/dir] false
22
23:16:38.940 -> topicTail: dir
23
23:16:38.940 -> Running loop
24
23:16:38.940 -> Running loop
25
23:16:38.940 -> --Pin set LOW; returning to main programm
26
23:16:38.940 -> Running loop
27
23:16:38.940 -> Running loop
28
23:16:38.940 -> Running loop
29
23:16:40.364 -> Message arrived [beet0/dir] true
30
23:16:40.364 -> topicTail: dir
31
23:16:40.364 -> Running loop

Und die Abo-funktion:

1
void subscribePins() {
2
  Serial.println("-- Subscribe to properties");
3
  client.setCallback(callback);
4

5
  for (int i = 0; i < sizeof(properties) / sizeof(properties[0]); i++) {
6
    if (client.connected()) {
7
      String topic = String(MQTT_BASE_TOPIC) + "/" + String(properties[i]);
8
      if (client.subscribe(topic.c_str())) {
9
        Serial.println("Subscribed to topic: " + topic);
10
      } else {
11
        Serial.println("Failed to subscribe to topic: " + topic);
12
      }
13
    }
14
  }
15
  String topic1 = String(MQTT_BASE_TOPIC) + "/MiFloraRead";
16
  if (client.subscribe(topic1.c_str())) {
17
    Serial.println("Subscribed to topic: " + topic1);
18
  } else {
19
    Serial.println("Failed to subscribe to topic: " + topic1);
20
  }
21
  String topic2 = String(MQTT_BASE_TOPIC) + "/MiFloraBatt";
22
   if (client.subscribe(topic2.c_str())) {
23
    Serial.println("Subscribed to topic: " + topic2);
24
  } else {
25
    Serial.println("Failed to subscribe to topic: " + topic2);
26
  }
27
}

Allgemein freue ich mich sehr über Verbesserungsvorschläge, da ich bestimmt einiges zu krude und ungelenk gemacht habe.

Liebe Grüße Timo

Angehängte Dateien:
#7658170
Lesenswert?

Im Interrupt Kontext darf man nicht beliebige API Funktionen aufrufen, schon gar nicht Serial.print oder MQTT senden. Außerdem fehlt das Attribut das die Funktion im RAM gehalten wird. Suche erstmal nach Interruptprogrammierung des ESP.

Sicher das der pumpe topic gesendet wird? Zum testen würde ich eher sowas wie MQTTExplorer verwenden.

OP #7658182
Lesenswert?

Danke für deine Antwort. Das Pumpen-Problem war tatsächlich ähnlich einfach wie angenommen: Da das Signal des Wasser-Sensors fehlt, gibt der smarthomeng-Server das Pumpenitem nicht frei. Deshalb wird nichts gesendet. Also gelöst.

Die Interruptprogrammierung werde ich mir dann mal zu Gemüte führen, mal sehen, ob ich selbst auf die Lösung komme :)

Im ersten Go bin ich auf folgenden Artikel gestossen: https://wolles-elektronikkiste.de/esp32-mit-arduino-code-programmieren#interrupts Hier wird ein Interrupt über einen GPIO beschrieben, also eigentlich so wie es bei mir auch geplant ist. Die Krux: Ich halte den Interrupt so für völlig unnütz! Die ISR setzt ja eine bool Variable, die dann im loop ausgelesen wird. Dann kann ich ja auch einfach den Input im loop auslesen und mir den Interrupt sparen, oder?

1
int interruptPin = 4; // define GPIO4 as interrupt pin
2
volatile bool event = false;
3

4
void IRAM_ATTR eventISR(){
5
  event = true;
6
  detachInterrupt(interruptPin);
7
}
8

9
void setup() {
10
  Serial.begin(115200);
11
  while(!Serial){}
12
  pinMode(interruptPin, INPUT_PULLDOWN);
13
  attachInterrupt(interruptPin, eventISR, RISING);
14
}
15

16
void loop() {
17
  if(event){
18
    Serial.println("Interrupt!");
19
    delay(1000); //debouncing
20
    event = false;
21
    attachInterrupt(interruptPin, eventISR, RISING);   
22
  }
23
}

Wie würdet ihr den Eingang für den Schwimmerschalter überwachen? Vielleicht einfach ein eigener Task, der den Sensor überwacht?

#7659207
Lesenswert?

Ja du kannst deinen Schwimmerschalter auch regelmäßig in der loop einlesen was für den Zweck vollkommen ausreicht. Beim ESP kannst du auch keinen Strom sparen indem du in der loop weniger nichts machst, nicht benutzte Rechenzeit wird immer irgendwo vertrödelt - egal ob mit dem Code in der loop oder einem delay im Hintergrund. Der Interrupt macht für Eingänge nur Sinn wenn das Signal so kurz anliegt das die Gefahr besteht das es bei entsprechender Durchlaufzeit der loop bis zur nächsten Abfrage schon wieder weg ist.

Sascha

Gast #7659210
Lesenswert?

Timo G. schrieb:

Sind regelmäßige geplante Reboots sinnig und wie würdet ihr sie umsetzen?

Mikrocontroller laufen normalerweise viele Jahre lang fehlerfrei durch. Wenn nicht, würde ich das gerne wissen um es beheben zu können. Regelmäßige Neustarts doktorn am Symptom herum, ohne das Problem zu lösen.

Der ESP8266 (vermutlich auch der ESP32) hat schon einen Watchdog, der den Chip automatisch neu startet, wenn er hängt. Nach meiner Erfahrung nützt das wenig, weil er kurz danach wieder Fehlfunktionen hat, und oft sogar schon beim Booten (bevor die erste Zeile eigener Code ausgeführt wird) stecken bleibt.

Ernsthaft Strom sparen kannst du nur, indem du WLAN deaktivierst oder den light sleep Modus nutzt.

#7659257
Lesenswert?

Sascha W. schrieb:

Der Interrupt macht für Eingänge nur Sinn wenn das Signal so kurz anliegt das die Gefahr besteht das es bei entsprechender Durchlaufzeit der loop bis zur nächsten Abfrage schon wieder weg ist.

Nicht nur dann. Sinnvoll ist ein Interrupt auch dann, wenn besonders schnell darauf reagiert werden muss. Wenn man immer erst wartet, bis loop() an der Abfrage vorbei kommt, kann es für manche Anwendungen zu spät sein.

Im Fall des OP trifft beides nicht zu, da sollte man ohne Interrupt arbeiten.

#7659272
Lesenswert?

Timo G. schrieb:

Hier wird ein Interrupt über einen GPIO beschrieben, also eigentlich so wie es bei mir auch geplant ist.

Das ist ja nur eine Demo, die zeigen soll, wie man mit Interrupts umgehen kann. Sie sagt nicht, unter welchem Umständen das sinnvoll ist.

Übrigens steckt da ein "Makel" drin, den man immer wieder sieht. Wozu denn in der ISR den Interrupt aushängen und später wieder einhängen? Das führt in vielen Szenarien zu gelegentlichen Fehlern. Was passiert z. B., wenn das nächste Ereignis erscheint, während die ISR gerade ausgehängt ist? Dann wird es unter Umständen nicht erkannt. Besser lässt man die ISR ständig eingehängt und lässt sie (oder loop) entscheiden, ob sie das Ereignis ignorieren will oder nicht.

Klar, man kann immer Gegenbeispiele für obige Argumente finden. Aber die sinnvolle Grundregel ist IMHO so.

#7659449
Lesenswert?

Timo G. schrieb:

Wie würdet ihr den Eingang für den Schwimmerschalter überwachen? Vielleicht einfach ein eigener Task, der den Sensor überwacht?

Mir scheint, du bist auf den falschen Zeitskalen unterwegs. Auch wenn ein richtige Flutwelle über dein Beet schwappt und es innerhalb von Bruchteilen von Sekunden flutet, nützt es dir überhaupt nichts, den Sensor im Mikrosekundenbereich auszuwerten. Was willst du dadurch veranlassen?

Timo G. schrieb:

Der Eingang für den Schwimmer des Wassertanks soll sofort übertragen werden.

Was heißt in diesem Zusammenhang "sofort"? Wie zeitkritisch ist diese Meldung? µs[ ], ms[ ] oder s[ ] (zutreffendes bitte ankreuzen)

OP #7659771
Lesenswert?

Vielen Dank für eure Antworten und euren Input! Der ESP läuft, momentan noch auf einem Breadboard, am Blumenbeet. Bislang läuft alles wie erhofft, jetzt stehen erstmal die Restarbeit am Beet selbst an.

Steve van de Grens schrieb:

Der ESP8266 (vermutlich auch der ESP32) hat schon einen Watchdog, der den Chip automatisch neu startet, wenn er hängt.

Den Watchdog habe ich auch schon entdeckt =) Du hast natürlich recht, er löst keine Code Probleme, aber er ist ja nunmal da.

Steve van de Grens schrieb:

Ernsthaft Strom sparen kannst du nur, indem du WLAN deaktivierst oder den light sleep Modus nutzt.

Also WLAN ausschalten geht schonmal nicht, das Beet muss ja Befehle via MQTT empfangen können. sleep ist eine gute Idee, um das Beet nachts länger zu pausieren. Da reicht es ja, wenn es alle 1-2h aufwacht, der Server ggf. die Lüftung verfährt und ihn danach wieder schlafen schickt. Das wäre dann aber ziemlich weit unten auf der ToDo Liste.

Rainer W. schrieb:

Mir scheint, du bist auf den falschen Zeitskalen unterwegs. Auch wenn ein richtige Flutwelle über dein Beet schwappt und es innerhalb von Bruchteilen von Sekunden flutet, nützt es dir überhaupt nichts, den Sensor im Mikrosekundenbereich auszuwerten. Was willst du dadurch veranlassen?

Ja, da hast du definitiv recht. Wenn der Sensor "leer" anzeigt, zieht die Pumpe ja nicht direkt Luft. Insofern:

Rainer W. schrieb:

Was heißt in diesem Zusammenhang "sofort"? Wie zeitkritisch ist diese Meldung? µs[ ], ms[ ] oder s[x] (zutreffendes bitte ankreuzen)

Ich habe jetzt folgendes im loop stehen:

1
  // check if input changed
2
  if (!wasserstand == digitalRead(waterSensorPin)) {
3
    Serial.println("input changed - sending value");
4
    delay(500);
5
    wasserstand = digitalRead(waterSensorPin);
6
    MQTTsendWLL();
7
  }

Das funktioniert wie gewünscht. Das delay von 500ms soll den Sensor etwas entprellen, dass nicht ständig eine Änderung geschickt wird wenn der Schwimmer umschaltet und die Pumpe so zum stottern bringt.

OP #7660817
Lesenswert?

Ich schreibe einfach mal hier weiter und mache keinen neuen Post dafür auf: Kaum läuft die erste Version, habe ich schon erste Veränderungen im Sinn. Denn der µc muss sowieso nochmal getauscht werden. Der WLAN Empfang der onboard Antenne lässt zu wünschen übrig und derzeit steht deshalb Repeater vor der Terassentür. Also will ich auf einen Seeed Xiao wechseln, da ich hier eine "richtige" WLAN Antenne anschließen kann. Außerdem läuft alles derzeit noch auf einem Breadboard, auch kein Dauerzustand. Die Schaltung von Beleuchtung (12V LED Streifen, definitive Länge und damit Stromverbrauch steht noch nicht fest), Bewässerungspumpe (12V 5A Membranpumpe) und Linearmotor für die Beetöffnung läuft derzeit über eine fertige 4 Kanal Relais + 1 Finder Doppelrelais für die Polaritätsumkehr des Linearantriebes. Für die Zukunft möchte ich den Anschluss für eine kleine Schlauch-/Dosierumpe für Düngemittel vorsehen. Funktioniert alles, finde es aber irgendwie nicht elegant genug. Außerdem nutze ich meine Projekte ja immer um etwas zu lernen.

Von Euch erhoffe ich mir nun Input zu folgenden Fragen:

  • Welche Art der Ansteuerung könnt ihr mir für welchen Ausgang empfehlen?
  • Wo finde ich einen guten Artikel zum Thema "Welche Last wie schalten"?

Meine aktuelle Idee:

  • 2 Kanal Motortreiber für Linearantrieb (wg. Richtungswechel) und Dosierpumpe (PWM zur feinen Dosierung)
  • FET oder Relais für LED-Licht: FET könnte man zwar dimmen, aber braucht man das in diesem Anwendungsfall?
  • Relais + ?Kondensator? für Bewässerungspumpe: Hier habe ich in der Hauptsache Sorge um den µc, da er via 7805 an der selben 12V Spannungsversorgung hängt. Kondensator dann im 12V Teil oder im 5V Teil?

Vielen Dank für Euren Input

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren