home
erste Version am 31.01.2017
letzte Änderung am 30.03.2017

Temperatur- und Helligkeits-Logger V2

Hier will ich mal schauen, ob ich den Hardware-Aufwand meines ersten Temperatur- und Helligkeits-Loggers mit den hier dokumentierten Erfahrungen mit dem ESP-12E deutlich reduzieren kann.


Inhalt

Vorüberlegungen
erste Tests
die Leiterplatte oder auch "das Shield" ist fertig
Firmware V2.0
ein kleiner Umbau
Logger V2.1
Logger V2.1 / Logger-04
Zeitumstellung
Umstellung auf UTC (oder MEZ-only)


Vorüberlegungen

Die bisherigen Erkenntnisse waren, dass der eine ADC-Pin des ESP-12E vermeintlich unpräzise arbeitet. Und weil es nur einer ist, lassen sich damit außerdem keine zwei Sensor-Paare anschließen. Die Überwachung der eigenen Versorgungsspannung ist ebenfalls nicht möglich - aber auch nicht nötig, weil der Strom aus einem always-on USB-Port kommen soll.
Das RTC+EEPROM-Modul sollte hier überflüssig sein, wenn die internen Timer des ESP-12E genutzt werden und Datum+Zeit einmal täglich über WLAN korrigiert wird.

Was steht also noch aus....
Die Daten sollen auch hier jeweils 24 Stunden lang lokal gesammelt und im Block per WLAN an das Python-Script übertragen werden. Also braucht es eine Anpassung der Hauptschleife. Die wartet dann nicht mehr per deepSleep, sondern per Abfrage von millis(). Dafür war ich auf eine tägliche Abweichung von ca. einer Sekunde gekommen.
Wird alle fünf Sekunden ein Messwert erfasst, müssten (bei einer Sekunde Abweichung pro Tag) alle Messwerte der korrekten Minute zugeordnet werden können, wenn ich in der ersten Minute in Sekunde 2 starte.
02 07 12 17 22 27 32 37 42 47 52 57    62 67 ...

Die Programm-Laufzeiten müssten nicht berücksichtigt werden, weil millis() unabhängig davon weiterläuft.
Wenn nach 24 Stunden eine neue Zeit über WLAN geholt wird, bräuchte es evt. einen Mechanismus, damit erst ab Beginn der nächsten Minute mit der neuen Zeit gearbeitet wird.
Immer wenn die Zeit über WLAN geholt wurde, muss diese als Basis für die folgenden 24 Stunden gespeichert werden. Weiterhin muss der aktuelle Wert von millis() gespeichert werden, um damit die Differenz zur gemerkten Zeit bestimmen zu können.
Wobei sich da noch die Frage stellt, wann millis() überläuft. Laut Arduino-Reference nach 50 Tagen.
Andererseits ist das "wann" eigentlich egal. Irgendwann läuft millis() über. Und das muss korrekt behandelt werden.
Hier gibt es eine umfängliche Abhandlung dazu.
Die Idee dabei ist also, dass die Subtraktion einer großen Zahl von einer kleineren Zahl unsigned immer noch den Abstand der zwei Werte ergibt. Bei acht Bit etwa: 0x02 - 0xFD = 0x05 (FE, FF, 00, 01, 02)
Wie lässt sich das jetzt mit den zwei Problemfällen verwurschteln:
- täglich kommt eine neue / korrigierte Zeit über WLAN und
- alle 50 Tage läuft millis() über.

In setup() wird initial die Zeit geholt und in einzelnen Variablen für Jahr, Monat, Tag, Stunde, Minute, Sekunde gespeichert.
Zeitnah wird der aktuelle Wert von millis() ebenfalls gespeichert.

In loop() wird von millis() der gemerkte Basiswert abgezogen:
    (millis() - Basiswert) / 1000 = Sekunden seit letzter WLAN-Zeit.
Das sollte Überlauf-sicher sein, wenn alles unsigned long ist.
Alle fünf Sekunden soll ein Messwerte-Paar geholt werden.
Wenn die Minute um ist, soll der Mittelwert der Messwerte dieser Minute gebildet und gespeichert werden.
Um das Protokoll vom Socket-Server einzuhalten, braucht es Blöcke mit den Daten einer Stunde. Samt Timestamp im Header.

Jetzt gibts zwei Möglichkeiten.
  1. Es wird die ganzen 24 Stunden lang mit den Basis-Werten gerechnet.
  2. Immer wenn 1000 Millisekunden vergangen sind, werden die Zeit-Variablen unter Berücksichtigung des Überlaufes inkrementiert. Das stellt solange kein Problem dar, wie kein Monatswechsel beim Datum eintritt. Aber dafür sollte sich in time.h was finden lassen.
Oder ich baue einfach beides.
Heißt: alle 24 Stunden wird Datum+Zeit via WLAN geholt und mit dem aktuellen Wert von millis() gespeichert.
Parallel dazu gibt es eine Kopie von Datum+Zeit, die alle 1000ms inkrementiert wird. Damit hätte ich ohne viel Rechnerei meinen Timestamp für den Header der Stunden-Blöcke jederzeit aktuell verfügbar.

Was mir noch ein bisschen Sorgen bereitet, sind die Sprünge in der Zeit. Also wenn die Zeit über WLAN korrigiert wird und es schlagartig ein oder zwei Sekunden früher oder später als zuvor ist.
Es soll auch nicht alle 24 Stunden eine Daten-Lücke von einer Minute Länge geben.

Regulär wird Datum+Zeit immer am Ende der letzten Minute der festgelegten Sync-Stunde geholt. Aber es gibt auch den Sonder-Sync. Somit kann ein Zeitsprung jederzeit auftreten. Theoretisch ist es möglich, dass der Sonder-Sync am Ende einer Minute angefordert wird, dann drei bis vier Sekunden für den WLAN-Connect braucht, damit erst in der neuen Minute fertig ist und schließlich die Zeit in die vorige Minute zurück-korrigiert. Gruselig.
Das sollte ich unbedingt in eine separate Funktion auslagern, damit ich dafür nur einmal nachdenken muss.
So nach dem Motto: die Funktion liefert ein Bitmuster.
    Bit 0==1 -> Wert erfassen,
    Bit 1==1 -> Mittelwert bilden und speichern,
    Bit 2==1 -> neuen Stundenblock anlegen,
    Bit 3==1 -> Daten an Socket-Server senden.
Wahrscheinlich brauche ich dafür mehr als eine Funktion, denn ich muss da ja mindestens auch irgendwie eine neue WLAN-Zeit reinfüttern können. Also wirds eher eine Klasse - oder auch ein abstrakter Datentyp.


erste Tests

Mit einer Time-Library kann ich mir sicher einiges an Arbeit sparen.
Und wenn ich die Schaltung ohnehin über USB speisen will, kann ich eigentlich auch direkt die Billig-Module (WeMos-D1-mini) verwenden.
Deren Stückpreis lag bei 3,9€, das Fünferset mit den nackigen ESP-12E-Modulen hat 19€ gekostet. Somit ein Stückpreis von 3,8€.
Dafür haben die WeMos-D1-mini aber schon den 3,3V-Regler, die Lebenszeichen-LED und den Reset-Taster mit drauf. Ich müsste nur noch den DS18B20 und den LDR mit je einem Widerstand, sowie den Sonder-Sync-Taster anschließen. Vorzugsweise mit einer kleinen Aufsteck-Platine.
Genau so soll das sein. Wenn ich schon beim Stromverbrauch rumaase, dann muss das ja auch einen Vorteil haben.
Damit sähe die Verschaltung vom WeMos-Logger so aus (Bild anklicken für volle Größe):
Breadboard-Verschaltung von
        LoggerV2   Schaltplan für LoggerV2

Bei ersten Tests stellte sich heraus, dass einige der Datenstrukturen von Logger zu groß werden.
Natürlich war das zu erwarten, wenn die CPU hier 32 Bit statt der 8 Bit des ATmega328 hat.
Beim ESP-12E liefert sizeof(int) den Wert 4. Der ATmega328 wird stattdessen 2 liefern.
Der Datentyp long ist beim ESP-12E ebenfalls 4 Byte lang.
Einen 16-bittigen signed int bekommt man unter dem Namen int16_t, unsigned int entsprechend uint16_t.
Weitere stehen hier.

Allerdings habe ich bisher noch keinen Weg gefunden, meine Datenstruktur SensorData wieder auf eine Länge von drei Byte zu bekommen. Der Compiler macht immer gnadenlos vier Byte draus.
struct SensorData {       // zur Aufnahme eines Datums von einem Sensor-Paar (3 Byte)
  int16_t  temp : 12;     // 12 Bit vom DS18B20 via I2C
  uint16_t ldr  : 10;     // 10 Bit vom GL5528 via analogRead()
  uint8_t  bad  : 1;      // auf 1 für "temp-CRC hat nicht gepasst", auf 0 für "Daten legal"
};

Abgesehen von den 60*24=1440 unnötig belegten Byte im RAM macht das die Funktion zur Kommunikation mit dem Socket-Server aufwendiger. Schließlich erwartet der Socket-Server drei Byte lange Sensor-Daten, weshalb jedes vierte Byte aus dem Sensor-Daten-Array ausgeblendet werden müsste. Analog bei Bildung der Checksum.
So ein Mist. Vielleicht ist es einfacher, den Socket-Server insofern umzubauen, dass er in der IDENT-Meldung ein Architektur-Attribut annimmt und ggf. auf vier Byte lange Sensor-Daten umschaltet.
Oder ich mache die Bit-Frickelei selbst und lege SensorData als array of char der Länge 3 an.

Eine Nachfrage im Arduino-Forum hat recht schnell zur Lösung geführt.
Mit einem "struct SensorData {...}__attribute__((packed));" macht der Compiler daraus wieder drei Byte.


Ich baue den Ablauf erstmal ganz grob in Pseudocode, um was zum dran lang hangeln zu haben, in dem ich Fehler suchen kann.
Abweichend zu obigen Überlegungen will ich es zunächst mit der Time-Library durchdenken.
Die kostet zwar Platz, könnte die Sache aber deutlich einfacher machen. Und Platz habe ich jetzt ja reichlich - zumindest im Vergleich zum ATmega328.
setup
sensor_move=0
wenn Sonder-Sync-Taster gedrückt ist
    sensor_move=1  // beim nächsten Sync melden, dass vorab ein neuer location-Satz für diese Einheit angelegt werden soll
minute_array auf ungültig setzen
minute_array_idx=0  // Index in minute_array
hour_array auf ungültig setzen
day_array auf ungültig setzen
Zeit über WLAN holen
Zeit per Time.h / setTime() einstellen
wenn second()>52: warte, bis second()==0 // mindestens zwei Messwerte für einen Minuten-Mittelwert
cur=now()
done_second=-1  // "diese Sekunde wurde bereits bearbeitet" auf "nicht diese" setzen -> also sofort Messwert holen
cur_minute=minute(cur)  // "diese Minute wird gerade bearbeitet"
cur_hour=hour(cur)  // "diese Stunde wird gerade bearbeitet"
done_day=-1  // "dieser Tag wurde bereits bearbeitet" auf "nicht heute" setzen
done_sync_minute=-1  // "in dieser Minute wurde schon synchronisiert" auf "nicht diese Minute" setzen
hour_array.timestamp=timestamp(cur)  // für aktuelle Stunde initialisieren


loop
cur=now()  // aktuelle Zeit holen und für diesen Durchlauf statisch halten

// wenn es Zeit ist, einen Messwert zu holen und er nicht schon geholt wurde
wenn (second(cur)-2)%5==0 && done_second!=second(cur)
    Messwerte holen und ablegen in minute_array[minute_array_idx]
    minute_array_idx++
    Lebenszeichen-LED kurz blinken lassen
    done_second=second(cur)  // diese Sekunde wurde bereits bearbeitet

// wenn die Minute um ist
wenn cur_minute!=minute(cur)
    Mittelwerte über minute_array bilden und ablegen in hour_array.data[cur_minute]
    minute_array auf ungültig setzen
    minute_array_idx=0
    cur_minute=minute(cur)  // diese Minute wird gerade bearbeitet

// wenn die Stunde um ist
wenn cur_hour!=hour(cur)
    day_array[cur_hour]=hour_array  // abgelaufene Stunde umkopieren
    hour_array auf ungültig setzen
    hour_array.timestamp=timestamp(cur)  // für neue Stunde initialisieren
    cur_hour=hour(cur) // diese Stunde wird gerade bearbeitet

// wenn der Tag noch nicht bearbeitet wurde und die Sync-Stunde läuft
// und (für den Fall, das nicht erfolgreich gesendet werden konnte) die Minute ganzzahlig durch 5 teilbar ist
// und diese Minute hier noch nicht bearbeitet wurde
wenn done_day!=day(cur) && hour(cur)==SyncZeit && (minute(cur)%5)==0 && done_sync_minute!=minute(cur)
    day_array an Socket-Server senden und von dort die Zeit holen
    wenn erfolgreich gesendet wurde
        Zeit per Time.h / setTime() einstellen
        wenn hour(cur)!=hour()  // wenn die Zeit in die vorige Stunde zurück-korrigiert wurde
            wenn now()<cur  // wenn tatsächlich zurück-korrigiert wurde
                warte, bis minute()==0
        day_array auf ungültig setzen
        done_day=day(cur)  // heute kein Sync mehr
        done_sync_minute=-1  // beim nächsten Sync sofort Wiederholung erlauben
    sonst, wenn nicht erfolgreich gesendet wurde
        done_sync_minute=minute(cur)  // erst in 5 Minuten wieder einen Sync-Versuch starten

wenn Sonder-Sync-Taster gedrückt ist
    day_array an Socket-Server senden
    hour_array an Socket-Server senden

delay(100)

--------
Das Stunden-Array wird benötigt, um darüber einen WLAN-Ausfall von bis zu einer Stunde abfangen zu können.
Der Socket-Server liefert bei GET_TIME immer Normalzeit, sodass Zeitumstellung nicht berücksichtigt werden muss.
Die Kommunikation mit dem Socket-Server sollte maximal 30 Sekunden dauern, um keine Daten-Lücken zu erhalten.

Mögliche Probleme:
In der Sende-Bedingung kann das "hour(cur)==SyncZeit" durch "(hour(cur)==SyncZeit || done_sync_minute!=-1)" erweitert werden.
Wenn er damit aber in den nächsten Tag rutscht, würde zwar erstmal gesendet, danach jedoch der aktuelle Tag mit "done_day_g=day(cur)" geblockt werden. Also braucht es noch eine Variable done_sync_day, in der im Fehlerfall der beim nächsten erfolgreichen Sync zu blockende Tag vermerkt wird.
In die Bedingung zum "Messwert holen" sollte die Prüfung der Minute als "&& minute(cur)==cur_minute_g" aufgenommen werden.
Auf der nächsten Seite geht es weiter.