home    Inhaltsverzeichnis
erste Version am 03.12.2016
letzte Änderung am 11.12.2016

Temperatur- und Helligkeits-Logger - Seite 2


Test-Aufbau

Bisher sieht der Test-Aufbau noch so aus:
Foto vom Test-Aufbau mit ArduinoUno,
        ESP8266, DS3231- und AT24C32-Modul sowie 2xDS18B20 und 1xGL5528

Beim RTC/EEPROM-Modul habe ich mich gegen den Einsatz der DS3231_Simple-Library entschieden. Für den DS3231 und den AT24C32 wird nun nur Wire.h benötigt.
Der DS18B20 braucht OneWire.h und der ESP8266 SoftwareSerial.h.

Die hier mal auf die schnelle zusammen-kopierte ESP8266-Klasse habe ich noch deutlich umgebaut, um effektiver mit der Übertragung von Binär-Daten umgehen zu können. Wobei ich mit Binär-Daten etwas meine, was kein Null-terminierter String ist - also immer auch eine Längen-Angabe braucht.
Dementsprechend muss der Python-SocketServer ebenfalls deutlich umgebaut werden.
Auf gegenseitige Authentifizierung zwischen ATmega328 und Python-Script werde ich diesmal verzichten. Der maximale Schaden, der durch Schabernack entstehen könnte, wäre schließlich nur eine falsche Statistik bzgl. Temperatur und/oder Helligkeit.

Beim Testen der Schreib-Funktionen ins AT24C32-EEPROM hat es mich etwas Zeit gekostet, zu realisieren, dass der Chip immer nur 32 Byte lange fixe Blöcke bei einem Schreib-Vorgang zulässt. Die Blöcke sind nicht nur in ihrer Länge vorgegeben, sondern auch in ihrer absoluten Position. Will heißen: wenn ich z.B. die zwei Byte an den Positionen 0x1F und 0x20 schreiben will, geht das nur mit zwei einzelnen Schreib-Anforderungen an den Chip.
Der erste Block geht von 0x00 bis 0x1F, der zweite von 0x20 bis 0x3F usw.
Die implementierte Lösung ist nun, dass zum einen immer nur 16 Byte in einem Rutsch geschrieben werden und zum anderen alle Start-Adressen hexadezimal mit einer Null enden (also immer ganzzahlig durch 16 teilbar sind).

Die angedachte delay()-Funktion, die intern mit SLEEP_MODE_PWR_DOWN arbeitet, ist relativ trivial:
//                   0:16ms    1:32ms    2:64ms    3:128ms   4:256ms   5:512ms   6:1024ms  7:2048ms  8:4096ms  9:8192ms
byte sleep_times[]={ 0b000000, 0b000001, 0b000010, 0b000011, 0b000100, 0b000101, 0b000110, 0b000111, 0b100000, 0b100001 };

/* -------------------------------------------------------------------
 *  Wartet "ms" Millisekunden per Tiefschlaf.
 *  Bei "ms" <=0 wird nicht geschlafen.
 */
void deepSleep(int ms) {
  int start_at=8192, idx=9;               // bei der längsten Zeit anfangen

  while(ms>0 && idx>=0) {                 // solange warten noch nötig und möglich ist
    while(ms>=start_at) {                 // solange Wartezeit noch passt
      sleep_PWR_DOWN(sleep_times[idx]);   // aktuelle Zeit warten
      ms-=start_at;                       // Rest-Wartezeit korrigieren
    }
    start_at>>=1;                         // nächst-kleinere Zeit einstellen
    idx--;                                // und Index entsprechend setzen
  }
}


Die EEPROM-Speicherverwaltung sieht so aus, dass pro Stunde ein Datensatz geschrieben wird und jede Stunde ihre feste Adresse innerhalb der 5KB EEPROM-Speicher besitzt. Somit wird ein alter Datensatz immer nach 24 Stunden mit einem neuen Datensatz überschrieben.
Bei zwei Sensor-Paaren reicht der Platz nur für 12 Stunden. Hier wird jeder Datensatz folglich bereits nach 12 Stunden überschrieben. Unabhängig davon kann zu jeder beliebigen Zeit (innerhalb der 12 oder 24 Stunden) mit dem Python-Script synchronisiert werden.
Jeder Datenblock enthält einen Datum/Zeit-Stempel, über den sein Gültigkeitsbereich bestimmt werden kann. Weiterhin wird pro Datensatz auch eine Prüfsumme abgelegt, über die ein defekter EEPROM-Bereich detektierbar wird.
Ich bin noch unentschlossen, ob ich eine Ausweich-Logik einbauen soll, die einen Datenblock dann auf eine andere Adresse umleitet, wenn ein EEPROM-Bereich als defekt erkannt wurde. Platz für drei Ausweich-Blöcke wäre da - bzw. für einen, wenn die Daten von zwei Sensor-Paaren gespeichert werden müssen.

Eben hat es mir einige zusätzliche graue Haare beschert, bis ich schließlich begriffen hatte, dass, wenn ich mit C eine Prüfsumme dadurch bilde, 184 Byte vom Typ "char" auf einen "unsigned int" aufzusummieren, nicht das gleiche Ergebnis rauskommt, als wenn ich mit Python die selben Werte per ord()-Funktion aufsummiere und die Summe final per "&0xFFFF" auf 16 Bit zurechtschneide. Natürlich muss ich auf der C-Seite vor dem Aufsummieren nach "byte" alias "unsigned char" cast'en, damit das klappt.
Aber....ist ja alles nur (bei mir selbst) geklaut....die neue Funktion sieht so aus:
unsigned int ESP8266_PowerSave::getBinChecksum(char *buf, int len) {
  unsigned int cs=0;
  for(; len>0; len--)  {
    cs+=(unsigned char)*buf++;
  }
  return(cs);
}

Die alte von der Rollladensteuerung so (und sie funktioniert bizarrerweise problemlos):
char *getChecksum(char *strg) {
  static char buf[5];
  int cs=0;
  for(char *ptr=strg; *ptr!='\0'; ptr++)  {
    cs+=*ptr;
  }
  snprintf(buf, 5, "%04x", cs);
  return(buf);
}

Obwohl die Python-Seite in beiden Fällen quasi identisch aussieht:
def getChecksum(strg):
  cs=0
  for c in strg:
    cs+=ord(c)
  return("%0.4x"%cs)

def getBinChecksum(strg):
  cs=0
  for c in strg:
    cs+=ord(c)
  return(cs&0xFFFF)

Sehr sonderbar. Aber eindeutig: sobald ich den cast nach unsigned rausnehme, gibts falsche Prüfsummen.
Gemäß diesem Thread muss es wohl daran liegen, dass ich auf der C-Seite unterschiedliche Header-Files lade und einmal <limits.h> mit-geladen wird und das andere mal nicht. Egal. Mit dem expliziten cast auf unsigned ist es auf jeden Fall sicherer und somit sauberer.
Merke: "char" alleine kann signed oder unsigned sein. Böse Falle.

Erste Daten von der Schaltung

Vorhin, kurz vor Feierabend, habe ich die Schaltung mit Strom versorgt und meinen daran angepassten SocketServer gestartet.
Zwei Stunden mit sinnvollen Daten wurden schon mal geliefert:
0000  10 9b 08 00 47 01 20 47 f1 1f 47 f1 1f 47 d1 1f  ....G. G..G..G..
0010  47 b1 1f 47 81 1b 47 01 04 46 11 04 46 11 04 46  G..G..G..F..F..F
0020  21 04 45 21 04 45 21 04 45 21 04 45 21 04 44 21  !.E!.E!.E!.E!.D!
0030  04 44 01 03 43 41 01 43 41 01 43 41 01 43 41 01  .D..CA.CA.CA.CA.
0040  42 41 01 42 41 01 42 41 01 42 41 01 41 41 01 41  BA.BA.BA.BA.AA.A
0050  41 01 41 41 01 40 41 01 40 41 01 40 41 01 40 51  A.AA.@A.@A.@A.@Q
0060  01 40 51 01 3f 41 01 3f 41 01 3f 41 01 3f 41 01  .@Q.?A.?A.?A.?A.
0070  3f 51 01 3f f1 00 3f f1 00 3e f1 00 3e f1 00 3e  ?Q.?..?..>..>..>
0080  01 09 3e e1 1f 3f 91 1f 40 a1 1f 40 91 1f 40 71  ..>..?..@..@..@q
0090  1f 41 71 1f 41 51 1f 42 41 1f 42 21 1f 42 11 1f  .Aq.AQ.BA.B!.B..
00A0  42 01 1f 43 f1 1e 43 e1 1e 43 e1 1e 43 21 18 42  B..C..C..C..C!.B
00B0  f1 00 42 f1 00 42 f1 00 bc 2b                    ..B..B...+

[(20.4, 512), (20.4, 511), (20.4, 511), (20.4, 509), (20.4, 507), (20.4, 440), (20.4, 64), (20.4, 65), (20.4, 65), (20.4, 66), (20.3, 66), (20.3, 66), (20.3, 66), (20.3, 66), (20.3, 66), (20.3, 48), (20.2, 20), (20.2, 20), (20.2, 20), (20.2, 20), (20.1, 20), (20.1, 20), (20.1, 20), (20.1, 20), (20.1, 20), (20.1, 20), (20.1, 20), (20.0, 20), (20.0, 20), (20.0, 20), (20.0, 21), (20.0, 21), (19.9, 20), (19.9, 20), (19.9, 20), (19.9, 20), (19.9, 21), (19.9, 15), (19.9, 15), (19.9, 15), (19.9, 15), (19.9, 144), (19.9, 510), (19.9, 505), (20.0, 506), (20.0, 505), (20.0, 503), (20.1, 503), (20.1, 501), (20.1, 500), (20.1, 498), (20.1, 497), (20.1, 496), (20.2, 495), (20.2, 494), (20.2, 494), (20.2, 386), (20.1, 15), (20.1, 15), (20.1, 15)]

(16, 12, 6, 17, 0, 0)

Zuerst ein Hexdump des Datenblocks für eine Stunde, der quasi unverändert aus dem EEPROM zum Script geschickt wurde.
Darunter die 60 Werte des Sensor-Paars im Format (Temperatur, Helligkeit).
Und als letztes Datum+Zeit, in dem der Datenblock erfasst wurde.

Analog dazu die Folge-Stunde:
0000  10 1b 09 04 42 f1 00 41 f1 00 41 f1 00 41 f1 00  ....B..A..A..A..
0010  40 f1 00 40 f1 00 40 f1 00 40 f1 00 40 f1 00 40  @..@..@..@..@..@
0020  f1 00 40 f1 00 41 f1 00 41 f1 00 41 f1 00 41 f1  ..@..A..A..A..A.
0030  00 41 f1 00 41 f1 00 41 f1 00 41 f1 00 41 f1 00  .A..A..A..A..A..
0040  41 f1 00 41 f1 00 41 f1 00 41 f1 00 41 f1 00 41  A..A..A..A..A..A
0050  f1 00 41 f1 00 41 f1 00 41 f1 00 41 f1 00 41 f1  ..A..A..A..A..A.
0060  00 41 f1 00 41 f1 00 41 f1 00 40 f1 00 40 f1 00  .A..A..A..@..@..
0070  40 f1 00 40 f1 00 40 11 19 3e 51 21 33 71 21 2d  @..@..@..>Q!3q!-
0080  51 21 25 31 21 20 31 21 1a 41 21 17 21 21 14 11  Q!%1! 1!.A!.!!..
0090  21 11 21 21 10 01 21 0f d1 20 0e a1 20 0c b1 20  !.!!..!.. .. ..
00A0  0a b1 20 0c d1 20 0e a1 20 0c b1 20 0c 91 20 0b  .. .. .. .. .. .
00B0  91 20 0d 61 20 0d 71 20 2e 3c                    . .a .q .<

[(20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.0, 15), (20.0, 15), (20.0, 15), (20.0, 15), (20.0, 15), (20.0, 15), (20.0, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.1, 15), (20.0, 15), (20.0, 15), (20.0, 15), (20.0, 15), (20.0, 401), (19.9, 533), (19.2, 535), (18.8, 533), (18.3, 531), (18.0, 531), (17.6, 532), (17.4, 530), (17.3, 529), (17.1, 530), (17.0, 528), (16.9, 525), (16.9, 522), (16.8, 523), (16.6, 523), (16.8, 525), (16.9, 522), (16.8, 523), (16.8, 521), (16.7, 521), (16.8, 518), (16.8, 519)]

(16, 12, 6, 18, 0, 1)

Wie man sieht...um 17:05 Uhr (20.4, 440) habe ich das Licht in meinem Büro ausgemacht.
Der Monitor war aber noch an. Der hat sich um 17:15 Uhr (20.3, 48) abgeschaltet.
Um 17:41 Uhr (19.9, 144) kam dann Sohnemann #1 ins Büro - sehr wahrscheinlich, um eine zu rauchen.
Da blieb er bis 17:56 Uhr (20.2, 386) und hat sich offenbar nicht an meinem gesperrten Rechner vergangen (weil der Monitor nicht wieder hell, sondern es schlagartig [fast] ganz dunkel, geworden ist).
Ab 18:38 Uhr (20.0, 401) war das Mathe-Pauken mit Sohnemann #2 beendet, ich bin wieder ins Büro gegangen, habe Licht eingeschaltet, erstmal gelüftet (sinkende Temperatur!) .... und selbst eine geraucht. Um 19:00 Uhr wurden die Daten an den SocketServer übermittelt.
Direkt danach ist dieses Kapitel entstanden und es wurde kontinuierlich kälter, weil das Fenster offen blieb.

Datenbank-Design

Nun stellt sich die Frage, wie die ankommenden Daten in der Datenbank organisiert werden sollen.
Die Entitäten sind:
Die Host-Tabelle wird nur ein paar Einträge haben.
Host
Key
Hostname
IP-Adresse
Sensor-Paare
Aufstellungeort
Bemerkungen
1
ESP8266-2
192.168.42.102
1 Küche

2
ESP8266-3
192.168.42.103 2
Büro


Ebenso die Tabelle der Sensor-Paare:
Sensor
Key
Host(ref)
Sensor
Aufstellungsort
1
1
1
Küche
2
2
1
Büro (innen)
3
2
2
Büro (außen)


Die Uhrzeiten wiederholen sich täglich, bestehen aber nur aus 24*60=1440 unterschiedlichen Werten. Da macht es aus Speicherplatz-Gründen schon mal wenig Sinn, sie in eine eigene Tabelle auszulagern. Die Länge des Fremdschlüssels dürfte ähnlich lang sein, wie die Uhrzeit selbst.

Beim Datum sieht es eigentlich analog aus.
Aber irgendwie widerstrebt es mir, einfach eine Tabelle "Dump" anzulegen, die folgenden Aufbau hätte:
Dump
Key
Sensor(ref)
Jahr
Monat
Tag
Stunde
Minute
Temperatur
Helligkeit
1
1
2016
12
7
17
0
20
500
2
1
2016
12
7
17
1
20
505
3
1
2016
12
7
17
2
20.3
503
4
2
2016
12
7
18
0
21
550
5
3
2016
12
7
18
0
8,7
100
6
2
2016
12
7
18
1
21
551
7
3
2016
12
7
18
1
8,7
100


Eine Alternative wäre, das Datum auszulagern.
Datum
Key
Jahr
Monat
Tag
1
2016
12
7

Zeit/Messwert
Key
Sensor(ref)
Datum(ref)
Stunde
Minute
Temperatur
Helligkeit
1
1
1
17
0
20
500
2
1
1
17
1
20
505
3
1
1
17
2
20.3
503
4
2
1
18
0
21
400
5
3
1
18
0
8,7
100
6
2
1
18
1
21
401
7
3
1
18
1
8,7
100

Oder sogar noch die Stunde mit in die Datums-Tabelle aufnehmen.


Hätte ich nun vor, hunderte Hosts zu haben, wäre vielleicht folgendes Konstrukt sinnvoll:

Datum
Key
Jahr
Monat
Tag
1
2016
12
7

Zeit
Key
Stunde
Minute
1
17
0
2
17
1
3
17
2
4
18
0
5
18
1

Datum/Zeit
Key
Datum(ref)
Zeit(ref)
1
1
1
2
1
2
3
1
3
4
1
4
5
1
5

Messwert
Key
Sensor(ref)
Datum/Zeit(ref)
Temperatur
Helligkeit
1
1
1
20
500
2
1
2
20
505
3
1
3
20.3
503
4
2
4
21
400
5
3
5
8,7
100
6
2
4
21
401
7
3
5
8,7
100

Hier würde quasi nur die Tabelle Datum leicht, dafür aber die Tabellen Datum/Zeit und Messwert zügig wachsen. Außerdem  bräuchte es garantiert einen Index auf ein paar Spalten, damit der Join noch performed. Dadurch wird dann der Insert ausgebremst und mehr Speicherplatz wird das in Summe wohl auch brauchen.
Warum soll man eigentlich Normalisieren.....eigentlich doch primär deswegen, um keine Update-Anomalien zu bekommen...wenn ich mich da richtig entsinne.
Und Updaten will ich nicht. Löschen ebenfalls nicht. Damit ist das doch per Definition ein simpler Dump.
Also baue ich das jetzt -ganz profan- mit den drei Tabellen Host, Sensor und Dump.

Obwohl...was ich wahrscheinlich mal machen will, wäre eine Standort-Änderung für eine Schaltung bzw. ein Sensor-Paar.
Das ist bisher ganz und gar nicht schön abbildbar.
Wäre "Aufstellungsort" statt eines Attributes von "Sensor" eine eigene Entität "Standort", könnten ein Messwert statt "Sensor" den "Standort" referenzieren.
Die Beziehung Host zu Sensor kann wohl als statisch angesehen werden, die Beziehung Sensor zu Standort wäre jedoch abhängig vom Datum. Aber weder Host noch Sensor kennen ihren Standort zum Zeitpunkt der Messwert-Aufnahme.
Somit kennt das Python-Programm beim Eintreffen der Messwerte davon erstmal nur Datum/Zeit, Sensor und Host.
Der Standort muss nachträglich (z.B. mit dem Firefox Addon SQLite Manager) eingepflegt werden können.
Ich brauche also noch die Relation: Sensor hatte zu Datum/Zeit folgenden Standort.
Damit könnte ich nachträglich einen Update machen, der einem Sensor ab einer bestimmten Datum/Zeit einen neuen Standort zuweist.
Vielleicht einfach mit einer weiteren Tabelle, die einem Standort einen Sensor und einen Datum-Bereich zuweist.
Bei diesem Ansatz dürfte ein View für (Datum/Zeit, Messwert, Standort) allerdings ziemlich tricky werden.
Oder gleich die Tabelle Sensor durch Standort ersetzen....wenn darin auch noch der Host referenziert wird:
Standort
Key
Host(ref)
Sensor
Standort
Jahr_von
Monat_von
Tag_von
Jahr_bis
Monat_bis
Tag_bis
1
1
1
Küche
2016
12
15
2017
1
31
2
2
1
Büro (innen)
2016
12
16



3
2
2
Büro (außen)
2016
12
16



4
1
1
Wohnzimmer
2017
2
1




Eine nachträgliche Standort-Korrektur sollte sich durch ein einfaches Update-Statement realisieren lassen, das in Dump allen Sätzen ab einem bestimmten Datum einen anderen Fremdschlüssel in Sensor(ref) bzw. ab sofort Standort(ref) zuweist.
Das wird jetzt sicher irgend eine Normalform (mit recht kleiner Nummer) verletzen....denn da können nun ja ganz wunderschöne Widersprüchlichkeiten bezüglich des Datums entstehen.
Der Insert in Dump müsste den Wert für den Fremdschlüssel Standort(ref) dadurch ermitteln, dass aus Standort der zum Datum passende Satz gesucht wird. Das hätte den netten Nebeneffekt, dass direkt der richtige Standort gewählt würde, wenn man den neuen Standort-Satz bereits vor dem Eintreffen der ersten Messwerte angelegt hat. Man könnte sich also den nachträglichen Update sparen.
Betrachtet man Standort.Key als fortlaufende Nummer, könnte einfach max(key) vom passenden Satz (bzgl. Host und Sensor) in Dump.Standort(ref) eingesetzt werden.
Dadurch wären die ganzen Datums-Spalten in Standort nur noch "zur Info". Und damit sähe Standort fast wieder genauso aus wie Sensor - mit dem kleinen Unterschied, dass jetzt Tupel aus (Host(ref), Sensor) mehrfach vorkommen dürften und beim Insert in Dump immer das neuste Tupel zu referenzieren wäre. Weiterhin muss die Spalte Host.Aufstellungsort weg.
Ebenfalls sollte es jetzt problemlos abbildbar sein, bei einer Schaltung die Anzahl der Sensor-Paare nachträglich zu ändern (beispielsweise dann, wenn der externe Sensor an einem neuen Standort nicht nutzbar ist). Dafür müsste dann die Spalte Host.Sensor-Paare weg - oder sie wäre als "aktueller Stand" zu betrachten.
Zu beachten wäre, dass beide Änderungen (Standort und Sensor-Anzahl) immer für eine volle Sync-Phase gelten würden.

Das war jetzt ja mal viel Text für wenig Änderung.....

Auf der nächsten Seite geht es weiter.