В предыдущей статье вывел показания счетчика на 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.ru https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library https://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 display enum 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.