From 43ff3591d4de400a5b812b7dd34bd0d60f3112ca Mon Sep 17 00:00:00 2001 From: "de@itstall.de" <de@itstall.de> Date: Mon, 3 Feb 2020 18:04:00 +0100 Subject: [PATCH] Connection to backend established --- Wetterstation.HMI | Bin 3885042 -> 3885042 bytes wetterstation/PubSubClient.cpp | 653 ++++++++++++++++++++++++++++++++ wetterstation/PubSubClient.h | 173 +++++++++ wetterstation/settings.h | 49 +++ wetterstation/wetterstation.ino | 457 +++++++--------------- 5 files changed, 1022 insertions(+), 310 deletions(-) create mode 100644 wetterstation/PubSubClient.cpp create mode 100644 wetterstation/PubSubClient.h create mode 100644 wetterstation/settings.h diff --git a/Wetterstation.HMI b/Wetterstation.HMI index a60d230890cfb6a381f23448640624b82e6ac854..dfc54bf2550c1b2488f437b699c14cc2118bf681 100644 GIT binary patch delta 2319 zcmeH|TS!zv7{|X^J?chFr`*|GJ*lgCuU#!m(+Cq%>|&cmM5Q3*Q&bXmqb#DOt?VXC zPKZIpKA7~7tgvJcY7vB8427Pg3l{VuNQDy7<$u<*%+h1zL)5?z{_~yrzQg&?d~-ft z9%ml`+tSU7nD4AfHD7x%k0^?W1vcrSVy4Er1lL2pzBq1nwR5T~#ADw;TzuBW=bZfY zn9=m;AKXu=;K|)`r6Au%A_>p#mItOxYodLAlbXG;Mx)(G50A)%z!{DFIiUn4uq`%$ z6&E-gQCyCZ_bJpB5sA!hu1uw4A58k|6yJro#^}=A`*Tk_%wC6?0TYM<(Lez)L5Df^ z@#H`I;U3j9*E7!(@3Ho9SG(mvdf<}2t;H~Vj#$~w^G^0a4x6UUKZ662Y-qnibOm#L z6q<=H8hT(h(K$n}Rf*0TdN^Ljcg&aiU82-`lV#0s4x;l><P5#Qn?clS6iUF^m@stQ zVxmiiZp%k<koA^)Qi5M^@W~wU<|{80H=lB=(sZP$s!`OB@!RaYP&+v{Jz%2#3%3f_ zA#IVU?cg_s`?OtUOmv^pPKfWlB{AatBW@GT*SRWQjqq48bk*)=Hqkx7<74(U97aj8 zx|`>Up-!zr-00$pCF*$pYrPG0_cZZ+5bn8|bT2Jhx>pt>9eW1%!j5gV$oN=<dwZ;M z&1bk5cKZ}ME1WwkMQUGKNS_S3r)5f8@4&rq@(Q>Ii$mif+zS_4j}s7%zbumLca_Tc z_$s;nP`Qj#1yPGpv!!Ys(Pd+jxIyaMZlX-%91YdT?d;nsw{ydGxqh@3A0V)`n}heB z^0J+6qkJh#n$fL&I~BRLUfg0}K1cv|kO-231^HTXnx*SRtyRCy{?@i>o7uLRBiyRZ zCMIriMO3%gGnp3ZqS@8^Q>)ymM;n@)PUkPZBwD+8rRey<`@Owh4J-i3!Cr5Qz0Oi{ z<$zzm|Nr;dbnpJrK5LuVKKqTc;Ke^3I6xXm2Ma+4$OKs+8{~jokOvll#lQ(%ARiQf YC15Ei1k1p3Py~v>3Q!`ND@zal1m+zQk^lez delta 27753 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∾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 NG(l*+0gSZ+{|^;#N5%jE diff --git a/wetterstation/PubSubClient.cpp b/wetterstation/PubSubClient.cpp new file mode 100644 index 0000000..0fa420d --- /dev/null +++ b/wetterstation/PubSubClient.cpp @@ -0,0 +1,653 @@ +/* + PubSubClient.cpp - A simple client for MQTT. + Nick O'Leary + http://knolleary.net +*/ + +#include "PubSubClient.h" +#include "Arduino.h" + +PubSubClient::PubSubClient() { + this->_state = MQTT_DISCONNECTED; + this->_client = NULL; + this->stream = NULL; + setCallback(NULL); +} + +PubSubClient::PubSubClient(Client& client) { + this->_state = MQTT_DISCONNECTED; + setClient(client); + this->stream = NULL; +} + +PubSubClient::PubSubClient(IPAddress addr, uint16_t port, Client& client) { + this->_state = MQTT_DISCONNECTED; + setServer(addr, port); + setClient(client); + this->stream = NULL; +} +PubSubClient::PubSubClient(IPAddress addr, uint16_t port, Client& client, Stream& stream) { + this->_state = MQTT_DISCONNECTED; + setServer(addr,port); + setClient(client); + setStream(stream); +} +PubSubClient::PubSubClient(IPAddress addr, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client) { + this->_state = MQTT_DISCONNECTED; + setServer(addr, port); + setCallback(callback); + setClient(client); + this->stream = NULL; +} +PubSubClient::PubSubClient(IPAddress addr, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client, Stream& stream) { + this->_state = MQTT_DISCONNECTED; + setServer(addr,port); + setCallback(callback); + setClient(client); + setStream(stream); +} + +PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, Client& client) { + this->_state = MQTT_DISCONNECTED; + setServer(ip, port); + setClient(client); + this->stream = NULL; +} +PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, Client& client, Stream& stream) { + this->_state = MQTT_DISCONNECTED; + setServer(ip,port); + setClient(client); + setStream(stream); +} +PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client) { + this->_state = MQTT_DISCONNECTED; + setServer(ip, port); + setCallback(callback); + setClient(client); + this->stream = NULL; +} +PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client, Stream& stream) { + this->_state = MQTT_DISCONNECTED; + setServer(ip,port); + setCallback(callback); + setClient(client); + setStream(stream); +} + +PubSubClient::PubSubClient(const char* domain, uint16_t port, Client& client) { + this->_state = MQTT_DISCONNECTED; + setServer(domain,port); + setClient(client); + this->stream = NULL; +} +PubSubClient::PubSubClient(const char* domain, uint16_t port, Client& client, Stream& stream) { + this->_state = MQTT_DISCONNECTED; + setServer(domain,port); + setClient(client); + setStream(stream); +} +PubSubClient::PubSubClient(const char* domain, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client) { + this->_state = MQTT_DISCONNECTED; + setServer(domain,port); + setCallback(callback); + setClient(client); + this->stream = NULL; +} +PubSubClient::PubSubClient(const char* domain, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client, Stream& stream) { + this->_state = MQTT_DISCONNECTED; + setServer(domain,port); + setCallback(callback); + setClient(client); + setStream(stream); +} + +boolean PubSubClient::connect(const char *id) { + return connect(id,NULL,NULL,0,0,0,0,1); +} + +boolean PubSubClient::connect(const char *id, const char *user, const char *pass) { + return connect(id,user,pass,0,0,0,0,1); +} + +boolean PubSubClient::connect(const char *id, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage) { + return connect(id,NULL,NULL,willTopic,willQos,willRetain,willMessage,1); +} + +boolean PubSubClient::connect(const char *id, const char *user, const char *pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage) { + return connect(id,user,pass,willTopic,willQos,willRetain,willMessage,1); +} + +boolean PubSubClient::connect(const char *id, const char *user, const char *pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage, boolean cleanSession) { + if (!connected()) { + int result = 0; + + if (domain != NULL) { + result = _client->connect(this->domain, this->port); + } else { + result = _client->connect(this->ip, this->port); + } + if (result == 1) { + nextMsgId = 1; + // Leave room in the buffer for header and variable length field + uint16_t length = MQTT_MAX_HEADER_SIZE; + unsigned int j; + +#if MQTT_VERSION == MQTT_VERSION_3_1 + uint8_t d[9] = {0x00,0x06,'M','Q','I','s','d','p', MQTT_VERSION}; +#define MQTT_HEADER_VERSION_LENGTH 9 +#elif MQTT_VERSION == MQTT_VERSION_3_1_1 + uint8_t d[7] = {0x00,0x04,'M','Q','T','T',MQTT_VERSION}; +#define MQTT_HEADER_VERSION_LENGTH 7 +#endif + for (j = 0;j<MQTT_HEADER_VERSION_LENGTH;j++) { + buffer[length++] = d[j]; + } + + uint8_t v; + if (willTopic) { + v = 0x04|(willQos<<3)|(willRetain<<5); + } else { + v = 0x00; + } + if (cleanSession) { + v = v|0x02; + } + + if(user != NULL) { + v = v|0x80; + + if(pass != NULL) { + v = v|(0x80>>1); + } + } + + buffer[length++] = v; + + buffer[length++] = ((MQTT_KEEPALIVE) >> 8); + buffer[length++] = ((MQTT_KEEPALIVE) & 0xFF); + + CHECK_STRING_LENGTH(length,id) + length = writeString(id,buffer,length); + if (willTopic) { + CHECK_STRING_LENGTH(length,willTopic) + length = writeString(willTopic,buffer,length); + CHECK_STRING_LENGTH(length,willMessage) + length = writeString(willMessage,buffer,length); + } + + if(user != NULL) { + CHECK_STRING_LENGTH(length,user) + length = writeString(user,buffer,length); + if(pass != NULL) { + CHECK_STRING_LENGTH(length,pass) + length = writeString(pass,buffer,length); + } + } + + write(MQTTCONNECT,buffer,length-MQTT_MAX_HEADER_SIZE); + + lastInActivity = lastOutActivity = millis(); + + while (!_client->available()) { + unsigned long t = millis(); + if (t-lastInActivity >= ((int32_t) MQTT_SOCKET_TIMEOUT*1000UL)) { + _state = MQTT_CONNECTION_TIMEOUT; + _client->stop(); + return false; + } + } + uint8_t llen; + uint16_t len = readPacket(&llen); + + if (len == 4) { + if (buffer[3] == 0) { + lastInActivity = millis(); + pingOutstanding = false; + _state = MQTT_CONNECTED; + return true; + } else { + _state = buffer[3]; + } + } + _client->stop(); + } else { + _state = MQTT_CONNECT_FAILED; + } + return false; + } + return true; +} + +// reads a byte into result +boolean PubSubClient::readByte(uint8_t * result) { + uint32_t previousMillis = millis(); + while(!_client->available()) { + yield(); + uint32_t currentMillis = millis(); + if(currentMillis - previousMillis >= ((int32_t) MQTT_SOCKET_TIMEOUT * 1000)){ + return false; + } + } + *result = _client->read(); + return true; +} + +// reads a byte into result[*index] and increments index +boolean PubSubClient::readByte(uint8_t * result, uint16_t * index){ + uint16_t current_index = *index; + uint8_t * write_address = &(result[current_index]); + if(readByte(write_address)){ + *index = current_index + 1; + return true; + } + return false; +} + +uint16_t PubSubClient::readPacket(uint8_t* lengthLength) { + uint16_t len = 0; + if(!readByte(buffer, &len)) return 0; + bool isPublish = (buffer[0]&0xF0) == MQTTPUBLISH; + uint32_t multiplier = 1; + uint16_t length = 0; + uint8_t digit = 0; + uint16_t skip = 0; + uint8_t start = 0; + + do { + if (len == 5) { + // Invalid remaining length encoding - kill the connection + _state = MQTT_DISCONNECTED; + _client->stop(); + return 0; + } + if(!readByte(&digit)) return 0; + buffer[len++] = digit; + length += (digit & 127) * multiplier; + multiplier *= 128; + } while ((digit & 128) != 0); + *lengthLength = len-1; + + if (isPublish) { + // Read in topic length to calculate bytes to skip over for Stream writing + if(!readByte(buffer, &len)) return 0; + if(!readByte(buffer, &len)) return 0; + skip = (buffer[*lengthLength+1]<<8)+buffer[*lengthLength+2]; + start = 2; + if (buffer[0]&MQTTQOS1) { + // skip message id + skip += 2; + } + } + + for (uint16_t i = start;i<length;i++) { + if(!readByte(&digit)) return 0; + if (this->stream) { + if (isPublish && len-*lengthLength-2>skip) { + this->stream->write(digit); + } + } + if (len < MQTT_MAX_PACKET_SIZE) { + buffer[len] = digit; + } + len++; + } + + if (!this->stream && len > MQTT_MAX_PACKET_SIZE) { + len = 0; // This will cause the packet to be ignored. + } + + return len; +} + +boolean PubSubClient::loop() { + if (connected()) { + unsigned long t = millis(); + if ((t - lastInActivity > MQTT_KEEPALIVE*1000UL) || (t - lastOutActivity > MQTT_KEEPALIVE*1000UL)) { + if (pingOutstanding) { + this->_state = MQTT_CONNECTION_TIMEOUT; + _client->stop(); + return false; + } else { + buffer[0] = MQTTPINGREQ; + buffer[1] = 0; + _client->write(buffer,2); + lastOutActivity = t; + lastInActivity = t; + pingOutstanding = true; + } + } + if (_client->available()) { + uint8_t llen; + uint16_t len = readPacket(&llen); + uint16_t msgId = 0; + uint8_t *payload; + if (len > 0) { + lastInActivity = t; + uint8_t type = buffer[0]&0xF0; + if (type == MQTTPUBLISH) { + if (callback) { + uint16_t tl = (buffer[llen+1]<<8)+buffer[llen+2]; /* topic length in bytes */ + memmove(buffer+llen+2,buffer+llen+3,tl); /* move topic inside buffer 1 byte to front */ + buffer[llen+2+tl] = 0; /* end the topic as a 'C' string with \x00 */ + char *topic = (char*) buffer+llen+2; + // msgId only present for QOS>0 + if ((buffer[0]&0x06) == MQTTQOS1) { + msgId = (buffer[llen+3+tl]<<8)+buffer[llen+3+tl+1]; + payload = buffer+llen+3+tl+2; + callback(topic,payload,len-llen-3-tl-2); + + buffer[0] = MQTTPUBACK; + buffer[1] = 2; + buffer[2] = (msgId >> 8); + buffer[3] = (msgId & 0xFF); + _client->write(buffer,4); + lastOutActivity = t; + + } else { + payload = buffer+llen+3+tl; + callback(topic,payload,len-llen-3-tl); + } + } + } else if (type == MQTTPINGREQ) { + buffer[0] = MQTTPINGRESP; + buffer[1] = 0; + _client->write(buffer,2); + } else if (type == MQTTPINGRESP) { + pingOutstanding = false; + } + } else if (!connected()) { + // readPacket has closed the connection + return false; + } + } + return true; + } + return false; +} + +boolean PubSubClient::publish(const char* topic, const char* payload) { + return publish(topic,(const uint8_t*)payload,strlen(payload),false); +} + +boolean PubSubClient::publish(const char* topic, const char* payload, boolean retained) { + return publish(topic,(const uint8_t*)payload,strlen(payload),retained); +} + +boolean PubSubClient::publish(const char* topic, const uint8_t* payload, unsigned int plength) { + return publish(topic, payload, plength, false); +} + +boolean PubSubClient::publish(const char* topic, const uint8_t* payload, unsigned int plength, boolean retained) { + if (connected()) { + if (MQTT_MAX_PACKET_SIZE < MQTT_MAX_HEADER_SIZE + 2+strlen(topic) + plength) { + // Too long + return false; + } + // Leave room in the buffer for header and variable length field + uint16_t length = MQTT_MAX_HEADER_SIZE; + length = writeString(topic,buffer,length); + uint16_t i; + for (i=0;i<plength;i++) { + buffer[length++] = payload[i]; + } + uint8_t header = MQTTPUBLISH; + if (retained) { + header |= 1; + } + return write(header,buffer,length-MQTT_MAX_HEADER_SIZE); + } + return false; +} + +boolean PubSubClient::publish_P(const char* topic, const char* payload, boolean retained) { + return publish_P(topic, (const uint8_t*)payload, strlen(payload), retained); +} + +boolean PubSubClient::publish_P(const char* topic, const uint8_t* payload, unsigned int plength, boolean retained) { + uint8_t llen = 0; + uint8_t digit; + unsigned int rc = 0; + uint16_t tlen; + unsigned int pos = 0; + unsigned int i; + uint8_t header; + unsigned int len; + + if (!connected()) { + return false; + } + + tlen = strlen(topic); + + header = MQTTPUBLISH; + if (retained) { + header |= 1; + } + buffer[pos++] = header; + len = plength + 2 + tlen; + do { + digit = len % 128; + len = len / 128; + if (len > 0) { + digit |= 0x80; + } + buffer[pos++] = digit; + llen++; + } while(len>0); + + pos = writeString(topic,buffer,pos); + + rc += _client->write(buffer,pos); + + for (i=0;i<plength;i++) { + rc += _client->write((char)pgm_read_byte_near(payload + i)); + } + + lastOutActivity = millis(); + + return rc == tlen + 4 + plength; +} + +boolean PubSubClient::beginPublish(const char* topic, unsigned int plength, boolean retained) { + if (connected()) { + // Send the header and variable length field + uint16_t length = MQTT_MAX_HEADER_SIZE; + length = writeString(topic,buffer,length); + uint16_t i; + uint8_t header = MQTTPUBLISH; + if (retained) { + header |= 1; + } + size_t hlen = buildHeader(header, buffer, plength+length-MQTT_MAX_HEADER_SIZE); + uint16_t rc = _client->write(buffer+(MQTT_MAX_HEADER_SIZE-hlen),length-(MQTT_MAX_HEADER_SIZE-hlen)); + lastOutActivity = millis(); + return (rc == (length-(MQTT_MAX_HEADER_SIZE-hlen))); + } + return false; +} + +int PubSubClient::endPublish() { + return 1; +} + +size_t PubSubClient::write(uint8_t data) { + lastOutActivity = millis(); + return _client->write(data); +} + +size_t PubSubClient::write(const uint8_t *buffer, size_t size) { + lastOutActivity = millis(); + return _client->write(buffer,size); +} + +size_t PubSubClient::buildHeader(uint8_t header, uint8_t* buf, uint16_t length) { + uint8_t lenBuf[4]; + uint8_t llen = 0; + uint8_t digit; + uint8_t pos = 0; + uint16_t len = length; + do { + digit = len % 128; + len = len / 128; + if (len > 0) { + digit |= 0x80; + } + lenBuf[pos++] = digit; + llen++; + } while(len>0); + + buf[4-llen] = header; + for (int i=0;i<llen;i++) { + buf[MQTT_MAX_HEADER_SIZE-llen+i] = lenBuf[i]; + } + return llen+1; // Full header size is variable length bit plus the 1-byte fixed header +} + +boolean PubSubClient::write(uint8_t header, uint8_t* buf, uint16_t length) { + uint16_t rc; + uint8_t hlen = buildHeader(header, buf, length); + +#ifdef MQTT_MAX_TRANSFER_SIZE + uint8_t* writeBuf = buf+(MQTT_MAX_HEADER_SIZE-hlen); + uint16_t bytesRemaining = length+hlen; //Match the length type + uint8_t bytesToWrite; + boolean result = true; + while((bytesRemaining > 0) && result) { + bytesToWrite = (bytesRemaining > MQTT_MAX_TRANSFER_SIZE)?MQTT_MAX_TRANSFER_SIZE:bytesRemaining; + rc = _client->write(writeBuf,bytesToWrite); + result = (rc == bytesToWrite); + bytesRemaining -= rc; + writeBuf += rc; + } + return result; +#else + rc = _client->write(buf+(MQTT_MAX_HEADER_SIZE-hlen),length+hlen); + lastOutActivity = millis(); + return (rc == hlen+length); +#endif +} + +boolean PubSubClient::subscribe(const char* topic) { + return subscribe(topic, 0); +} + +boolean PubSubClient::subscribe(const char* topic, uint8_t qos) { + if (qos > 1) { + return false; + } + if (MQTT_MAX_PACKET_SIZE < 9 + strlen(topic)) { + // Too long + return false; + } + if (connected()) { + // Leave room in the buffer for header and variable length field + uint16_t length = MQTT_MAX_HEADER_SIZE; + nextMsgId++; + if (nextMsgId == 0) { + nextMsgId = 1; + } + buffer[length++] = (nextMsgId >> 8); + buffer[length++] = (nextMsgId & 0xFF); + length = writeString((char*)topic, buffer,length); + buffer[length++] = qos; + return write(MQTTSUBSCRIBE|MQTTQOS1,buffer,length-MQTT_MAX_HEADER_SIZE); + } + return false; +} + +boolean PubSubClient::unsubscribe(const char* topic) { + if (MQTT_MAX_PACKET_SIZE < 9 + strlen(topic)) { + // Too long + return false; + } + if (connected()) { + uint16_t length = MQTT_MAX_HEADER_SIZE; + nextMsgId++; + if (nextMsgId == 0) { + nextMsgId = 1; + } + buffer[length++] = (nextMsgId >> 8); + buffer[length++] = (nextMsgId & 0xFF); + length = writeString(topic, buffer,length); + return write(MQTTUNSUBSCRIBE|MQTTQOS1,buffer,length-MQTT_MAX_HEADER_SIZE); + } + return false; +} + +void PubSubClient::disconnect() { + buffer[0] = MQTTDISCONNECT; + buffer[1] = 0; + _client->write(buffer,2); + _state = MQTT_DISCONNECTED; + _client->flush(); + _client->stop(); + lastInActivity = lastOutActivity = millis(); +} + +uint16_t PubSubClient::writeString(const char* string, uint8_t* buf, uint16_t pos) { + const char* idp = string; + uint16_t i = 0; + pos += 2; + while (*idp) { + buf[pos++] = *idp++; + i++; + } + buf[pos-i-2] = (i >> 8); + buf[pos-i-1] = (i & 0xFF); + return pos; +} + + +boolean PubSubClient::connected() { + boolean rc; + if (_client == NULL ) { + rc = false; + } else { + rc = (int)_client->connected(); + if (!rc) { + if (this->_state == MQTT_CONNECTED) { + this->_state = MQTT_CONNECTION_LOST; + _client->flush(); + _client->stop(); + } + } + } + return rc; +} + +PubSubClient& PubSubClient::setServer(uint8_t * ip, uint16_t port) { + IPAddress addr(ip[0],ip[1],ip[2],ip[3]); + return setServer(addr,port); +} + +PubSubClient& PubSubClient::setServer(IPAddress ip, uint16_t port) { + this->ip = ip; + this->port = port; + this->domain = NULL; + return *this; +} + +PubSubClient& PubSubClient::setServer(const char * domain, uint16_t port) { + this->domain = domain; + this->port = port; + return *this; +} + +PubSubClient& PubSubClient::setCallback(MQTT_CALLBACK_SIGNATURE) { + this->callback = callback; + return *this; +} + +PubSubClient& PubSubClient::setClient(Client& client){ + this->_client = &client; + return *this; +} + +PubSubClient& PubSubClient::setStream(Stream& stream){ + this->stream = &stream; + return *this; +} + +int PubSubClient::state() { + return this->_state; +} diff --git a/wetterstation/PubSubClient.h b/wetterstation/PubSubClient.h new file mode 100644 index 0000000..cb7a36d --- /dev/null +++ b/wetterstation/PubSubClient.h @@ -0,0 +1,173 @@ +/* + PubSubClient.h - A simple client for MQTT. + Nick O'Leary + http://knolleary.net +*/ + +#ifndef PubSubClient_h +#define PubSubClient_h + +#include <Arduino.h> +#include "IPAddress.h" +#include "Client.h" +#include "Stream.h" + +#define MQTT_VERSION_3_1 3 +#define MQTT_VERSION_3_1_1 4 + +// MQTT_VERSION : Pick the version +//#define MQTT_VERSION MQTT_VERSION_3_1 +#ifndef MQTT_VERSION +#define MQTT_VERSION MQTT_VERSION_3_1_1 +#endif + +// MQTT_MAX_PACKET_SIZE : Maximum packet size +#ifndef MQTT_MAX_PACKET_SIZE +#define MQTT_MAX_PACKET_SIZE 1024 +#endif + +// MQTT_KEEPALIVE : keepAlive interval in Seconds +#ifndef MQTT_KEEPALIVE +#define MQTT_KEEPALIVE 15 +#endif + +// MQTT_SOCKET_TIMEOUT: socket timeout interval in Seconds +#ifndef MQTT_SOCKET_TIMEOUT +#define MQTT_SOCKET_TIMEOUT 15 +#endif + +// MQTT_MAX_TRANSFER_SIZE : limit how much data is passed to the network client +// in each write call. Needed for the Arduino Wifi Shield. Leave undefined to +// pass the entire MQTT packet in each write call. +//#define MQTT_MAX_TRANSFER_SIZE 80 + +// Possible values for client.state() +#define MQTT_CONNECTION_TIMEOUT -4 +#define MQTT_CONNECTION_LOST -3 +#define MQTT_CONNECT_FAILED -2 +#define MQTT_DISCONNECTED -1 +#define MQTT_CONNECTED 0 +#define MQTT_CONNECT_BAD_PROTOCOL 1 +#define MQTT_CONNECT_BAD_CLIENT_ID 2 +#define MQTT_CONNECT_UNAVAILABLE 3 +#define MQTT_CONNECT_BAD_CREDENTIALS 4 +#define MQTT_CONNECT_UNAUTHORIZED 5 + +#define MQTTCONNECT 1 << 4 // Client request to connect to Server +#define MQTTCONNACK 2 << 4 // Connect Acknowledgment +#define MQTTPUBLISH 3 << 4 // Publish message +#define MQTTPUBACK 4 << 4 // Publish Acknowledgment +#define MQTTPUBREC 5 << 4 // Publish Received (assured delivery part 1) +#define MQTTPUBREL 6 << 4 // Publish Release (assured delivery part 2) +#define MQTTPUBCOMP 7 << 4 // Publish Complete (assured delivery part 3) +#define MQTTSUBSCRIBE 8 << 4 // Client Subscribe request +#define MQTTSUBACK 9 << 4 // Subscribe Acknowledgment +#define MQTTUNSUBSCRIBE 10 << 4 // Client Unsubscribe request +#define MQTTUNSUBACK 11 << 4 // Unsubscribe Acknowledgment +#define MQTTPINGREQ 12 << 4 // PING Request +#define MQTTPINGRESP 13 << 4 // PING Response +#define MQTTDISCONNECT 14 << 4 // Client is Disconnecting +#define MQTTReserved 15 << 4 // Reserved + +#define MQTTQOS0 (0 << 1) +#define MQTTQOS1 (1 << 1) +#define MQTTQOS2 (2 << 1) + +// Maximum size of fixed header and variable length size header +#define MQTT_MAX_HEADER_SIZE 5 + +#if defined(ESP8266) || defined(ESP32) +#include <functional> +#define MQTT_CALLBACK_SIGNATURE std::function<void(char*, uint8_t*, unsigned int)> callback +#else +#define MQTT_CALLBACK_SIGNATURE void (*callback)(char*, uint8_t*, unsigned int) +#endif + +#define CHECK_STRING_LENGTH(l,s) if (l+2+strlen(s) > MQTT_MAX_PACKET_SIZE) {_client->stop();return false;} + +class PubSubClient : public Print { +private: + Client* _client; + uint8_t buffer[MQTT_MAX_PACKET_SIZE]; + uint16_t nextMsgId; + unsigned long lastOutActivity; + unsigned long lastInActivity; + bool pingOutstanding; + MQTT_CALLBACK_SIGNATURE; + uint16_t readPacket(uint8_t*); + boolean readByte(uint8_t * result); + boolean readByte(uint8_t * result, uint16_t * index); + boolean write(uint8_t header, uint8_t* buf, uint16_t length); + uint16_t writeString(const char* string, uint8_t* buf, uint16_t pos); + // Build up the header ready to send + // Returns the size of the header + // Note: the header is built at the end of the first MQTT_MAX_HEADER_SIZE bytes, so will start + // (MQTT_MAX_HEADER_SIZE - <returned size>) bytes into the buffer + size_t buildHeader(uint8_t header, uint8_t* buf, uint16_t length); + IPAddress ip; + const char* domain; + uint16_t port; + Stream* stream; + int _state; +public: + PubSubClient(); + PubSubClient(Client& client); + PubSubClient(IPAddress, uint16_t, Client& client); + PubSubClient(IPAddress, uint16_t, Client& client, Stream&); + PubSubClient(IPAddress, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client); + PubSubClient(IPAddress, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&); + PubSubClient(uint8_t *, uint16_t, Client& client); + PubSubClient(uint8_t *, uint16_t, Client& client, Stream&); + PubSubClient(uint8_t *, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client); + PubSubClient(uint8_t *, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&); + PubSubClient(const char*, uint16_t, Client& client); + PubSubClient(const char*, uint16_t, Client& client, Stream&); + PubSubClient(const char*, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client); + PubSubClient(const char*, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&); + + PubSubClient& setServer(IPAddress ip, uint16_t port); + PubSubClient& setServer(uint8_t * ip, uint16_t port); + PubSubClient& setServer(const char * domain, uint16_t port); + PubSubClient& setCallback(MQTT_CALLBACK_SIGNATURE); + PubSubClient& setClient(Client& client); + PubSubClient& setStream(Stream& stream); + + boolean connect(const char* id); + boolean connect(const char* id, const char* user, const char* pass); + boolean connect(const char* id, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage); + boolean connect(const char* id, const char* user, const char* pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage); + boolean connect(const char* id, const char* user, const char* pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage, boolean cleanSession); + void disconnect(); + boolean publish(const char* topic, const char* payload); + boolean publish(const char* topic, const char* payload, boolean retained); + boolean publish(const char* topic, const uint8_t * payload, unsigned int plength); + boolean publish(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained); + boolean publish_P(const char* topic, const char* payload, boolean retained); + boolean publish_P(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained); + // Start to publish a message. + // This API: + // beginPublish(...) + // one or more calls to write(...) + // endPublish() + // Allows for arbitrarily large payloads to be sent without them having to be copied into + // a new buffer and held in memory at one time + // Returns 1 if the message was started successfully, 0 if there was an error + boolean beginPublish(const char* topic, unsigned int plength, boolean retained); + // Finish off this publish message (started with beginPublish) + // Returns 1 if the packet was sent successfully, 0 if there was an error + int endPublish(); + // Write a single byte of payload (only to be used with beginPublish/endPublish) + virtual size_t write(uint8_t); + // Write size bytes from buffer into the payload (only to be used with beginPublish/endPublish) + // Returns the number of bytes written + virtual size_t write(const uint8_t *buffer, size_t size); + boolean subscribe(const char* topic); + boolean subscribe(const char* topic, uint8_t qos); + boolean unsubscribe(const char* topic); + boolean loop(); + boolean connected(); + int state(); +}; + + +#endif diff --git a/wetterstation/settings.h b/wetterstation/settings.h new file mode 100644 index 0000000..5200d5f --- /dev/null +++ b/wetterstation/settings.h @@ -0,0 +1,49 @@ +// +// settings for WiFi connection +// +char * ssid = "Sensors"; // WiFi SSID +char * password = "mySensorNetz"; // WiFi password + +// +// MQTT Server +// +String frontendId = String(ESP.getChipId()); // Set chipId as frontendId +const char* mqttHost = "MQTT_HOST"; // Adresse des MQTT Servers +const char* mqttUser = "MQTT_USER"; // MQTT Username +const char* mqttPass = "MQTT_PASS"; // MQTT Password +int mqttPort = 1883; // 1883 - Ist der Standard TCP Port + +// +// MQTT Topics to subscribe +// +const char* mqttTopic = String("/wetterstation/frontend/" + frontendId + "/weather").c_str(); +const char* mqttTopicBackend = "/wetterstation/backend/settings"; + +// +// Settings for Backend +// +String backendPlz = "YOUT_PLZ"; +String backendLngCode = "YOUT_LANDUAGE_CODE"; +String backendCity = "YOUR_CITY"; + +// +// settings +// +int startupDelay = 1000; // startup delay +int loopDelay = 1000; // main loop delay between sensor updates + +int maxForecastLoop = 10; // number of main loops before the forecast is refreshed, looping through all cities +int weatherForecastLoop = 0; +int weatherForecastLoopInc = 1; + +int displayStartupDimValue = 30; // startup display backlight level +int displayDimValue = 150; // main display backlight level +int displayDimStep = 1; // dim step +int dimStartupDelay = 5; // delay for fading in +int dimPageDelay = 0; // delay for fading between pages + +const char* ntpServerName = "de.pool.ntp.org"; // server pool +const long timeZoneoffsetGMT = 3600; // offset from Greenwich Meridan Time +boolean DST = false; // daylight saving time +int timeServerDelay = 1000; // delay for the time server to reply +int timeServerRefreshDelay = 3600; diff --git a/wetterstation/wetterstation.ino b/wetterstation/wetterstation.ino index 85d42c8..a3e3ed1 100644 --- a/wetterstation/wetterstation.ino +++ b/wetterstation/wetterstation.ino @@ -15,7 +15,6 @@ contact: dev@itstall.de **********************************************************************/ - //#define DEBUG #define DEBUG_NO_PAGE_FADE #define DEBUG_NO_TOUCH @@ -27,100 +26,39 @@ #include <Wire.h> #include <SPI.h> // I2C library #include <ArduinoJson.h> // https://github.com/bblanchon/ArduinoJson +#include <NTPClient.h> #include <ESP8266WiFi.h> // WiFi library #include <WiFiUdp.h> // Udp library #include <TimeLib.h> // Time library +#include "PubSubClient.h" // http://knolleary.net/arduino-client-for-mqtt/ +#include "settings.h" extern "C" { #include "user_interface.h" } -// -// settings for WiFi connection -// -char * ssid = ""; // WiFi SSID -char * password = ""; // WiFi password - unsigned int localPort = 2390; // local port to listen for UDP packets IPAddress timeServerIP; // IP address of random server -const char* ntpServerName = "de.pool.ntp.org"; // server pool +IPAddress mqttHostIP; // IP address of random server byte packetBuffer[48]; // buffer to hold incoming and outgoing packets -int timeZoneoffsetGMT = 3600; // offset from Greenwich Meridan Time -boolean DST = false; // daylight saving time -WiFiUDP clockUDP; // initialize a UDP instance - -// -// settings for the openweathermap connection -// sign up, get your api key and insert it below -// -char * servername = "api.openweathermap.org"; // remote server with weather info -String APIKEY = "45f6ce019087f08b45d73872319c0573"; // personal api key for retrieving the weather data - -const int httpPort = 80; -String result; -int cityIDLoop = 0; - -// a list of cities you want to display the forecast for -// get the ID at https://openweathermap.org/ -// type the city, click search and click on the town -// then check the link, like this: https://openweathermap.org/city/5128581 -// 5128581 is the ID for New York -String cityIDs[] = { - "2836455", // Schonungen - "" // end the list with an empty string -}; - -// -// settings -// -int startupDelay = 1000; // startup delay -int loopDelay = 3000; // main loop delay between sensor updates - -int timeServerDelay = 1000; // delay for the time server to reply -int timeServerPasses = 4; // number of tries to connect to the time server before timing out -int timeServerResyncNumOfLoops = 3000; // number of loops before refreshing the time. one loop takes approx. 28 seconds -int timeServerResyncNumOfLoopsCounter = 0; -boolean timeServerConnected = false; // is set to true when the time is read from the server - -int maxForecastLoop = 10; // number of main loops before the forecast is refreshed, looping through all cities -int weatherForecastLoop = 0; -int weatherForecastLoopInc = 1; - -int displayStartupDimValue = 30; // startup display backlight level -int displayDimValue = 150; // main display backlight level -int displayDimStep = 1; // dim step -int dimStartupDelay = 50; // delay for fading in -int dimPageDelay = 0; // delay for fading between pages // // initialize variables // int page = 0; - -float bmeAltitude = 0; -float bmeHumidity = 0; -float bmePressure = 0; -float bmeTemperature = 0; - String command; String doubleQuote = "\"\""; -// -// initialize timer -// -os_timer_t secTimer; - -void timerDisplayTime(void *pArg) { - displayTime(); -} +WiFiClient wifiClient; +WiFiUDP ntpUDP; +NTPClient timeClient(ntpUDP, ntpServerName, timeZoneoffsetGMT, timeServerRefreshDelay); +PubSubClient mqttClient(wifiClient); -// -// setup -// void setup() { -#ifdef DEBUG +//#ifdef DEBUG Serial.begin(9600); -#endif + Serial.println("setup..."); +//#endif nexSerial.begin(9600); @@ -130,53 +68,28 @@ void setup() { delay(startupDelay); - displayFadeIn(0, displayStartupDimValue, dimStartupDelay); + displayFadeIn(1, displayStartupDimValue, dimStartupDelay); - connectToWifi(); - clockUDP.begin(localPort); + reconnectToWifi(); + timeClient.begin(); getTimeFromServer(); - if (timeServerConnected) { - displayTime(); - } - - displayDate(); - - os_timer_setfn(&secTimer, timerDisplayTime, NULL); - if (timeServerConnected) { - os_timer_arm(&secTimer, 1000, true); - } - displayFadeIn(displayStartupDimValue, displayDimValue, dimStartupDelay / 2); -#ifdef DEBUG - Serial.println("Starting main loop"); -#endif + mqttClient.setServer(mqttHost, mqttPort); + mqttClient.setCallback(getWeatherDataMqtt); + reconnectMqtt(); } - -// -// main loop -// void loop() { - if (!timeServerConnected) { - getTimeFromServer(); - if (timeServerConnected) - os_timer_arm(&secTimer, 1000, true); - } +#ifdef DEBUG + Serial.println("loop"); +#endif if (page == 0) { if (weatherForecastLoop == maxForecastLoop) { - timeServerResyncNumOfLoopsCounter += 1; - if (timeServerResyncNumOfLoopsCounter == timeServerResyncNumOfLoops) { - getTimeFromServer(); - timeServerResyncNumOfLoopsCounter = 0; - } - page = 1; - getWeatherData(); weatherForecastLoopInc = -weatherForecastLoopInc; - displayDate(); } delayCheckTouch(loopDelay); @@ -197,9 +110,6 @@ void loop() { #endif weatherForecastLoopInc = -weatherForecastLoopInc; - - displayTime(); - displayDate(); } else delayCheckTouch(loopDelay); @@ -207,45 +117,30 @@ void loop() { weatherForecastLoop += weatherForecastLoopInc; -#ifdef DEBUG - Serial.print(page); - Serial.print(" "); - Serial.print(weatherForecastLoop); - Serial.print(" "); - Serial.print(timeServerResyncNumOfLoopsCounter); - Serial.print(" "); - Serial.print(timeServerResyncNumOfLoops); - Serial.println(""); -#endif -} - -void displayDate() { - time_t t = now(); + // Update date and time on display + sendToLCD(1, "time", String(timeClient.getFormattedTime())); + sendToLCD(1, "date", dayAsString(timeClient.getDay())); - command = "date.txt=\"" + String(day(t)) + " " + monthAsString(month(t)) + "\""; - printNextionCommand(command); - command = "year.txt=\"" + String(year(t)) + "\""; - printNextionCommand(command); -} + reconnectToWifi(); -void displayTime() { - time_t t = now(); - char timeString[9]; + reconnectMqtt(); + mqttClient.loop(); - snprintf(timeString, sizeof(timeString), "%02d:%02d:%02d", hour(t), minute(t), second(t)); - command = "time.txt=\"" + String(timeString) + "\""; - printNextionCommand(command); +#ifdef DEBUG + Serial.print(page); + Serial.print(" "); + Serial.print(weatherForecastLoop); + Serial.println(""); +#endif } - -// -// Nextion commands -// void printNextionCommand (String command) { nexSerial.print(command); endNextionCommand(); } - void sendToLCD(uint8_t type, String index, String cmd) { +#ifdef DEBUG + Serial.println("sendToLCD"); +#endif if (type == 1 ) { nexSerial.print(index); nexSerial.print(".txt="); @@ -270,14 +165,15 @@ void sendToLCD(uint8_t type, String index, String cmd) { endNextionCommand(); } - void endNextionCommand() { nexSerial.write(0xff); nexSerial.write(0xff); nexSerial.write(0xff); } - void displayFadeIn(int fromValue, int toValue, int fadeDelay) { +#ifdef DEBUG + Serial.println("displayFadeIn"); +#endif for (int i = fromValue; i <= toValue; i += displayDimStep) { if (i > toValue) { i = toValue; @@ -286,8 +182,10 @@ void displayFadeIn(int fromValue, int toValue, int fadeDelay) { delay(fadeDelay); } } - void displayFadeOut(int fromValue, int fadeDelay) { +#ifdef DEBUG + Serial.println("displayFadeOut"); +#endif for (int i = fromValue; i >= 0; i -= displayDimStep) { if (i < 0) { i = 0; @@ -296,174 +194,101 @@ void displayFadeOut(int fromValue, int fadeDelay) { delay(fadeDelay); } } - -// -// network functions -// -void connectToWifi() { - int wifiBlink = 0; - - // WiFi.enableSTA(true); - WiFi.mode(WIFI_STA); - WiFi.disconnect(); - - WiFi.begin(ssid, password); - - while (WiFi.status() != WL_CONNECTED) { - if (wifiBlink == 0) { - printNextionCommand("wifi_connect.txt=\"connect...\""); - wifiBlink = 1; - } - else { - printNextionCommand("wifi_connect.txt=" + doubleQuote); - wifiBlink = 0; +void reconnectToWifi() { +#ifdef DEBUG + Serial.println("reconnectToWifi..."); +#endif + if(WiFi.status() != WL_CONNECTED) { + int wifiBlink = 0; + + // WiFi.enableSTA(true); + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + WiFi.begin(ssid, password); + + while (WiFi.status() != WL_CONNECTED) { + if (wifiBlink == 0) { + printNextionCommand("wifi_connect.txt=\"connect...\""); + sendToLCD(1, "txtIp", "\"No IP yet...\""); + wifiBlink = 1; + } + else { + printNextionCommand("wifi_connect.txt=" + doubleQuote); + wifiBlink = 0; + } + delay(500); } - delay(500); } +#ifdef DEBUG + Serial.print("Wifi IP Address: "); + Serial.println(WiFi.localIP().toString()); +#endif + sendToLCD(1, "txtIp", WiFi.localIP().toString()); printNextionCommand("wifi_connect.txt=" + doubleQuote); } - void getTimeFromServer() { #ifdef DEBUG - Serial.print("Getting time from server..."); -#endif - - int connectStatus = 0, i = 0; - unsigned long unixTime; - - while (i < timeServerPasses && !connectStatus) { -#ifdef DEBUG - Serial.print(i); - Serial.print("..."); + Serial.println("getTimeFromServer"); #endif - printNextionCommand("time.txt=\"get time..\""); - WiFi.hostByName(ntpServerName, timeServerIP); - sendNTPpacket(timeServerIP); - delay(timeServerDelay / 2); - connectStatus = clockUDP.parsePacket(); - printNextionCommand("time.txt=" + doubleQuote); - delay(timeServerDelay / 2); - i++; + sendToLCD(1, "time", "get time.."); + while(!timeClient.update()) { + timeClient.forceUpdate(); } - - if (connectStatus) { + timeClient.setTimeOffset(timeZoneoffsetGMT); + timeClient.setUpdateInterval(timeServerRefreshDelay); +} +void reconnectMqtt() { + String settings; + StaticJsonDocument<1024> doc; + JsonObject object = doc.to<JsonObject>(); + object["plz"] = backendPlz; + object["lngCode"] = backendLngCode; + object["frontendId"] = frontendId; + serializeJson(doc, settings); + + while (!mqttClient.connected()) { + if (mqttClient.connect(frontendId.c_str(), mqttUser, mqttPass)) { + Serial.println("MQTT Connected"); + //mqttClient.subscribe(mqttTopic); + mqttClient.subscribe("/wetterstation/frontend/7363421/weather"); + Serial.print("MQTT Subscribe to: "); + Serial.println(mqttTopic); + mqttClient.publish(mqttTopicBackend, settings.c_str()); + } + else { #ifdef DEBUG - Serial.print(i); - Serial.println("...connected"); + Serial.print("failed, rc="); + Serial.print(mqttClient.state()); + Serial.println(" retrying in 5 seconds"); #endif - - timeServerConnected = true; - clockUDP.read(packetBuffer, 48); - - // the timestamp starts at byte 40 of the received packet and is four bytes, or two words, long. - unsigned long highWord = word(packetBuffer[40], packetBuffer[41]); - unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]); - // the timestamp is in seconds from 1900, add 70 years to get Unixtime - unixTime = (highWord << 16 | lowWord) - 2208988800 + timeZoneoffsetGMT; - - if (DST) { - unixTime = unixTime + 3600; + delay(5000); } - - setTime(unixTime); } - else { +} +void getWeatherDataMqtt(char* topic, byte* payload, unsigned int length) { #ifdef DEBUG - Serial.print(i); - Serial.println("...failed..."); + Serial.println("getWeatherDataMqtt..."); + Serial.println("From mqttTopic: " + String(mqttTopic)); + Serial.println("From topic: " + String(topic)); #endif - - printNextionCommand("time.txt=\"failed....\""); - delay(timeServerDelay); - printNextionCommand("time.txt=" + doubleQuote); - } -} - -unsigned long sendNTPpacket(IPAddress& address) { - memset(packetBuffer, 0, 48); - packetBuffer[0] = 0b11100011; // LI, Version, Mode - packetBuffer[1] = 0; // Stratum, or type of clock - packetBuffer[2] = 6; // Polling Interval - packetBuffer[3] = 0xEC; // Peer Clock Precision - // 8 bytes of zero for Root Delay & Root Dispersion - packetBuffer[12] = 49; - packetBuffer[13] = 0x4E; - packetBuffer[14] = 49; - packetBuffer[15] = 52; - - clockUDP.beginPacket(address, 123); //NTP requests are to port 123 - clockUDP.write(packetBuffer, 48); - clockUDP.endPacket(); -} - -// -// get and display weather data -// -void getWeatherData() { //client function to send/receive GET request data. - WiFiClient client; - if (!client.connect(servername, httpPort)) { - return; - } - - String cityID = cityIDs[cityIDLoop]; - cityIDLoop++; - - if (cityIDs[cityIDLoop] == "") { - cityIDLoop = 0; - } - - String url = "/data/2.5/forecast?id=" + cityID + "&units=metric&cnt=1&APPID=" + APIKEY + "&lang=de"; - //String url = "/data/2.5/weather?id=" + cityID + "&units=metric&cnt=1&APPID=" + APIKEY; - //check weather properties at https://openweathermap.org/current - - // This will send the request to the server - client.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + servername + "\r\n" + "Connection: close\r\n\r\n"); - unsigned long timeout = millis(); - while (client.available() == 0) { - if (millis() - timeout > 5000) { - client.stop(); - return; - } - } - - result = ""; - // Read all the lines of the reply from server - while (client.available()) { - result = client.readStringUntil('\r'); - } - - result.replace('[', ' '); - result.replace(']', ' '); - - char jsonArray [result.length() + 1]; - result.toCharArray(jsonArray, sizeof(jsonArray)); - jsonArray[result.length() + 1] = '\0'; - - StaticJsonBuffer<1024> json_buf; - JsonObject& root = json_buf.parseObject(jsonArray); - if (!root.success()) { - nexSerial.println("parseObject() failed"); - } - - //check properties forecasts at https://openweathermap.org/forecast5 - - int weatherID = root["list"]["weather"]["id"]; - - String tmp0 = root["city"]["name"]; - String tmp1 = root["list"]["weather"]["main"]; - String tmp2 = root["list"]["weather"]["description"]; - float tmp3 = root["list"]["main"]["temp_min"]; - float tmp4 = root["list"]["main"]["temp_max"]; - float tmp5 = root["list"]["main"]["humidity"]; - float tmp6 = root["list"]["clouds"]["all"]; - float tmp7 = root["list"]["rain"]["3h"]; - float tmp8 = root["list"]["snow"]["3h"]; - float tmp9 = root["list"]["wind"]["speed"]; - int tmp10 = root["list"]["wind"]["deg"]; - float tmp11 = root["list"]["main"]["pressure"]; - timeZoneoffsetGMT = root["city"]["timezone"]; + // Parsing + StaticJsonDocument<1024> root; + deserializeJson(root, payload, length); + + String tmp0 = backendCity; + //String tmp1 = root["main"]; // Icon main + //String tmp2 = root["description"]; // Icon description + float tmp3 = root["tempMin"]; + float tmp4 = root["tempMax"]; + float tmp5 = root["humidity"]; + float tmp6 = root["clouds"]; + float tmp7 = root["rain3h"]; + float tmp8 = root["snow3h"]; + float tmp9 = root["windSpeed"]; + int tmp10 = root["windDeg"]; + float tmp11 = root["pressure"]; //String tmp12 = root["list"]["dt_text"]; command = command + tmp12; #if !defined (DEBUG_NO_PAGE_FADE) @@ -476,9 +301,9 @@ void getWeatherData() { //client function to send/receive GET request data. displayFadeIn(0, displayDimValue, dimPageDelay ); #endif - setWeatherPicture(weatherID); + //setWeatherPicture(weatherID); sendToLCD(1, "city", tmp0); - sendToLCD(1, "description", tmp2); + //sendToLCD(1, "description", tmp2); sendToLCD(1, "humidity", String(tmp5, 0)); sendToLCD(1, "rain", String(tmp7, 1)); sendToLCD(1, "wind_dir", getShortWindDirection(tmp10)); @@ -488,8 +313,10 @@ void getWeatherData() { //client function to send/receive GET request data. sendToLCD(1, "temp_min", String(tmp3, 1)); sendToLCD(1, "temp_max", String(tmp4, 1)); } - String getWindDirection (int degrees) { +#ifdef DEBUG + Serial.println("getWindDirection..."); +#endif int sector = ((degrees + 11) / 22.5 - 1); switch (sector) { case 0: return "Nord"; @@ -510,8 +337,10 @@ String getWindDirection (int degrees) { case 15: return "Nord-Nordwest"; } } - String getShortWindDirection (int degrees) { +#ifdef DEBUG + Serial.println("getShortWindDirection..."); +#endif int sector = ((degrees + 11) / 22.5 - 1); switch (sector) { case 0: return "N"; @@ -532,8 +361,10 @@ String getShortWindDirection (int degrees) { case 15: return "NNW"; } } - void setWeatherPicture(int weatherID) { +#ifdef DEBUG + Serial.println("setWeatherPicture..."); +#endif switch (weatherID) { case 200: case 201: @@ -598,29 +429,35 @@ void setWeatherPicture(int weatherID) { default: sendToLCD(3, "weatherpic", "10"); break; // dunno } } - void delayCheckTouch (int delayTime) { +#ifdef DEBUG + Serial.println("delayCheckTouch..."); +#endif unsigned long startMillis = millis(); while (millis() - startMillis < delayTime) { delay(1000); } } - String dayAsString(int day) { +#ifdef DEBUG + Serial.println("dayAsString..."); +#endif switch (day) { - case 1: return "Sonntag"; - case 2: return "Montag"; - case 3: return "Dienstag"; - case 4: return "Mittwoch"; - case 5: return "Donnerstag"; - case 6: return "Freitag"; - case 7: return "Samstag"; + case 1: return "Montag"; + case 2: return "Dienstag"; + case 3: return "Mittwoch"; + case 4: return "Donnerstag"; + case 5: return "Freitag"; + case 6: return "Samstag"; + case 7: return "Sonntag"; } return "" ; } - String monthAsString(int month) { +#ifdef DEBUG + Serial.println("monthAsString..."); +#endif switch (month) { case 1: return "Januar"; case 2: return "Februar"; -- GitLab