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

В предыдущей статье вывел показания счетчика на 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-ами:

PinFunctionESP-8266 Pin
TXTXDTXD
RXRXDRXD
A0Analog input, max 3.3V inputA0
D0IOGPIO16
D1IO, SCLGPIO5
D2IO, SDAGPIO4
D3IO, 10k Pull-upGPIO0
D4IO, 10k Pull-up, BUILTIN_LEDGPIO2
D5IO, SCKGPIO14
D6IO, MISOGPIO12
D7IO, MOSIGPIO13
D8IO, 10k Pull-down, SSGPIO15
GGroundGND
5V5V
3V33.3V3.3V
RSTResetRST

Мне потребуется два входа для подключения индикатора по I2C:

  • SDA => D2.
  • SCL => D1

Адаптированный код Arduino из прошлой статьи под ESP8266.

//Wemos D1 Mini water meter counter
//(c) Andrey A. Fedorov
//E-mail: 2af@mail.ru
 
#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())); // See 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)); // + "&amp;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();
  }
}

Spread the love
Запись опубликована в рубрике IT tools, IT рецепты с метками , , . Добавьте в закладки постоянную ссылку.