В предыдущей статье вывел показания счетчика на LCD индикатор. По нынешним временам — этого недостаточно. Уж очень примитивно. Нынче модный тренд облака. Передадим данные телеметрии в облако, чтобы в дальнейшем можно было анализировать данные.
В качестве протокола используем MQTT. Облачный сервер будет от MatLab: http://thingspeak.com. Помимо него есть Blynk, Cayenne и множество других серверов. Ранее был популярен Sparkfun, но, похоже, проект закрыли.
Почему облако, а не локальный сервер? Понятно, что из-за лени. 🙂 Зачем разворачивать локальный сервер, сопровождая его, если можно решить задачу практически мгновенно?
Хотя, можно поднять и локальный IoT сервер, OpenSource вариантов множество: OpenHab, Domoticz, OpenMotics, Calaos, Home Assistant, MisterHouse и много других.
Протокол MQTT поддерживается всеми ведущими IoT платформами. Это позволит оперативно сменить облачного IoT провайдера, если возникнут проблемы с сервисом, либо изменятся условия использования.
На данный момент у https://thingspeak.com/prices/thingspeak_home есть бесплатная подписка для домашних пользователей.
У меня не оказалось под руками Wi-Fi Shield для Arduino, поэтому я воспользовался ESP-8266 модулем в реализации на плате Wemos D1 mini. Wemos D1 mini (ESP8266) широко представлена на Aliexpress по цене менее 3 USD.
Модуль компактен, потребляет меньше энергии, чем Arduino, уже имеет на борту Wi-Fi с нативной поддержкой необходимых для передачи данных в облако AT команд и поддерживается средой разработки Arduino.
Выходы ESP 8266
Для начала разберемся с PIN-ами:
| Pin | Function | ESP-8266 Pin |
| TX | TXD | TXD |
| RX | RXD | RXD |
| A0 | Analog input, max 3.3V input | A0 |
| D0 | IO | GPIO16 |
| D1 | IO, SCL | GPIO5 |
| D2 | IO, SDA | GPIO4 |
| D3 | IO, 10k Pull-up | GPIO0 |
| D4 | IO, 10k Pull-up, BUILTIN_LED | GPIO2 |
| D5 | IO, SCK | GPIO14 |
| D6 | IO, MISO | GPIO12 |
| D7 | IO, MOSI | GPIO13 |
| D8 | IO, 10k Pull-down, SS | GPIO15 |
| G | Ground | GND |
| 5V | 5V | — |
| 3V3 | 3.3V | 3.3V |
| RST | Reset | RST |
Мне потребуется два входа для подключения индикатора по I2C:
- SDA => D2.
- SCL => D1
Адаптированный код Arduino из прошлой статьи под ESP8266.
//Wemos D1 Mini water meter counter//(c) Andrey A. Fedorov//E-mail: 2af@mail.ruhttps://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-libraryhttps://pubsubclient.knolleary.net/https://www.mathworks.com/help/thingspeak/use-arduino-client-to-publish-to-a-channel.html#include "Bounce2.h";#include "Wire.h" #include "LiquidCrystal_I2C.h"#include "PubSubClient.h"#include "ESP8266WiFi.h"WiFiClient client;PubSubClient mqttClient(client); // Initialize the PuBSubClient library.LiquidCrystal_I2C lcd(0x3F,16,2); // // set the LCD address to 0x3F for a 16 chars and 2 line displayenum PIN { Cold = 0, Hot = 1, Last };struct Input { String name; Bounce debouncer; bool laststate; double value; int pin; int field;};Input inputs[Last] = { {"Cold", Bounce(), false, 39.703, D5, 1}, {"Hot", Bounce(), false, 25.203, D6, 2} };unsigned long lastConnectionTime = 0; const unsigned long postingInterval = 1L * 1000L; // Post data every 1 seconds for test purpose.void setup(){ //pinMode(LED_PIN, OUTPUT); Serial.begin(9600, SERIAL_8N1); for (int i=Cold; i < Last; i++) { inputs[i].debouncer.attach(inputs[i].pin); // Attach pulse pin for debouncing inputs[i].debouncer.interval(50); //Ignoring interval pinMode(inputs[i].pin, INPUT_PULLUP); // Initialize digital pin as an input with the internal pull-up (+5V) resistor enabled } lcd.init(); //initialize the lcd lcd.backlight(); //open the backlight lcd.setCursor(0, 0); // set the cursor to column 3, line 0 lcd.print("Cold: " + String(inputs[Cold].value) + " m3"); // Print a message to the LCD initMQTT(); // Connect to Wi-Fi network connectWiFi();}void loop(){ for (int i=Cold; i <= Hot; i++) { inputs[i].debouncer.update(); // Read Reed Switch state int value = inputs[i].debouncer.read(); // Now all processes are finished and we know exactly the state of the Reed Switch if ( value == LOW ) { //digitalWrite(LED_PIN, HIGH ); if (inputs[i].laststate == false) { inputs[i].value += 0.01; //Add 10 liters (0,01 m3) Serial.println(inputs[i].name + " water: " + (String)(inputs[i].value) + " m3"); lcd.setCursor(0, i); // set the cursor to column 0, line 0 lcd.print(inputs[i].name + ": " + (String)(inputs[i].value) + " m3"); // Print a message to the LCD SendDataToCloud(i); } } inputs[i].laststate = !(bool)value; }}void SendDataToCloud(int index){ if (WiFi.status() != WL_CONNECTED) { connectWiFi(); } if (WiFi.status() == WL_CONNECTED) { // If interval time has passed since the last connection, Publish data to ThingSpeak if (millis() - lastConnectionTime > postingInterval) { mqttpublish(index); } }} |
В коде добавлено:
- Поддержка дополнительных входов. Достаточно заполнить PIN, Input и inputs.
- Добавлен вызов модуля для передачи данных по Wi-Fi (код ниже).
- Добавлена передача данных в облако по протоколу MQTT.
Код стабильно считает импульсы с импульсного входа водосчетчика горячей и холодной воды.
Передача данных по Wi-Fi
В IDE Arduino нажмем сочетание клавиш Ctrl-Shift-N для создания ещё одного *.ino файла, чтобы вынести в него код касаемый работы ESP8266 в качестве Wi-Fi клиента. В принципе, в коде нет ничего сложного.
#include "ESP8266WiFi.h"
//SSID of your network
char ssidWiFi[] = YYYY"; //SSID of your Wi-Fi router
char passWiFi[] = "XXXX"; //Password of your Wi-Fi router
bool connectWiFi()
{
Serial.println("Tryng to connect to Wi-Fi AP with SSID [" + String(ssidWiFi) + "]");
WiFi.mode(WIFI_STA);
WiFi.begin(ssidWiFi, passWiFi);
int connRes = WiFi.waitForConnectResult();
if (connRes != WL_CONNECTED)
{
Serial.println("Wi-Fi connection to " + WiFi.SSID() + " failed. Status " + String(WiFi.status()) + ", " + WiFiconnectionStatus(WiFi.status()) + ", Retrying later...\n");
WiFi.disconnect();
return false;
}
else {
Serial.print("Wi-Fi connected to " + WiFi.SSID() + ". IP address: ");
Serial.println(WiFi.localIP()); //IP2STR( WiFi.localIP()));
mqttClient.setServer(server, 1883); // Set the MQTT broker details.
mqttClient.setCallback(mqttSubscriptionCallback);
}
return true;
}
String WiFiconnectionStatus(int which)
{
switch (which)
{
case WL_CONNECTED:
return "Connected";
break;
case WL_NO_SSID_AVAIL:
return "Network is not availible";
break;
case WL_CONNECT_FAILED:
return "Wrong password";
break;
case WL_IDLE_STATUS:
return "Idle status";
break;
case WL_DISCONNECTED:
return "Disconnected";
break;
default:
return "Unknown";
break;
}
}Передача телеметрии в ThingSpeak по MQTT
Как уже упоминал выше, в качестве протокола для передачи данных выбран MQTT. Хотя по опыту эксплуатации он меня несколько разочаровал.
- После публикации данных функция всегда возвращает true. Даже если данные не появились в ThingsSpeak. Соответственно, это может приводить к выпадению части данных телеметрии.
- Подписка на канал отрабатывает плохо. По данным по которым уже прошла публикация не приходит уведомление о изменении поля (field). Соответственно, этот механизм не может быть использован для подтверждения внесенных изменений.
- Теоретически, подписку на канал можно использовать для передачи параметров настройки с сервера. Например, чтобы переслать на устройство текущие показания счетчика. Однако, не понятно как гарантировать доставку.
- Из примеров работы с MQTT на сайте ThingsSpeak нужно убирать все моменты с delay(). Пока отрабатывает delay() может произойти срабатывание геркона водосчетчика и может быть пропущено значение. В идеале, чтобы избежать этого нужно использовать аппаратные прерывания. Это устранит зависимость от задержек выполнения операций вроде отправки данных по MQTT. Но в случае с прерываниями обязательно нужно использовать аппаратное устранение «дребезга контактов». В простейшем варианте — сглаживание RC цепочкой.
#include "PubSubClient.h"const char* server = "mqtt.thingspeak.com"; char mqttUserName[] = "XXXXXXXX"; // Can be any name.char mqttAPIKey[] = "G8UYMVHO6CYYYY"; // Change this your MQTT API Key from Account -> MyProfile.char readAPIKey[] = "81Y5MLWESY1YYYY"; // Change to your channel Read API Key.long channelID = 662562; //Change to your Account -> MyProfile -> Channels -> My Channels char writeAPIKey[] = "A5OKIYQSAFHYYYY"; // Change to your channel Write API Key.String topicString;static const char alphanum[] ="0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz"; // For random generation of client ID.void initMQTT(){ // Create a topic string and publish data to ThingSpeak channel feed. topicString ="channels/" + String(channelID) + "/publish/"+String(writeAPIKey);}void mqttSubscriptionCallback(char* topic, byte* payload, unsigned int length){ Serial.print("Message arrived [" + String(topic) + "]: "); for (int i = 0; i < length; i++) { Serial.print((char)payload[i]); } Serial.println();}/** * Subscribe to fields of a channel * subChannelID - channel to subscribe to * field - field to subscribe to. Value 0 means subscribe to all fields. * readKey - Read API Key for the subscribe channel. * unSub - set to 1 for unsubscribe. */int mqttSubscribe(long subChannelID, int field, char* readKey, int unsubSub){ String myTopic; // There is no field zero, so if field 0 is sent to subscribe to, then subscribe to the whole channel feed. if (field == 0) { myTopic = "channels/" + String(subChannelID) + "/subscribe/json/" + String(readKey); } else { myTopic = "channels/" + String(subChannelID) + "/subscribe/fields/field" + String(field) + "/" + String(readKey); } //Serial.println( "Subscribing to " + myTopic ); //Serial.println( "State= " + String(mqttClient.state())); if (unsubSub == 1) { return mqttClient.unsubscribe(myTopic.c_str()); } return mqttClient.subscribe(myTopic.c_str(), 0);}String MQTTConnectionStatus(int code){ switch (code) { case MQTT_CONNECTION_TIMEOUT: //-4 return "The server didn't respond within the keepalive time"; break; case MQTT_CONNECTION_LOST: //-3 : return "The network connection was broken"; break; case MQTT_CONNECT_FAILED: //-2 return "The network connection failed"; break; case MQTT_DISCONNECTED: //-1 return "The client is disconnected cleanly"; break; case MQTT_CONNECTED: //0 return "The client is connected"; break; case MQTT_CONNECT_BAD_PROTOCOL: //1 return "The server doesn't support the requested version of MQTT"; break; case MQTT_CONNECT_BAD_CLIENT_ID: //2 return "The server rejected the client identifier"; break; case MQTT_CONNECT_UNAVAILABLE: //3 return "The server was unable to accept the connection"; case MQTT_CONNECT_BAD_CREDENTIALS: //4 return "The username/password were rejected"; break; case MQTT_CONNECT_UNAUTHORIZED: //5 return "The client was not authorized to connect"; break; default: return "Unknown"; break; }}/** * Build a random client ID * clientID - char array for output * idLength - length of clientID (actual length will be one longer for NULL) */void getID(char clientID[], int idLength){ // Generate ClientID for (int i = 0; i < idLength; i++) { clientID[i] = alphanum[random(51)]; } clientID[idLength] = '\0';}bool reconnect(){ char clientID[9]; Serial.println("Attempting MQTT connection..."); getID(clientID, 8); // Connect to the MQTT broker if (mqttClient.connect(clientID, mqttUserName, mqttAPIKey)) { String str = "Connected with Client ID: " + String(clientID) + ", Username: " + mqttUserName + " , API Key: " + mqttAPIKey; Serial.println(str); for (int i = 1; i < Last; i++) { if (mqttSubscribe(channelID, i, readAPIKey, 0) == 1) { Serial.println("Subscribed to the channel [" + String(channelID) +"], the field [field" + String(i) + "]."); } } return true; } else { Serial.print("Connection failed with status: " + MQTTConnectionStatus(mqttClient.state())); https://pubsubclient.knolleary.net/api.html#state for the failure code explanation. } return false;}void mqttpublish(int index){ if (!mqttClient.connected()) { reconnect(); } if (mqttClient.connected()) { mqttClient.loop(); // Call the loop regularly to allow the client to process incoming messages and maintain its connection to the server. // Create data string to send to ThingSpeak String data = String("field" + String(inputs[index].field) + "=" + String(inputs[index].value)); // + "&field2=" + String(inputs[Hot].value)); String str = "Trying to send data to server [" + String(server) + "]:\r\n"; str += "\tData: " + data + "\r\n"; str += "\tTopic string: " + topicString; Serial.println(str); bool result = mqttClient.publish(topicString.c_str(), data.c_str()); //ThingSpeak always return true. :-( if (result) { Serial.println("Data successfully published to channel: " + String(channelID)); } else { Serial.println("Error data sending."); } lastConnectionTime = millis(); }} |

1 Responses to Передача показаний счетчика расхода воды в облако по Wi-Fi. ESP 8266.