Удаленное управление домофоном на ESP8266. MQTT. Часть 3.

Разработанное устройство позволяет помимо удаленного открывания двери для гостей реализовать механизм шаринга домофона (domophone sharing):

  • Несколько квартир в подъезде объединяются, отказываясь от услуги домофона, поскольку она неадекватно дорогая.
  • Только на одной из квартир остается подключенной услуга. На этой квартире подключается разработанное устройство удаленного управления домофоном.
  • На мобильные всех объединившихся квартир устанавливается разработанное приложение.
  • В дальнейшем домофон открывается с мобильного удаленно, в т.ч. для гостей.

Если есть желание приобрести устройство, то можно разместить заявку в форме предзаказа в конце статьи.

В предыдущей статье была оттестирована схемотехника модуля управления домофоном. Код на ESP8266 выполнял простую операцию — автоматическое открывание домофона. Усложню код, чтобы можно было контролировать открывание удаленно. В данном случае я использую MQTT брокера, работу с которым подробно рассмотрел в этой статье.

К сожалению, я не нашел способа инициировать открывание замка домофона со стороны квартиры. Сначала гость должен набрать номер квартиры с панели домофона. Затем коммутационная матрица домофона соединяет трубку определенной квартиры с панелью домофона.

Только после этого плата удаленного управления домофоном может прикинуться домофонной трубкой и отворить дверь в ответ на сигнал вызова. Поэтому процесс удаленного открытия будет непривычным:

  • Незадолго до прихода гостей, которых нужно впустить, плата управления активируется с MQTT сервера. До этого момента набор номера квартиры не приводит к открытию двери, в квартире просто будет звонить домофон.
  • Когда гость набирает номер квартиры, плата уже активирована с сервера и ожидает прихода сигнала с домофона.
  • При поступлении вызова сопротивление между 3-4 ножками оптрона падает, и по входу D5 («DETECT_CALL_PIN») срабатывает прерывание, определяющее переход с логической единицы на ноль. ESP8266 обрабатывает информацию о поступлении вызова и начинает отсчет времени, заданного переменной delayBeforeOpenDoor.
  • По завершению времени delayBeforeOpenDoor ESP8266 подает на выход D1 («LINE_SWITCH_PIN») сигнал логической 1 и переключает реле «LINE SWITCH» с домофонной трубки на плату управления домофоном.
  • Вторая пара контактов реле U1 замыкает ножку D6 («DETECT_LINE_PIN») на землю, и по этому входу срабатывает прерывание, подтверждающее срабатывание реле. Микроконтроллер отправляет publish на MQTT сервер с topic «linestatus».
  • Уже можно открывать дверь. Для этого на ножку D2 («OPEN_DOOR_PIN») подается уровень логической единицы в течение openDoorDelay (20 сек). Этого времени достаточно, чтобы домофон посчитал, что кнопка на домофонной трубке нажата, и открыл замок гостям. Код для открытия можно разместить как в функцию, обрабатывающую прерывание DETECT_LINE_PIN, так и в loop, не дожидаясь подтверждения о срабатывании U1.
  • После срабатывания реле U2 вход D7 («DETECT_DOOR_PIN») закорачивается на землю, и срабатывает прерывание, воспринимаемое микроконтроллером как подтверждение об открытии двери. Информация об этом ESP8266 отправляет на MQTT сервер с topic «doorstatus».
  • Состояние активации держится в течение resetActivationDelay (10 минут) на случай, если гость не успел открыть дверь и т.п., и ему нужно повторить открытие.
  • Если с мобильного приложения выставлен флаг keepactivation, то активация по прошествии resetActivationDelay не сбрасывается, т.е. фактически всегда работает режим автоматического открывания двери.

Передача данных идет по Wi-Fi. Настройка параметров Wi-Fi соединения подключением к точке доступа DomophoneAP и выбором Wi-Fi точки доступа в помещении с вводом пароля.

Алгоритм незамысловатый, перейду к коду.

Настройка MQTT мобильного приложения

В качестве MQTT сервера воспользуюсь Cloudmqtt. Этот сервер выбран из-за стабильной работы с конструкторами мобильных приложений. о которых подробно рассказано в статье.

Android приложение для работы с MQTT сервером: IoT MQTT Panel. Настройку приложения делаем как указано в статье. Добавим следущие элементы управления:

ControlNameTopicSubscribe TopicPayload onPayload offКомментарий
SwitchActivationactivationactivationstate10Активация автоматического открывания двери
SwitchLinelinerelaylinestatus10Ручное переключение реле линии
Switch Doordorrrelaydoorstatus10Ручное переключение реле двери
LEDLinelinestatus10Индикатор состояния реле линии
LEDDoordoorstatus10Индикатор состояния реле двери
LEDActivationactivationstate10
Text InputDelay before opendelaybeforeopenДля изменения времени с момента поступления сигнала вызова до момента автоматического открывания двери.
ButtonUpdate StatusupdateNo payload
SwitchKeep activationkeepactivationkeepactivation 10

Важный момент. «Subscribe topic» желательно, чтобы отличался от «Topic», чтобы переключатель гарантированно отображал состояние, отправленное со стороны микроконтроллера только после выполнения всех операций по переключению.

Со смартфона можно отправить сигнал на переключение каждого реле. Даже для миниатюрных реле слышны щелчки при срабатывании. Вторая контактная группа каждого реле заведена на входы микроконтроллера для контроля срабатывания. Отрабатывает прерывание, и отправляется publish для уведомления сервера о смене состояния. Элементы управления (controls) в мобильном приложении принимают от MQTTсервера subscribe topic (linestatus или doorstatus), подтверждающий срабатывание соответствующих реле.

Для дополнительного контроля для каждого переключателя добавлены LED индикаторы. Они дублируют функционал переключателей, можно их не добавлять. Каждый из переключателей и так отображает состояние полученное от MQTT сервера.

Мобильное приложение Android для управления домофоном

В случае выгрузки приложения нужно обновить состояние переключателей. Для этого добавлена кнопка «Update status». При её нажатии на MQTT сервер передается publish, сообщающий текущее состояние переключателей.

Обновить время задержки с момента поступления вызова до момента автоматического открывания двери можно в поле «delaybeforeopen». Время задается в мс.

По умолчанию я задал activation = true, т.е. сразу при загрузке микроконтроллера он встает в состояние автоматического открывания двери. Если нужно перевести в режим ручного открывания двери, то нужно снять активацию.

Код удаленного управления домофоном через MQTT

Основной код удаленного управления (Domophone_remote_control.ino).

//File Domophone_Remote_Control.ino
#include <PubSubClient.h>
#include <ESP8266WiFi.h>
#include <WiFiManager.h>
#include "vars.h"
#include "mqtt_utils.h"

void ICACHE_RAM_ATTR callDetector(); //Very important!!! Otherwise will get the error "ISR not in IRAM!"  
void ICACHE_RAM_ATTR DoorStatus(); //Very important!!! Otherwise will get the error "ISR not in IRAM!"  
void ICACHE_RAM_ATTR LineStatus(); //Very important!!! Otherwise will get the error "ISR not in IRAM!"
//void ICACHE_RAM_ATTR ActivationStatus(); //Very important!!! Otherwise will get the error "ISR not in IRAM!"

void setup() {
  Serial.begin(115200);

  Serial.println("Domophone remote control is enabled. Millis = " + String(millis())); 

  pinMode(LINE_SWITCH_PIN, OUTPUT);   
  pinMode(OPEN_DOOR_PIN, OUTPUT); 
  pinMode(DETECT_DOOR_PIN, INPUT_PULLUP);   
  pinMode(DETECT_LINE_PIN, INPUT_PULLUP);   
  //pinMode(ACTIVATION_PIN, INPUT_PULLUP);   
  
  initDetectCallPin();

  attachInterrupt(digitalPinToInterrupt(DETECT_CALL_PIN), callDetector, FALLING);
  attachInterrupt(digitalPinToInterrupt(DETECT_DOOR_PIN), DoorStatus, CHANGE);
  attachInterrupt(digitalPinToInterrupt(DETECT_LINE_PIN), LineStatus, CHANGE);
  //attachInterrupt(digitalPinToInterrupt(ACTIVATION_PIN), ActivationStatus, CHANGE);
  
  WiFiManager wifiManager;
  wifiManager.autoConnect("DomophoneAP");
  Serial.println("Connected to WiFi successfully.");

  init_MQTT();

  setMQTTCallback();
  
  mqttConnect();

  subscribeToMQTT("activation");
  subscribeToMQTT("linerelay");
  subscribeToMQTT("doorrelay");
  subscribeToMQTT("update");
  subscribeToMQTT("delaybeforeopen");
  subscribeToMQTT("keepactivation");
    
  Init_SPIFF();
  String _delaybeforeopen = readFile();
  Serial.println("Delay before door open: " + _delaybeforeopen);
  delayBeforeOpenDoor = _delaybeforeopen.toInt();

  SendCurrentStatusToMQTT();
}

//Reset DETECT_CALL_PIN to LOW, otherwise it initializes as HIGH
void initDetectCallPin() 
{
  pinMode(DETECT_CALL_PIN, OUTPUT);   
  digitalWrite(DETECT_CALL_PIN, LOW); 
  pinMode(DETECT_CALL_PIN, INPUT);
}

/*void ActivationStatus()
{
  activation = digitalRead(ACTIVATION_PIN);
  activation = !activation;
  Serial.println("Activation PIN is " + boolToState(activation)); 
  publishToMQTT("activationstate", (char*)String(activation).c_str()); 
}*/

void DoorStatus()
{
  doorStatus = !digitalRead(DETECT_DOOR_PIN);
  Serial.println("The Door status is " + boolToState(doorStatus)); 
  publishToMQTT("doorstatus", (char*)String(doorStatus).c_str()); 
  openDoorMillis = 0;
  doorOpenedMillis = millis();
}

void LineStatus()
{
  lineStatus = !digitalRead(DETECT_LINE_PIN);
  Serial.println("The Line status is " + boolToState(lineStatus)); 
  publishToMQTT("linestatus", (char*)String(lineStatus).c_str()); 

  if (lineStatus)
  {
    Serial.println("Send signal to open the door."); 
    digitalWrite(OPEN_DOOR_PIN, HIGH);
  } else
  {
    Serial.println("Switch off DOOR OPEN relay."); 
    digitalWrite(OPEN_DOOR_PIN, LOW); 
  }
}

void callDetector() {
  if (!callDetectorStatus)
  {
    Serial.println("The call has detected and interrupt is fired."); 
    openDoorMillis = millis(); //Save time when call detector was fired 
    callDetectorStatus = true;
  }
}

//int lastMillis = 0;
//int value = 1;
//int call = LOW;
//long lastRebootMillis = 0;
void loop() 
{
    if (callDetectorStatus && (millis() - openDoorMillis > delayBeforeOpenDoor) && activation) //make delay for domophone ring
    {
      Serial.println("Switch line to the Remote Control"); 
      digitalWrite(LINE_SWITCH_PIN, HIGH);
      callDetectorStatus = false;
    }

    //Reset OPEN_DOOR relay and LINE relay
    if (doorStatus && (millis() - doorOpenedMillis > openDoorDelay))
    {
      Serial.println("Return LINE SWITCH relay back to domophone."); 
      digitalWrite(LINE_SWITCH_PIN, LOW);
    }

    //Reset dompohone remote control activation after resetActivationDelay ms 
    if (activation && !keepactivation && (millis() - doorOpenedMillis > resetActivationDelay)) 
    {
      activation = false;
      doorOpenedMillis = 0;
    }

    //Reboot ESP8266 each rebootInterval ms
    if (millis() > rebootInterval)
    {
      ESP.restart();
    }
    
    mqttClient.loop(); // Call the loop to maintain connection to the server.
}

Код взаимодействущий с MQTT сервером (Mqtt_utils.h)

//File mqtt_utils.h
#include <PubSubClient.h>
#include <ESP8266WiFi.h>
#include <WiFiManager.h>
#include "vars.h"
#include "passwords.h"
#include "spiff.h";

WiFiClient espClient;
PubSubClient mqttClient(espClient); 

const char* server = "farmer.cloudmqtt.com";
int mqtt_port = 11528; 

// Handle messages from MQTT subscription.
int mqttSubscriptionCallback(char* topic, byte* payload, unsigned int length);

// Generate a unique client ID and connect to MQTT broker.
bool mqttConnect();  

// Connect to a given Wi-Fi SSID
int connectWifi();

// Measure the Wi-Fi signal strength.
void updateRSSIValue();

//Publish to MQTT server with connection checking
void publishToMQTT(char* topic, char* payload);

//Subscribe to MQTT server with connection checking
void subscribeToMQTT(char* topic);

void init_MQTT();

void setMQTTCallback();

String boolToState(bool state);

void init_MQTT()
{
  mqttClient.setServer(server, mqtt_port); // Set the MQTT broker details.
}


void publishToMQTT(char* topic, char* payload)
{
  if (mqttConnect())
  {
    mqttClient.publish(topic, payload);
  }
}

void setMQTTCallback()
{
  if (mqttConnect())
  {
    mqttClient.setCallback(mqttSubscriptionCallback); 
  }
}

void subscribeToMQTT(char* topic)
{
  if (mqttConnect())
  {
    mqttClient.subscribe(topic);
  }
}

bool mqttConnect()
{
    if (WiFi.status() != WL_CONNECTED) //Check WiFi connection state and reconnect if connection was down
    {
        connectWifi();
    }

    int counter = 0;   
    if (WiFi.status() == WL_CONNECTED)
    {
      while ( !mqttClient.connected() )
      {
        Serial.println("Connecting to MQTT...");
 
        if (mqttClient.connect("Domophone", mqttUserName, mqttPass)) 
        {
          Serial.println("has connected.");  
          return true;
        } 
        else 
        {
          Serial.print("has failed with the state: ");
          Serial.println(mqttClient.state());
          delay(2000);
          counter++;
        }
        if (counter > 100)
        {
          Serial.print("Error during connection to MQTT server^ " + String(mqttClient.state()));
          return false;
        }
      }
      return true;
    }
    return false;
}

/**
 * Process messages received from subscribed channel via MQTT broker.
 *   topic - Subscription topic for message.
 *   payload - Field to subscribe to. Value 0 means subscribe to all fields.
 *   mesLength - Message length.
 */

String payloadToStr(byte* payload, unsigned int mesLength)
{
  String result = "";
  for (int i = 0; i < mesLength; i++) 
  {
    result += (char)payload[i];
  }
  return result;
}

bool payloadToBool(byte* payload, unsigned int mesLength)
{
  String str = payloadToStr(payload, mesLength);
  return (str == "1") ? true : false;
}

String boolToState(bool state)
{
  return state ? "ON": "OFF";
}

void SendCurrentStatusToMQTT()
{
    Serial.println("Refresh parameters:");
    Serial.println("\tactivationstate = " + String(activation));
    Serial.println("\tdoorstatus = " + String(doorStatus));
    Serial.println("\tlinestatus = " + String(lineStatus));
    Serial.println("\tdelaybeforeopen = " + String(delayBeforeOpenDoor));
    Serial.println("\tkeepactivation = " + String(keepactivation));
    
    publishToMQTT("activationstate", (char*)String(activation).c_str()); 
    publishToMQTT("doorstatus", (char*)String(doorStatus).c_str()); 
    publishToMQTT("linestatus", (char*)String(lineStatus).c_str());   
    publishToMQTT("delaybeforeopen", (char*)String(delayBeforeOpenDoor).c_str());  
    publishToMQTT("keepactivation", (char*)String(keepactivation).c_str()); 
}

int mqttSubscriptionCallback(char* topic, byte* payload, unsigned int mesLength ) 
{
  bool res = payloadToBool(payload, mesLength);
  String Topic = String(topic);

  if (Topic == "activation") 
  {
    Serial.println("Activation state is " + boolToState(res));
    if (activation != res) 
    {
      publishToMQTT("activationstate", (char*)String(res).c_str()); 
      activation = res;
    } 
    if (!activation)
    {
      callDetectorStatus = false;
      doorStatus = false;
      lineStatus = false;
      publishToMQTT("doorstatus", "0"); 
      publishToMQTT("linestatus", "0");
    }
  } else if (Topic == "doorrelay") 
  {
    Serial.println("Door state is " + boolToState(res)); 
    digitalWrite(OPEN_DOOR_PIN, res);
  } else if (Topic == "linerelay") 
  {
    Serial.println("Line state is " + boolToState(res)); 
    digitalWrite(LINE_SWITCH_PIN, res);
  } else if (Topic == "update")
  {
    SendCurrentStatusToMQTT();
  }else if (Topic == "keepactivation")
  {
    Serial.println("Keep activation is " + boolToState(res)); 
    if (keepactivation != res)
    {
      publishToMQTT("keepactivation", (char*)String(res).c_str()); 
      keepactivation = res;
      activation = res ? res : activation;
      publishToMQTT("activationstate", (char*)String(res).c_str()); 
    }
  } else if (Topic == "delaybeforeopen")
  {
    String tmp = payloadToStr(payload, mesLength);
    if (String(delayBeforeOpenDoor) != tmp)
    {
      Serial.println("Old delay is " + String(delayBeforeOpenDoor) + ". New delays is " + tmp + ".");
      writeFile(tmp);
      delayBeforeOpenDoor = tmp.toInt();
      Serial.println("Delay before Open: " + tmp);
      publishToMQTT("delaybeforeopen", (char*)tmp.c_str());
    }
  } else
  {
      Serial.println("Topic: " + String(topic));
      Serial.println("Payload: " + payloadToStr(payload, mesLength));
  }
}

int connectWifi()
{
    while ( WiFi.status() != WL_CONNECTED ) 
    {
        WiFi.begin();// ssid, pass );
        delay( 2500 );
        Serial.println( "Connecting to Wi-Fi" ); 
    }
    Serial.println( "Connected" );
}

void updateRSSIValue()
{
   long rssi = WiFi.RSSI();  
   Serial.print( "RSSI:" );
   Serial.println(rssi);
   //dataToPublish[ DATA_FIELD ]=float( rssi );
}

Константы и переменные я свел для удобства в отдельный файл.

///File vars.h
#ifndef DEBUG_VARS_H
#define DEBUG_VARS_H

#define LINE_SWITCH_PIN   5   //Switch line to domofon
#define OPEN_DOOR_PIN     4   //Open the door
#define DETECT_DOOR_PIN   13  //D7 - Detect door relay status
#define DETECT_LINE_PIN   12  //D6 - Detect line switch relay status
#define DETECT_CALL_PIN   14  //D5 - Detect call
//#define ACTIVATION_PIN    A0  // - activation switch pin

const int openDoorDelay           = 20*1000;  //ms to open the door
int delayBeforeOpenDoor           = 1000;        //ms before open the door 
const int rebootInterval          = 24*60*60*1000; //reboot ESP8266 once a day
const int resetActivationDelay    = 10*60*1000; //Delay after door is opened before resetting activation

bool doorStatus           = false;
bool lineStatus           = false;
bool callDetectorStatus   = false; 
bool activation           = true;
bool keepactivation       = true;

long openDoorMillis = 0;
long doorOpenedMillis = 0;

#endif

Сохранение параметра delayBeforeOpenDoor в энергонезависимой памяти SPIFF.

//File spiff.h<br />#include &lt;ESP8266WiFi.h&gt;
#include &lt;FS.h&gt;   //Include File System Headers

const char* filename = "/vars.txt";

bool Init_SPIFF();
bool formatSPIFF();
bool writeFile(String str);
String readFile();

int counter = 0;
bool Init_SPIFF()
{
  if(SPIFFS.begin())
  {
    Serial.println("SPIFFS Initialize....ok");
    return true;
  }
  else
  {
    if (counter &lt; 3)
    {
      if (formatSPIFF())
      {
        Init_SPIFF();
      }
      Serial.println("SPIFFS Initialization...failed");
      counter++;
    }
    return false;
  }
}

bool formatSPIFF()
{
  if(SPIFFS.format())
  {
    Serial.println("File System Formated");
    return true;
  }
  else
  {
    Serial.println("File System Formatting Error");
  }
  return false;
}

bool writeFile(String str)
{
  File f = SPIFFS.open(filename, "w");
  
  if (!f) {
    Serial.println("file open failed");
    return false;
  }
  else
  {
      //Write data to file
      Serial.println("Writing Data to File");
      f.print(str);
      f.close();  //Close file
      return true;
  }
}

String readFile()
{
    //Read File data
  File f = SPIFFS.open(filename, "r");
  String result = "";
  
  if (!f) {
    Serial.println("file open failed");
    result = "0";
  }
  else
  {
      Serial.println("Reading Data from File:");
      //Data from file
      for (int i = 0; i &lt; f.size(); i++) //Read upto complete file size
      {
        result += (char)f.read();
      }
      f.close();  //Close file
      Serial.println("File Closed");
  }
  return result;
}
//File passwords.h
char* mqttUserName = "Domophone";
char* mqttPass = "XXXXXXXXXXXXXXXXXX";

Форма предзаказа

Для заказа устройства просьба заполнить форму, или отправить мне e-mail. Я не гарантирую быструю отправку, поскольку часть компонентов изготавливаются в Китае. О примерных сроках и стоимости будет сообщено дополнительно.

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

Добавить комментарий