Die gesamte Firmware ist in einzelne Funktionsgruppen aufgeteilt. Zur jeder dieser Gruppen gibt es eine oder mehrere Klassen. Durch die strikte Trennung steigt die Übersichtlichkeit und Seiteneffekte werden vermieden.
In diesem Kapitel werden die Basis-Komponenten beschrieben.
Inhaltsverzeichnis
Verwendung (Klassen EnergySensorClass und EnergyData)
Implementierung (Klassen EnergySensorClass und PZEMSerial)
Persistenz (Klasse EMPersistenceClass)
Die Klasse EnergySensorClass stellt die Verbindung mit dem PZEM-004T-Messmodul her. Die wesentlichen Methode ist getLastReading(), die die zuletzt ausgelesenen Messwerte in Form eines Objekts vom Typ EnergyData zurück liefert, und handle(), die für das regelmäßige Auslesen der Messwerte sorgt.
Die Klasse EnergySensorClass kommuniziert mit dem PZEM-004T über eine serielle Schnittstelle. Die Schnittstelle ist eine Software-Schnittstelle. Als RX- und der TX-Pin stehen prinzipiell die GPIOs 0..5, 12..15 zur Verfügung. Wegen der Sonderfunktionen auf einigen Pins verbleiben 4 (D2), 5 (D1), 12 (D6), 13 (D7), 14 (D5) und 15 (D8). Die entsprechenden Pins müssen im Konstruktor angegeben werden. Weiterhin kann man den Leiter, dessen Stromfluss man messen will, mehrfach durch den Stromtransformator führen. Das erhöht die Empfindlichkeit, reduziert aber den Messbereich. Die Anzahl der Windungen ist ebenfalls im Konstruktor anzugeben. In Arduino-Manier wird in EnergieSensor.h die Instanz EnergySensor der Klasse deklariert und in EnergieSensor.h definiert:
EnergySensorClass EnergySensor(PZEM_TX, PZEM_RX, COIL_TURNS);
Die zugehörigen Konstanten sind in EnergyMeter.h definiert:
#define PZEM_RX D6 // Pin, an den das RX-Signal des PZEM-004T angeschlossen ist
#define PZEM_TX D7 // entsprechend das TX-Signal
#define COIL_TURNS 4 // Anzahl Windungen durch den Stromtransformator
Die Methode begin() initialisiert die zu Grunde liegende Schnittstelle und sollte in setup() aufgerufen werden. Der Methode kann Pointer auf ein von Print abgeleitet Objekt mitgegeben über das Status- und Fehlermeldungen ausgegeben werden.
EnergySensor.begin(EnergyMeterLog); // Kommunikation mit dem PZEM-004T initialisieren
Die Methode reset() setzt den Gesamtverbrauchszähler zurück auf 0. Die Basisdaten zur Berechnung des Gesamtverbrauchs werden im EEPROM gespeichert und bei Programmstart von dort geladen. Der Gesamtverbrauch bleibt also über einen Reset hinaus erhalten.
Über getSensorData() können die aktuellen Messwerte als ein Objekt der Klasse EnergyData ausgelesen werden.
Methode | Bedeutung | Anmerkung |
---|---|---|
EnergySensorClass( uint8_t rxPin, uint8_t txPin, uint8_t coilTurns = 1, uint16_t interval = 5) |
Initialisiert eine neue Instanz von EnergySensorClass.
|
rxPin muss mit dem Sende-Pin des PZEM-004T verbunden
werden und umgekehrt. In EnergySensor.cpp ist eine Instanz diese Klasse mit der Bezeichnung EnergySensor definiert. |
bool begin( Print* logDevice = NULL) |
Initialisiert die Kommunikation mit dem PZEM-004T. logDevice: Ausgabeschnittstelle für Status- und Fehlermeldungen. |
Der persistierte Basis-Wert für den Gesamtverbrauch wird aus dem EEPROM geladen. |
void reset() | Setzt den Gesamtverbrauchszähler auf 0. | Der aktuelle Basis-Wert für den Gesamtverbrauch wird im EEPROM gespeichert. |
void handle() | Sorgt für die Auslesung des PZEM in den festgelegten Abständen. | |
void setInterval(uint16_t interval) | Legt den Zeitabstand in Sekunden zwischen zwei Ablesungen des PZEM fest. | |
EnergyData getLastReading() | Ruft die zuletzt ausgelesenen Messwerte ab. | EnergyData wird weiter unten beschrieben. |
void onEnergySensorReset (EnergySensorResetHandlerFunction f) | Registriert die Callback-Funktion f. | EnergySensorResetHandlerFunction ist definiert als
typedef std::function<void(float startValue, time_t startTime)> EnergySensorResetHandlerFunction .f wird aufgerufen, wenn der Verbrauchszähler zurück gesetzt wurde. startValue enthält den aktuellen Verbrauch des PZEM-004T-Verbrauchzähler. |
Über ein Objekt der Klasse EnergyData werden die aktuellen Messwerte zurückgeliefert.
Feld / Methode | Bedeutung | Anmerkung |
---|---|---|
float power | Aktuelle Leistungsaufnahme in Watt. | Direkt vom PZEM-004T ausgelesenen und mit CoilTurns verrechnet. |
float energy | Bisherige Gesamtverbrauch in Wh. | Verrechnet mit dem beim letzten Ausführen reset() gespeicherten Basiswert und mit CoilTurns. |
float energyRaw | Eingelesener Rohwert des Gesamtverbrauchs. | Direkt vom PZEM-004T eingelesener Wert. Unklar ist der Wert bei dem ein Überlauf stattfindet. |
bool isValid | Gibt an, ob die vorhergehenden Felder gültige Daten enthalten. | False bei Kommunikationsproblemen |
time_t timeStamp | Zeitpunkt der Messung. | Unix-Zeitstempel, Zeitpunkt: Anfang des Datenaustausch mit dem PZEM-004T |
uint32_t secondsSinceStart | Sekunden seit Start der aktuellen Messreihe. | Nur valide bei timeStramp = true. |
bool isTimeStampValid | Gibt an, ob timeStamp einen gültigen UNIX-Zeitstempel enthält. | Bei false enthält timeStamp den Wert von millis()/1000. |
String toString() | Liefert einen String-Ausdruck der Messwerte. | <Leistung>;<Verbrauch>;<PZEM-Fehlerkennung>;<Laufzeit>;<Zeit>;<Zeit-Fehlerkennung> z.B.: "105.5;0.018;ok;27668;0d 07h 41m;ok" Zur Erläuterung der einzelnen Felder siehe HTTP-Interface |
Basis für die Kommunikation mit PZEM-004T ist die Bibliothek: GitHub: olehs/PZEM004T. Die dort enthaltene Klasse PZEM004T kommuniziert über eine serielle Schnittstelle mit dem Messmodul. Es kann sowohl eine Hardware- als auch eine Software-Schnittstelle genutzt werden. Der Betrieb mit der Software-Schnittstelle nutzt eine Methode (listen()), die in der Standard-Implementierung des ESP8226 in der Version 2.3.0 nicht enthalten ist. Des Weiteren lieferten die Lese-Methode keine aussagekräftigen Fehlermeldungen. Weil die Klasse sowieso gekapselt werden sollte, um die öffentliche Schnittstelle zu vereinfachen, wurde die Klasse EnergySensorClass eingerichtet und alle wesentlichen Methoden der Library übernommen.
Ähnliches gilt für die in der Library PZEM004T benutzten serielle Softwareschnittstelle. Das Problem mit listen() wurde bereits beschrieben. Des Weiteren traten nach etwa 30 Minuten Betrieb des Geräts erhebliche Lesefehler auf. Nach etwa 45 Minuten war keine Kommunikation mehr möglich. Meist wurde das erste Bit des übertragenen Bytes als 0 eingelesen, obwohl eine 1 gesendet wurde. Das legt nahe, dass das Startbit nicht richtig ausgefiltert wird. Vermutlich geht etwas von der Flankensteilheit verloren und der Pin-Change-Interrupt wird zum falschen Zeitpunkt ausgelöst. Nach dem Interrupt wartet die ISR auf das erste Bit. m_bitTime enthält die Anzahl Prozessorzyklen für ein Bit und ist abhängig von der Baudrate:
// Advance the starting point for the samples but compensate for the
// initial delay which occurs before the interrupt is delivered
unsigned long wait = m_bitTime + m_bitTime / 3 - 500;
Die Verlängerung auf … + m_bitTime / 2 …
und die Verringerung
des auf dem PZEM-004T enthalten PullUp-Widerstands durch Hinzuschalten des internen PullUp-Widerstandes
pinMode(m_rxPin, INPUT_PULLUP); // Original: pinMode(m_rxPin, INPUT)
führte zu einem stabilen Betrieb. Um die Originalversion von SoftwareSerial nicht zu verändern wurde die Modifikationen an einer Kopie (PZEMSerial) vorgenommen.
Die Ermittlung des Gesamtverbrauchs bedarf einer etwas genaueren Betrachtung. Es gibt keine einfache Möglichkeit diesen Wert auf dem PZEM-004T zurück zu setzen. Deshalb wird vom aktuellen Messwert des Gesamtverbrauchs der Wert abgezogen, den er beim Ausführen der Methode reset() hatte. Dieser Wert wird im internen Feld energyBase gehalten. energyBase wird bei jedem Aufruf von reset() vom PZEM-004T ausgelesen und und im EEPROM persistiert (s. Abschnitt Peristenz). Bei einem Neustart des Programms, z.B. nach einem unvorhergesehenen Reset, kann also mit den alten Daten weiter gearbeitet werden
Ebenfalls ist unklar, bei welchem Wert ein Überlauf des Gesamtverbrauchs statt findet. Aus diesem Grunde wird der eingelesene Gesamtverbrauch (Rohwert) mit dem vorhergehenden verglichen. Ist der neue Wert kleiner als der alte, wird der alte Wert in der Datei overflow.txt abgelegt.
// Überlaufswert sichern
if (e.energyRaw < lastEnergy) { // Überlauf
SPIFFS.begin();
File f = SPIFFS.open("/overflow.txt", "a");
f.println(lastEnergy, 3);
f.close();
}
lastEnergy = e.energyRaw;
Ist der Wert einmal bekannt, kann die Ermittlungsmethode für den Gesamtverbrauch entsprechend angepasst werden.
Der Basiswert für die Berechnung des Gesamtverbrauchs soll bei einem Reset des System erhalten bleiben. Er wird deshalb nach jeder Änderung im EEPROM abgelegt. Das Speichern und Zurücklesen erledigen Methoden der Struktur EMPersistenceClass.
Die grundsätzliche Nutzung des EEPROMs ist in ESP8266 spezielle Klassen: EEPROM beschrieben. Um das Ganze ein wenig übersichtlicher und einfacher handhabbar zu machen, wurden die zu persistierenden Daten in einer Struktur (EMPersistenceClass) zusammenfast. Der Konstruktor initialisiert das EEPROM-System und lädt die Strukturvariable aus dem EEPROM-Bereich des Flash. Die Methode save() überträgt die Variablen-Inhalte zurück ins Flash.
class EMPersistenceClass {
float startValue = -1; // Gesamtverbrauchwert des PZEM, als die Mesung gestartet wurde.
time_t startTime = 0; // Unix-Zeitstempel, an dem die Messung gestartet wurde.
RelaisState relaisState = RelaisState::on; // on, wenn der Verbraucher eingeschaltet ist.
EMPersistenceClass(); // Initialisiert das EEPROM und lädt das EEPROM in den internen Puffer
void save(); // Überträgt die Felder dieser Klasse in das EEPROM
};
Zur Zeit sind nur die Startwerte für einen Messzyklus hinterlegt:
Initial sind die Werte undefiniert. Definierte liegen erst nach dem ersten
Speichern vor.
Achtung: Die Standardbibliothek EEPROM kann nicht benutzt werden.
Es wird der Konstruktor und die Methode save() definiert und die Instanz EMPersistence für den Zugriff auf die Daten und Methoden angelegt. Sie entsprechenden i.W. dem in ESP8266 spezielle Klassen: EEPROM gezeigten Vorgehen. Lese- und Schreibzugriffe erfolgen beginnend mit der Adresse 0.
vEMPersistenceClass::EMPersistenceClass() {
// EEPROM initialiseren und Daten einlesen
EEPROM.begin(sizeof(EMPersistenceClass));
EEPROM.get(0, (*this));
}
void EMPersistenceClass::save() {
// Im EEPROM ablegen
EEPROM.put(0, (*this));
EEPROM.commit();
}
EMPersistenceClass EMPersistence;
Als Zeitquelle fungiert der in ESP8266 UrsTime: Immer zur rechten Zeit beschriebene Variante eines NTP-Clients. Als NTP-Server sind ntp1.t-online.de und time.nist.gov vorgesehen.
Die Signal-LED ist eine WS2812. Diese kann mit der Adafruit-Neopixel-Biblothek betrieben werden (s. The Magic of NeoPixels). Einige der Parameter, die bei den Bibliotheksmethoden angegeben werden müssen, sind jedoch fix. Um die Benutzung zu vereinfachen kapselt die Klasse SignalLedClass eine Instanz der Adafruit_NeoPixel-Klasse.
Die Klasse SignalLedColors definiert die Farben für die Anzeige der verschiedenen Systemzustände. Dies erlaubt die Veränderung der Farbcodes an zentraler Stelle.
Dem Konstruktor der Klasse SignalLedClass wird die Pin-Nr. übermittelt, an die das WS2812-Modul angeschlossen ist. mit der Methode begin() wird die Kontrolle über den Pin übernommen und optional die Helligkeit eingestellt. Die LED ist sehr hell, so dass die Voreinstellung für die Helligkeit 32 ist.
Die Methoden setPixelColor() und setBrightness() erlauben das Einstellen der Farbe und der Helligkeit. Für setPixelColor() gibt es eine Variante, die vordefinierte Farbcodes der Klasse SignalLedColors entgegennimmt.
Methode | Bedeutung | Anmerkung |
---|---|---|
SignalLedClass (uint8_t p) | Initialisiert eine neue Instanz von SignalLedClass. p: Pin, an den der Dateneingang des WS2812-Moduls angeschlossen ist. |
|
void begin ( uint8_t br = 64, uint8_t flashBr = 128, uint32_t duration = 100, uint32_t interval = 5000) |
Übernimmt die Kontrolle über den Pin und konfiguriert das Signallicht:
|
Ruft Adafruit_NeoPixel::begin() auf. |
void setColor ( SignalLedColors c) |
Stellt eine der in SignalLedColors definierten Farben ein. | Die Farbe wird direkt angezeigt; spätestens nach Beendigung des Blitzes. |
void setBrightness ( uint8_t br) |
Stellt die Helligkeit ein. | Die Helligkeit wird unmittelbar angepasst; spätestens nach Beendigung des Blitzes. |
void handle() | Erledigt das Blitzen. |
Folgende Zustände/Farben sind definiert:
Zustand | Farbenname | Wert | |||
---|---|---|---|---|---|
WLAN | Station | Verbraucher | |||
off | off | off | connectingOff | weiß | |
off | off | on | connectingOn | gelb | |
off | on | off | apOff | magenta | |
off | on | on | apOn | blau | |
on | off | off | wifiOff | rot | |
on | off | on | wifiOn | grün | |
Fehler | error | violett bei voller Helligkeit |
Das WS2812-Modul verändert seine Farbe erst dann, wenn es entsprechende Befehle erhält. Um anzuzeigen, dass der ESP8266 noch funktioniert wird etwa alle 5 Sekunden die Helligkeit der LED erhöht. Sie blitzt kurz auf.
Zur Protokollierung des Firmware-Updates per OTA sind folgende Farben definiert:
Zustand | Wert | ||
---|---|---|---|
OtaStart | cyan | Zur Verdeutlichung, dass OTA in Gange ist, wird die Helligkeit der LED so hoch wie möglich eingestellt. | |
OtaEnd | weiß | ||
OtaError | violett |
Auch das Relais zum An- und Anschalten des Verbrauchers wurde in eine Klasse (RelaisClass) gepackt und der Zugriff über eine zentral definierte Instanz ermöglicht. Grund: Verwaltung eines Event-Handler der beim Umschalten aktiviert wird. Man kann so den Verbraucher an verschieden stellen schalten und zentral darauf reagieren.
Auf das Relais kann über die Instanz Relais der Klasse RelaisClass zugegriffen werden. Dem Konstruktor wird die Nummer des Pins übergeben, an den das Relais angeschlossen ist. Die Methode begin() übernimmt die Kontrolle über den Pin und schaltet ihn gemäß der übergebenen Wert.
Über die Methode setState() kann der Relais-Zustand geändert werden. Um hier eindeutige Variablennamen zu haben wurden die möglichen Zustandsangaben in der Enumeration RelaisState festgelegt.
Über die Methode onRelaisStateChanged kann eine Callback-Methode
registriert werden, die immer dann aufgerufen wird, wenn sich der Zustand des Relais ändert. Der
Typ dieser Methode ist RelaisStateChangedHandlerFunction. Eine
Methode der Art void RelaisStateChangedHandler(RelaisState s)
kann hier registriert
werden.
Methode | Bedeutung | Anmerkung |
---|---|---|
RelaisClass(uint8_t Pin); | Initialisiert eine neue Instanz von RelaisClass. p: Pin, an den das Relais ist. |
|
void begin (RelaisState state) | Übernimmt die Kontrolle über den Pin und schaltet das Relais gemäß Parameter 'state'. | |
void setState (RelaisState state) | Schaltet das Relais gemäß der Angabe in state. | Die Angaben RelaisState::on (schaltet den Verbraucher ein) und RelaisState::off (schaltet den Verbraucher aus) sind möglich. |
RelaisState getState() | Ruft die aktuelle Einstellung des Verbrauchers ab. | Die Angaben RelaisState::on (schaltet den Verbraucher ein) und RelaisState::off (schaltet den Verbraucher aus) sind möglich. |
bool isOn() | Ruft einen Wert ab, der angibt, ob der Verbraucher eingeschaltet ist. | Liefert true, wenn der Verbraucher eingeschaltet ist. |
void onRelaisStateChanged (RelaisStateChangedHandlerFunction f) | Registriert die Callback-Funktion f. | RelaisStateChangedHandlerFunction ist definiert als
typedef std::function<void(RelaisState state)> RelaissStateChangedHandlerFunction .f wird aufgerufen, wenn sich der Zustand des Relais geändert hat. state enthält den aktuellen Zustand des Relais. |