home     Inhaltsverzeichnis
erste Version am 30.09.2017
letzte Änderung am 01.10.2017

Zirkulationspumpen-Steuerung - Seite 4


Firmware V1

EEPROM

Als erstes habe ich mich mal mit dem EEPROM vom ESP8266-12E befasst. Die Arduino-Doku passt hier leider nicht.
Laut ESP8266-Doku sind bis zu 4096 Byte nutzbar. Um das zu prüfen, habe ich folgendes Test-Script gebaut:
#include <EEPROM.h>

/*
void begin(size_t size);
uint8_t read(int address);
void write(int address, uint8_t val);
bool commit();
void end();

T &get(int address, T &t);
const T &put(int address, const T &t)
*/

#define EEPROM_SIZE 4096

char array[EEPROM_SIZE];

void setup() {
Serial.begin(115200);
/*
EEPROM.begin(EEPROM_SIZE);
for(int i=0; i<EEPROM_SIZE; i++) {
array[i]=random(0, 256); // random(min[incl], max[excl])
EEPROM.write(i, array[i]);
}
EEPROM.commit();
EEPROM.end();
*/

}

void loop() {
char c;
EEPROM.begin(EEPROM_SIZE);

Serial.println();
for(int i=0; i<EEPROM_SIZE; i++) {
if(i%32==0) {
Serial.println();
Serial.print(i, HEX);
Serial.print(" ");
}
Serial.print(array[i], HEX);
Serial.print(" ");
}
Serial.println("\n------------");
for(int i=0; i<EEPROM_SIZE; i++) {
if(i%32==0) {
delay(10);
Serial.println();
Serial.print(i, HEX);
Serial.print(" ");
}
c=EEPROM.read(i);
Serial.print(c, HEX);
Serial.print(" ");
}

while(true) {
delay(1000);
}
}

Wichtig sind die beiden delay()-Befehle. Ohne den ersten (in der read-Schleife) gibts sporadische Lesefehler. Immer an anderen Positionen. Und ohne den zweiten (also stünde da nur while(true);) deutet der ESP8266 das als Programm-Absturz.
Beim ersten Programm-Lauf war der blaue Block in setup() aktiv. Danach habe ich den Output in einen Editor kopiert, den Block auskommentiert, das Programm neu hochgeladen und den Output des zweiten Blockes mit dem gespeicherten Output verglichen.
Ergebnis: passt!
Diese Notwendigkeit von delay() gefällt mir aber gar nicht. Wie soll ich denn wissen, wann ich warten muss und wann genug gewartet wurde....
Na denn, wenn ich nur kleine Datenmengen im EEPROM ablege, wird das schon passen. Außerdem plane ich ohnehin, die Daten im EEPROM wieder per Prüfsumme abzusichern.

Nachtrag: der delay() in der read-Schleife hat nichts mit Lesefehlern zu tun - er hilft hier offenbar eher gegen Ausgabe-Fehler bei Serial.print(). Ändere ich den Code nämlich dahingehend, dass beim zweiten Programm-Lauf in der ersten Schleife aus dem EEPROM ins RAM bzw. nach array[] kopiert und in der zweiten Schleife der Inhalt von array[] an Serial.print() übergeben wird, passt die Ausgabe immer zu 100% zur Ausgabe des ersten Programm-Laufs.
Fazit: zu viele EEPROM.read()'s schnell hintereinander führen dazu, dass Serial.print() seine Daten nicht mehr beim Terminal loswird und deshalb Zeichen verschluckt.
Der EEPROM.read() alleine funktioniert problemlos.

Im EEPROM sollen folgende Daten abgelegt werden:
Wobei sich da gleich die Frage stellt, wie diese Datenstruktur für die Zeit-Bereiche aussehen soll.
Entweder als dynamisch lange Liste von Uhrzeiten (ggf. samt Wochentag) oder als Bitmuster mit einem Bit pro Minute.
Letzteres wird wahrscheinlich sehr viel einfacher zu implementieren sein.
Das wären dann 60*24*7=10.080 Bit=1.260 Byte. (60 Minuten/Stunde * 24 Stunden/Tag * 7 Tage/Woche)
Alle fünf Minuten würde eigentlich auch reichen: 12*24*7=2.016 Bit=252 Byte.
Und wenn nur zwischen Wochentag und Wochenende unterschieden würde: 12*24*2=576 Bit=72 Byte.
Ach was, 252 Byte sind schon völlig okay. Aber reicht eigentlich ein Bit?
Zum simplen Einschalten sicher, Ausschalten könnte sowieso immer dynamisch passieren (also wenn sich die Z-Temperatur hinreichend an die E-Temperatur angenähert hat). Aber wollte man dabei zwei (oder mehr) Ausschalt-Bedingungen vorgeben können, bräuchte es mehr als ein Einschalt-Bit. So etwa als: 0="aus", 1="ein und wieder aus bei Z>35", 2="ein und wieder aus bei Z>40", 3="ein und wieder aus bei Z>E-5".

Und zum Lernen und Verlernen von Einschaltzeiten bräuchte es eine Historie. Also mehrere Bitmaps bzw. eine Dimension mehr?
Andererseits hatte ich dazu ja die Idee, dass ich sowas lieber in Python berechnen will.
Das hieße dann, das Python-Script liefert 1x täglich die neue Einschaltzeiten-Bitmap. Konnte aus irgendeinem Grund nicht mit dem Python-Script gesprochen werden, wird vom ESP8266-12E die letztgültige Bitmap verwendet.
Der ESP8266-12E bleibt somit zwar dumm, kann aber trotzdem lange Zeit unabhängig vom SocketServer laufen. Er meldet die Sensor-Daten und empfängt die Einschaltzeiten. Ausschaltzeiten werden anhand von Temperatur-Deltas bestimmt, liegen aber mindesten fünf Minuten nach der letzten Einschaltzeit. So ließe sich mit mehreren 1-Bits hintereinander eine beliebig lange erzwungene Einschaltzeit erreichen (Stichwort Legionellen-Schaltung).

Dabei fällt mir gerade ein, dass die Heizung nach einem Stromausfall grundsätzlich alles vergessen hat. Nicht nur aufwendig zusammen-getipperte Brauchwasser-Temperatur/Zeit-Profile (die ich deswegen nicht nutzen kann/will), sondern auch Wochtag und Uhrzeit. Somit könnte die Legionellen-Funktion nach einem kurzen Stromausfall theoretisch zu jedem Zeitpunkt eingeleitet werden. Würde die Schaltung dann nur ihre festen Pump-Zeiten abfahren, würde das besonders heiße Wasser nur zufällig durch die Rohre gepumpt werden. Daher sollte die Schaltung die Pumpe auch unabhängig von der Zeiten-Bitmap einschalten, wenn Sensor(E) mehr als 52°C oder Sensor(L) mehr als 60°C meldet. Und zwar solange, bis auch Sensor(Z) über 52°C meldet oder die Pumpe bereits zwei Stunden läuft.
Analog dazu kann die Schaltung eine vorgegebene Einschaltzeit überspringen (also die ausgeschaltete Pumpe nicht einschalten), wenn Sensor(Z) bereits mehr als 45°C und Sensor(E) weniger als 52°C meldet (das "und" ist hier als logische Verküpfung gemeint: Z>45 AND E<52).

mehrere SSIDs

Dann zur nächsten Erweiterung bzgl. Rollladensteuerung und den Loggern: der Name des WLANs soll bei dieser Firmware änderbar sein. Andernfalls kann ich dessen Namen im WLAN-Router nämlich nie (ohne extremen Aufwand == umflashen diverser Schaltungen) ändern. Weil dem ESP8266 ein neuer WLAN-Name aber nur mitgeteilt werden kann, wenn dieser gerade mit einem WLAN verbunden ist, muss er mindestens zwei WLAN-Namen verwalten können.
Der ConnectWLAN-Aufruf sähe dann so aus, dass der erste Versuch mit dem primären WLAN-Namen stattfindet. Läuft das in einen Timeout, wirds beim nächsten WLAN-Namen probiert. Ist er da erfolgreich, wird der primäre WLAN-Name entsprechend geändert.
Statt stumpf durchzuprobieren, sollten sich die aktuell sichtbaren WLANs auch vorab auflisten lassen.
Das mitgelieferte Beispiel-Sketch "WiFiScan" liefert hier im Büro:
scan start
scan done
3 networks found
1: FreifunkWees01.2 (http://ffw) (-78)
2: FreifunkWees01.3 (http://ffw) (-91)
3: FreifunkWees01.0 (http://ffw) (-40)
Da bekomme ich sogar die Info, welches WLAN am besten zu erreichen ist.

Dann baue ich mal was, womit alle aktuell gesehenen WLANs anhand einer Liste mit vorgegebenen SSIDs der Reihe nach durchprobiert werden:
#include "ESP8266WiFi.h"

char SSID[3][33]={"FreifunkWees01.0 (http://ffw)", "FreifunkWees01.2 (http://ffw)", "FreifunkWees01.3 (http://ffw)"};

int connectKnownWLAN(void) {
int i, wlan_cnt, wlan_nr, retries;
char ssid[33]; // maxLen(SSID): https://serverfault.com/a/45509/430599

WiFi.forceSleepWake();
wlan_cnt=WiFi.scanNetworks();
if(wlan_cnt>0) {
for(i=0; i<3; i++) {
for(wlan_nr=0; wlan_nr<wlan_cnt; ++wlan_nr) {
WiFi.SSID(wlan_nr).toCharArray(ssid, 33);
if(strcmp(ssid, SSID[i])==0) {
Serial.print(ssid); Serial.print(" "); Serial.println(WiFi.RSSI(wlan_nr));
WiFi.begin(ssid);
for(retries=0; retries<20 && WiFi.status()!=WL_CONNECTED; retries++) { // max. 10 Sekunden probieren
delay(500);
}
if(WiFi.status()==WL_CONNECTED) {
return(1);
}
}
}
}
}
return(0);
}


void printMAC(void) {
byte mac[6];
WiFi.macAddress(mac);
Serial.print("MAC: ");
for(int i=0; i<6; i++) {
if(mac[i]<16) Serial.print("0");
Serial.print(mac[i], HEX);
if(i<5) Serial.print(":");
}
Serial.println();
}

void setup() {
Serial.begin(115200);
Serial.println("\n\n----");
if(connectKnownWLAN()==1) {
Serial.println("verbunden als ");
Serial.println(WiFi.localIP());
}
printMAC();
}

void loop() {}

Das liefert dann sowas (die .0 steht hier im Büro):
FreifunkWees01.0 (http://ffw)  -48
verbunden als  
192.168.42.112
MAC: 60:01:94:14:E1:C1

Wenn ich in den ersten Namen aus SSID[][] ein "X" einfüge, um ihn damit ungültig zu machen, wird das hier geliefert (und er landet auf der .2 im Wohnzimmer):
FreifunkWees01.2 (http://ffw)  -71
verbunden als  
192.168.42.112
MAC: 60:01:94:14:E1:C1 

Noch ein "X" im zweiten Namen führt dazu, dass er drei Etagen höher (und durch zwei Stahlbeton-Decken) muss - dann wirds langsam schwierig mit dem Connect...aber grundsätzlich tut die Funktion, was sie soll:
FreifunkWees01.3 (http://ffw)  -92
MAC: 60:01:94:14:E1:C1

...reset...
...reset...

FreifunkWees01.3 (http://ffw)  -90
verbunden als 
192.168.42.112
MAC: 60:01:94:14:E1:C1

Will heißen: funktioniert wie vorgesehen.

Jetzt habe ich noch ein paar Funktionen aus der LoggerV2-Firmware zur Kommunikation mit dem SocketServer zugefügt. Und schon sieht es so aus:
FreifunkWees01.0 (http://ffw)  -48
verbunden als 
192.168.42.112
verbunden mit SocketServer
GET_TIME gesendet
2017.09.30 17:52:06
SocketServer wird getrennt
WLAN wird getrennt
MAC: 60:01:94:14:E1:C1

...oder entsprechend auf die .2, wenn ich wieder ein "X" in den ersten Namen setze.

Konsequenterweise sollte ich dann auch gleich noch die IP-Adresse samt Portnummer vom SocketServer änderbar machen.
Das war schnell erledigt. Liefert jetzt:
FreifunkWees01.0 (http://ffw)  -45
verbunden als 
192.168.42.112
verbinde mit SocketServer @192.168.42.253:12311
verbinde mit SocketServer @192.168.42.253:12311
verbinde mit SocketServer @192.168.42.253:12311
verbinde mit SocketServer @192.168.42.253:12311
verbinde mit SocketServer @192.168.42.253:12311
verbinde mit SocketServer @192.168.42.80:2628
verbunden mit SocketServer
GET_TIME gesendet
2017.09.30 18:21:25
SocketServer wird getrennt
WLAN wird getrennt
MAC: 60:01:94:14:E1:C1


Der Port 12311 war ein Test-Dummy - auf dem lauscht nix.

Gut....damit kann ich jetzt lesend und schreibend auf das EEPROM zugreifen und ich kann mit einem variablen SocketServer hinter einem variablen WLAN-Router sprechen. Mit dem vorgesehenen RTC-Modul kann ich schon länger kommunizieren. Die Sensoren abfragen, LEDs und Relais ansteuern habe ich ebenfalls schon geübt. Damit sollte alles beisammen sein.
Also braucht es nur noch einen angepassten SocketServer und die Main-Loop.

Vielleicht sollte ich noch vorsehen, bis zu vier Sync-Zeiten pro Tag einstellen zu können.
Auch die oben angedachten Temperatur-Schwellwerte zum außerordentlichen Ein- und Ausschalten der Pumpe sollten über WLAN justierbar sein.

Das Protokoll für die Schaltung zum SocketServer wäre dann:
	Verbindung herstellen
	Zeit anfragen und empfangen
	[nur aus setup()] Sensor-Adressen senden und Sensor-Funktionszuordnung empfangen
	WLAN-Namen, SocketServer-IP:Port senden und ggf. Änderungen empfangen
	Sync-Zeiten senden und ggf. Änderungen empfangen
	Temperatur-Schwellwerte senden und ggf. Änderungen empfangen
	[nicht aus setup()] gesammelte Sensor-Daten senden
	Schaltzeiten-Bitmap senden und ggf. Änderungen empfangen
	Verbindung schließen

Die beiden LEDs brauchen noch ihre Funktionszuordnung. Zu meldende Events wären:
Vielleicht so:
blaue LED
rote LED
blinkt im 1/4-Sekunden-Takt
Zirkulation läuft
blinkt im 1/4-Sekunden-TaktTemperatur-Problem erkannt
blinkt im Sekunden-Taktdie Schaltung fühlt sich wohlblinkt im Sekunden-TaktWLAN-Sync fehlgeschlagen
leuchtet konstantWLAN-Sync läuft

leuchtet nicht
ins Nirvana verabschiedet
leuchtet nichtkeine Probleme

Das soll für heute erstmal reichen. Morgen gehts dann auf der nächsten Seite weiter.