home     Inhaltsverzeichnis
erste Version am 24.12.2017
letzte Änderung am 27.12.2017

Logging-Device

Es soll eine kleine Schaltung mit ESP8266 (-01 oder -12) entstehen, die man an einen ATmega328 anschließen kann und die es dem ATmega328 damit ermöglicht, Log-Meldungen per WLAN an einen SocketServer zu übermitteln, ohne sich um sowas wie Verbindungsaufbau oder Timeouts kümmern zu müssen.
Das wichtigste Feature ist der CPU-Offload. Auf ATmega328-Seite soll man einfach Meldungen senden können. Die Laufzeit des Programms soll sich nicht verändern, weil z.B. erstmal die Verbindung zum WLAN hergestellt werden muss.

Würde ich den ESP8266-01 verwenden, hätte das den Vorteil, dass ich eine vorhandene Leiterplatte verwenden kann.
Idealerweise würde die Kommunikation auf ATmega328-Seite mittels Serial.print() erfolgen (also über die Pins D0 und D1).
Oder per SoftwareSerial. Damit bestünde freie Pin-Wahl. Und direkte Nutzbarkeit für mein Nachtlicht.

Besonders schön wäre es, zur Laufzeit erkennen zu können, ob der Adapter angeschlossen ist - um so ggf. die entsprechenden Log-Meldungen deaktivieren zu können und es damit ermöglichen, nur eine Sourcecode-Version pflegen zu müssen.
Auch sollten sich die Parameter WLAN-SSID, SocketServer-IP-Adresse und -PortNummer vom ATmega328 einstellen lassen. Diese Daten müssten im EEPROM abgelegt werden, um sie nicht bei jedem PowerOn des ESP-01 neu übergeben zu müssen.

Bisher habe ich die 01'er Module nur mit der vorinstallierten Firmware genutzt und über AT-Befehle angesprochen.
Daher brauche ich erstmal einen Adapter, mit dem ich das 01'er-Modul über mein ESP-12-Programmierboard flashen kann.
Die Pinbelegung des ESP-01 sieht so aus:
TX
GND

EN
GPIO2
Antenne
RST
GPIO0 (Ansicht von oben)
Vcc
RX

Der Adapter muss lediglich die 01'er Pins auf die gleichnamigen 12'er Pins legen.

Meine erste Machbarkeits-Studie sieht so aus:
Foto vom ersten Testaufbau
Der ArduinoUNO hängt am Laptop. Vom ArduinoUNO geht es über einen LevelShifter zum ESP-12-Board, das seine 5V aus einem Labornetzgerät bezieht. Auf dem Board steckt ein ESP-01 mittels Jumper-Kabel. Vom Board gehts per TTL-Konverter zum PC.
Auf dem ArduinoUNO läuft ein Mini-Sketch, das per SoftwareSerial alle fünf Sekunden ein println("INFO=selbst hallo welt esp") ausführt.
Der Sketch auf dem ESP-01 ist schon etwas komplexer - aber einfach aus der Firmware meiner Zirkulationspumpen-Steuerung zusammen-kopiert. Darüber wird mit einem SocketServer kommuniziert. Und der produziert dann fleißig Log-Einträge:
2017.12.25 13:50:23 Info: selbst hallo welt esp

2017.12.25 13:50:28 Info: selbst hallo welt esp

2017.12.25 13:50:33 Info: selbst hallo welt esp
Ergo....QED


Der nächste Schritt wäre nun, ein universelles Software-Modul für den ATmega328 zu erstellen.
Etwa mit folgenden Funktionen:
Andererseits reicht es für mein Nachtlicht erstmal, einfach nur Strings asynchron an den SocketServer senden zu können.
Also fange ich mal damit an.
Der ESP-01 soll möglichst viel buffern können - um dadurch mehr Zeit überbrücken zu können, bevor er die Daten beim SocketServer losgeworden ist.
Die Größe des Empfangs-Buffers von HardwareSerial lässt sich nicht anpassen. 0x80 = 128 Byte sind mir zu wenig.
Aber die ESP-Version von SoftwareSerial (in ~/.arduino15/packages/esp8266/hardware/esp8266/2.3.0/libraries/SoftwareSerial) kennt einen vierten Parameter "buffSize" im Konstruktor.
Dann muss man noch rausfinden, wie die Pins RX und TX angesprochen werden. Hier stehts: TX=GPIO1, RX=GPIO3.
Damit lautet der Aufruf: SoftwareSerial ser(3, 1, false, 4096);
Quäl-Tests haben weiterhin ergeben, dass die Baudrate zwischen ATmega328 und ESP-01 nicht sonderlich hoch sein darf, wenn keine Zeichen verloren gehen sollen. Mit 115200 ist der ATmega328 seine Daten zwar schnell los, aber der ESP-01 verschluckt dann gerne mal was, wenn er gerade anderweitig beschäftigt ist. Auch 38400 ist noch zu hoch. Aber mit 19200 Baud kommt jedes Byte beim SocketServer an.
Also nächster Aufruf: ser.begin(19200);
Damit stünde dann die gebufferte Verbindung zwischen ATmega328 und ESP-01.

Als kleinen Luxus baue ich mir gleich mal eine Sende/Empfangs-Funktion, mit der beliebige Datenblöcke (bis max. 64KB) übertragen werden können - und nicht nur Null-terminierte Strings.

Hier nun also das Sketch für den ESP-01:
#include <ESP8266WiFi.h>
#include <SoftwareSerial.h>

WiFiClient sockSrv_client;

// statt HardwareSerial wird SoftwareSerial genutzt, um die Buffer-Größe hochsetzen zu können
SoftwareSerial ser(3, 1, false, 4096); // GPIO1=TX, GPIO3=RX (https://arduino.stackexchange.com/a/29940/38968)

#define REC_BUF_SIZE 2048
uint8_t rec_buf[REC_BUF_SIZE];

int bad;

/* -------------------------------------------------------------------
* Stellt eine Verbindung zum WLAN her und liefert true, wenn es
* geklappt hat.
*/
int connectWLAN(void) {
int retries;

WiFi.forceSleepWake();
WiFi.begin("FreifunkWees01.2 (http://ffw)", "");
retries=20;
while(WiFi.status()!=WL_CONNECTED && retries>0) { // 10 Sek. lang probieren
delay(500);
retries--;
}
return(WiFi.status()==WL_CONNECTED);
}

/* -------------------------------------------------------------------
* Trennt die Verbindung zum WLAN.
*/
void disconnectWLAN(void) {
WiFi.disconnect();
delay(100); // dem disconnect etwas Zeit geben, bevor abgeschatet wird
WiFi.forceSleepBegin();
}

/* -------------------------------------------------------------------
* Stellt eine Verbindung zum übergebenen SocketServer her und
* liefert true, wenn es geklappt hat.
*/
int connectSocketServer(char *ipaddr, int portnr) {
return(sockSrv_client.connect(ipaddr, portnr));
}

/* -------------------------------------------------------------------
* Liefert true, wenn die Verbindung noch steht.
*/
int isConnectedToSocketServer(void) {
return(sockSrv_client.connected());
}

/* -------------------------------------------------------------------
* Trennt die Verbindung zum SocketServer.
*/
void disconnectSocketServer(void) {
sockSrv_client.stop();
}

/* -------------------------------------------------------------------
* Liefert eine 16 Bit lange Prüfsumme für den Speicherbereich ab
* "buf" in der Länge "len".
*/
uint16_t getBinChecksum(uint8_t *buf, uint16_t len) {
uint16_t cs=0;
for(; len>0; len--) {
cs+=*buf++;
}
return(cs);
}

/* -------------------------------------------------------------------
* Sendet den Inhalt des Speicherbereiches ab "buf" in der Länge
* "len" samt einer Prüfsumme an den SocketServer.
* Die Verbindung zum SocketServer muss offen sein.
*/
void sendSocketServer(uint8_t *buf, uint32_t len) {
uint8_t *buf_ptr;
uint16_t cs=getBinChecksum(buf, len);

buf_ptr=(uint8_t *)malloc(len+2);
if(buf_ptr!=NULL) {
memcpy(buf_ptr, buf, len);
*(buf_ptr+len)=cs&0xff;
*(buf_ptr+len+1)=(cs>>8)&0xff;
sockSrv_client.write((const uint8_t *)buf_ptr, len+2);
free(buf_ptr);
}
}

/* -------------------------------------------------------------------
* Empfängt bis zu "maxlen" Byte vom SocketServer und legt sie ab
* "buf" im Speicher ab. Passt die Checksum gemäß der letzten zwei
* Byte zu den davor liegenden Byte, wird die Länge der empfangenen
* Daten (ohne die letzten zwei Byte für die Checksum)
* zurückgeliefert. Ansonsten wird 0 geliefert.
* Die Verbindung zum SocketServer muss offen sein.
*/
uint32_t receiveSocketServer(uint8_t *buf, uint32_t maxlen, uint32_t timeout_ms=2000) {
uint32_t start_ms=millis();
uint32_t idx, len;
uint16_t cs;

while(sockSrv_client.available()==0 && (millis()-start_ms)<timeout_ms) {
delay(10);
}
idx=0;
while(sockSrv_client.available()>0 && idx<maxlen && (millis()-start_ms)<timeout_ms) {
buf[idx++]=sockSrv_client.read();
}
if(idx>=3 && (millis()-start_ms)<timeout_ms) {
// Mindestens ein Byte Nutzdaten empfangen und kein Timeout aufgetreten
len=idx-2;
cs=getBinChecksum(buf, len);
if(buf[len]==(uint8_t)cs&0xff && buf[len+1]==(uint8_t)(cs>>8)&0xff) {
buf[len]='\0'; // String draus machen...falls es keiner sein sollte
return(len);
} else {
#if DEBUG==1
// Serial.println("checksum fehler");
// Serial.println(len);
#endif
}
}
return(0);
}

/* -------------------------------------------------------------------
* Liefert true, wenn eine Verbindung zum SocketServer steht.
*/
int reconnectIfNeeded(char *host, int port) {
if(isConnectedToSocketServer()) { // wenn die Verbindung zum SocketServer steht
return(true); // ist alles gut
}
if(WiFi.status()!=WL_CONNECTED) { // wenn WLAN-Connect nicht mehr steht
if(connectWLAN()) { // im WLAN anmelden und wenns geklappt hat
return(connectSocketServer(host, port)); // beim SocketServer anmelden und Erfolgsstatus liefern
}
} else { // wenn WLAN-Connect steht
return(connectSocketServer(host, port)); // beim SocketServer anmelden und Erfolgsstatus liefern
}
return(false);
}

/* -------------------------------------------------------------------
*
*/
int receiveBlock(char *buf, int maxlen) {
unsigned int adr=0, len1, len2, cs;
unsigned char b, state=0;
char *ptr;

ptr=buf;
if(ser.available()>0) {
while(state!=7) {
while(ser.available()==0) { delay(1); }
b=ser.read();
if(state==6) { state=7; cs+=b; }
if(state==5) { state=6; cs=(b<<8); }
if(state==4) {
*ptr=b;
len2--;
maxlen--;
if(maxlen>=0) { maxlen--; ptr++; }
if(len2==0) { state=5; }
}
if(state==3) { state=4; len1+=b; len2=len1; }
if(state==2) { state=3; len1=(b<<8); }
if(state==1) { if(b==23) { state=2; } else { state=0; bad=1; } }
if(state==0 && b==42) { state=1; }
}
if(cs==getBinChecksum((uint8_t*)buf, len1)) {
return(len1);
} else {
bad=2;
}
}
return(0);
}

/* -------------------------------------------------------------------
*
*/
void setup() {
ser.begin(19200);
}

/* -------------------------------------------------------------------
*
*/
void loop() {
static int len;

bad=0;
len=receiveBlock((char*)rec_buf, REC_BUF_SIZE);
if(len>0) {
if(reconnectIfNeeded("192.168.42.99", 2627)) {
sendSocketServer(rec_buf, len);
receiveSocketServer(rec_buf, REC_BUF_SIZE, 2000);
}
}
if(bad>0) {
if(reconnectIfNeeded("192.168.42.99", 2627)) {
snprintf((char*)rec_buf, 100, "INFO=BAD=%d", bad);
sendSocketServer(rec_buf, len);
receiveSocketServer(rec_buf, REC_BUF_SIZE, 2000);
}
}
delay(1);
}

Das Test-Sketch für den ArduinoUNO besteht aus drei Dateien.
Erstmal das Headerfile LoggingDevice.h:
#ifndef LoggingDevice_h
#define LoggingDevice_h

#include <Arduino.h>
#include <SoftwareSerial.h>

class LoggingDevice {
public:
LoggingDevice(uint8_t rx, uint8_t tx, uint8_t power, uint8_t enable, long spd) : esp(rx, tx) {
rx_pin=rx;
tx_pin=tx;
power_pin=power;
enable_pin=enable;
speed=spd;
enabled=0;
pinMode(power_pin, OUTPUT);
pinMode(enable_pin, OUTPUT);
digitalWrite(power_pin, HIGH); // ESP ausschalten
};
void enable(void);
void disable(void);
void sendBuffer(char *buf, int len);
void sendString(char *buf);

private:
uint8_t power_pin;
uint8_t enable_pin;
uint8_t rx_pin;
uint8_t tx_pin;
long speed;
uint8_t enabled;

SoftwareSerial esp;
uint16_t getBinChecksum(uint8_t *buf, uint16_t len);
};

#endif

Dann LoggingDevice.cpp:
#include "LoggingDevice.h"

/* -------------------------------------------------------------------
* ESP-01 power on und enable
*/
void LoggingDevice::enable(void) {
if(!enabled) {
digitalWrite(power_pin, LOW); // ESP mit Strom versorgen
delay(10);
digitalWrite(enable_pin, HIGH); // ESP aktivieren
esp.begin(speed);
enabled=1;
esp.setTimeout(1000);
}
}

/* -------------------------------------------------------------------
* ESP-01 disable und power off
*/
void LoggingDevice::disable(void) {
digitalWrite(tx_pin, LOW); // keine Spannung am abgeschalteten ESP
digitalWrite(enable_pin, LOW); // ESP deaktivieren
delay(10);
digitalWrite(power_pin, HIGH); // Strom für ESP abschalten
enabled=0;
}

/* -------------------------------------------------------------------
* Sendet den Null-terminierten String "buf" an den ESP-01.
*/
void LoggingDevice::sendString(char *buf) {
sendBuffer(buf, strlen(buf)+1);
}

/* -------------------------------------------------------------------
*
*/
void LoggingDevice::sendBuffer(char *buf, int len) {
unsigned int i, cs;
cs=getBinChecksum((uint8_t*)buf, len);
esp.write(42);
esp.write(23);
esp.write((len>>8)&0xff);
esp.write(len&0xff);
for(i=0; i<len; i++) {
esp.write(*buf++);
}
esp.write((cs>>8)&0xff);
esp.write(cs&0xff);
}

/* -------------------------------------------------------------------
* Liefert eine 16 Bit lange Prüfsumme für den Speicherbereich ab
* "buf" in der Länge "len".
*/
uint16_t LoggingDevice::getBinChecksum(uint8_t *buf, uint16_t len) {
uint16_t cs=0;
for(; len>0; len--) {
cs+=*buf++;
}
return(cs);
}

Und schließlich das Hauptprogramm für den ArduinoUNO:
#include <SoftwareSerial.h>
#include "LoggingDevice.h"

#define ESP_POWER_PIN 7 // Stromversorgung für den 3.3V-Regler
#define ESP_EN_PIN 8 // Chip-Enable des ESP
#define RX_PIN_TO_ESP 9 // an diesem Pin wird vom ESP empfangen - also mit ESP-TX verbinden
#define TX_PIN_TO_ESP 10 // an diesem Pin wird zum ESP gesendet - also mit ESP-RX verbinden
#define ESP_SPEED 19200L // Baudrate zwischen ATmega328 und ESP-01

LoggingDevice log_dev(RX_PIN_TO_ESP, TX_PIN_TO_ESP, ESP_POWER_PIN, ESP_EN_PIN, ESP_SPEED);

char buf[100];

void setup() {
Serial.begin(115200);
log_dev.enable();
}

void loop() {
for(int i=0; i<40; i++) {
snprintf(buf, 100, "INFO=hallo %2d **********************************************************************", i);
log_dev.sendString(buf);
}
while(true) {
delay(100);
}
}

Das habe ich gerade in meine Nachtlicht-Firmware eingebaut und werde jetzt beobachten, wie das so auf die Dauer läuft.