Мы уже писали о MQTT-брокере и о том, как собрать сенсорный узел на базе Intel Edison. Устройство содержит кнопку, датчики движения, температуры и освещённости. Сегодня подключим всё это к Mosquitto MQTT-серверу, наладим двустороннюю связь, сделаем нашу конструкцию полноценной частью интернета вещей.



MQTT


Модель издатель-подписчик в MQTT


MQTT – это один из популярных протоколов для организации межмашинной коммуникации (M2M, Machine to Machine). Он работает по принципу «издатель-подписчик» и построен на базе TCP/IP. Два главных компонента MQTT – клиент и брокер. MQTT-клиенты публикуют сообщения на определённую тему (topic), или подписываются на сообщения и прослушивают их. MQTT-брокер получает все опубликованные издателями сообщения и перенаправляет релевантные сообщения подписчикам. Подписчики и издатели не должны знать друг о друге, роль играют лишь темы сообщений и сами сообщения. Для организации нормального взаимодействия издатели и подписчики должны использовать общие названия тем и формат сообщений.

Для публикации сообщений, или подписки на сообщения определённой темы, необходим MQTT-клиент. В дистрибутиве Mosquitto MQTT клиент, публикующий сообщения, называется «mosquitto_pub», а клиент, подписанный на сообщения – «mosquitto_sub».
Вот, как выглядит обмен сообщениями, о котором мы будем говорить ниже. В красном окне показана работа клиента-подписчика, в чёрном – клиента-издателя. Команды снабжены метками, это позволит удобно ссылаться на них.


Взаимодействие подписчика и издателя

Минимальный набор аргументов, необходимый для клиентов-подписчиков, это IP-адрес брокера и наименование темы сообщения, а для клиентов-издателей – ещё и текст сообщения. При условии, что MQTT-брокер доступен по адресу 127.0.0.1 (localhost), команда, показанная ниже, включит прослушивание всех сообщений, опубликованных с темой «edison/hello».

sub 3> mosquitto_sub –h localhost –t edison/hello

А вот, как можно опубликовать сообщение с темой «edison/hello».

pub 3> mosquitto_pub –h localhost –t edison/hello –m "Message#1: Hello World!"

Когда MQTT-брокер получает от клиента-издателя (pub 3) сообщение «Message#1: Hello World», опубликованное с темой «edison/hello», брокер просматривает список клиентов, которые подписаны на тему «edison/hello». Обнаружив, что клиент-подписчик (sub 3) подписан на указанную тему, MQTT-брокер перенаправляет сообщение этому клиенту.

Когда подписчик получает сообщение, он выводит его в окно терминала. В командах подписчика sub 3 и издателя pub 3, номер MQTT-порта по умолчанию задан неявно (1833), поэтому он не указывается в командной строке. В командах sub 4 и pub 4 номер порта указан. В командах sub 5 и pub 5 к аргументам подписки был добавлен ключ «-v». Это позволяет выводить в окно терминала не только сообщение, но и название темы. Это будет полезным при подписке на несколько тем с использованием шаблонов. Клиент-подписчик, однажды созданный, автоматически не уничтожается, поэтому дополнительные сообщения, опубликованные с темой «edison/hello», например, с помощью команды pub 6, будут перенаправлены этому подписчику.

Стандартное поведение MQTT-брокера заключается в том, что он отбрасывает сообщение сразу же после того, как оно будет доставлено клиентам. Клиент pub 7 публикует сообщение с новой темой, «edison/hello1», но клиент-подписчик sub 5 ждёт сообщений с темой «edison/hello», поэтому сообщения от pub 7 он не получит. Затем клиент-подписчик, sub 6, подписывается на тему «edison/hello1», но он так же не получит ранее отправленного сообщения с этой темой, так как оно уже было отброшено. Он получит лишь следующие сообщения, опубликованные с этой темой, например, опубликованные в строке pub 8.

Имеются дополнительные параметры, которые предписывают MQTT-брокеру сохранять последнее сообщение, опубликованное с некоей темой. Есть и директивы, позволяющие настраивать качество обслуживания (Quality of Service, QoS), которые влияют на то, как брокер будет доставлять сообщения. Подробности об этих возможностях можно найти в MQTT Wiki.
В зависимости от настройки портов могут понадобиться дополнительные аргументы. Позже мы вернёмся к этому вопросу.

Подписка на несколько тем


Обычно на практике используют MQTT-клиенты, которые подписаны на несколько тем и выполняют различные действия в зависимости от полученных сообщений. Расскажем о MQTT-темах.

MQTT-темы организованы в виде иерархической структуры. Символ «/» используется для выделения логических уровней тем. Вот, как может выглядеть логически сконструированное пространство тем.

Temperature/Building1/Room1/Location1
Temperature/Building1/Room1/Location2
Temperature/Building2/Room2
Temperature/Outdoor/West
Temperature/Outdoor/East
Temperature/Average

Подписчик может подписаться на некую отдельную тему, скажем, на «Temperature/Outdoor/West», или на несколько, используя шаблон.

В MQTT предусмотрены два подстановочных символа: «+» и «#». Символ «+» используется для подписки на темы, находящиеся на одном уровне иерархии. Символ «#» применяется для подписки на все подразделы заданной темы.

Например, подписка на тему «Temperature/#» доставит клиенту сообщения с любой из тем, приведённых в примере. Подписавшись на «Temperature/+», клиент получит лишь сообщения с темой «Temperature/Average», так как все остальные темы находятся на более низком иерархическом уровне. Подробности об организации пространств тем MQTT можно найти на сайте MQTT Wiki.

Настройка безопасного брокера Mosquitto


MQTT-клиент можно подключить к серверу Mosquitto четырьмя основными способами.

  1. Через открытый порт, то есть, без защиты. Это удобно при разработке и тестировании MQTT-устройств перед их практическим использованием. Данные между брокером и клиентом передаются в виде простого текста, без шифрования. Как результат, их несложно перехватить.

  2. С использованием имени пользователя и пароля. При таком способе брокер может проверить подлинность клиента. Однако, данные при передаче не шифруются.

  3. С использованием шифрования TLS-PSK. Клиент и брокер обладают общим секретным ключом, который применяется при установке безопасного соединения. Эта возможность доступна, начиная с Mosquitto 1.3.5.

  4. С применением SSL-шифрования. Это наиболее совершенная схема шифрования и аутентификации, доступная в Mosquitto MQTT. При использовании SSL необходимо получить сертификат сервера от доверенного центра сертификации (Certificate Authority, CA), среди таких центров – Verisign и Equifax. Часто, для тестирования, создают самостоятельно сгенерированный сертификат, которым можно воспользоваться для того, чтобы подписать сертификат сервера. SSL позволяет достичь высокого уровня безопасности, но реализовать такую схему работы сложнее, чем другие. Дело в том, что при применении SSL нужно снабдить подходящими сертификатами все устройства-клиенты. Кроме того, необходимо разработать механизм обновлений для поддержания соответствия сертификатов клиентов и брокера во время эксплуатации системы.

Для настройки безопасного брокера Mosquitto MQTT воспользуйтесь приведёнными ниже конфигурационными директивами. Здесь задействован самостоятельно сгенерированный сертификат, который подходит для тестовых целей. Такие сертификаты не следует использовать на рабочих серверах.

port 1883
password_file /home/mosquitto/conf/pwdfile.txt
log_type debug
log_type error
log_type warning
log_type information
log_type notice
user root
pid_file /home/mosquitto/logs/mosquitto.pid
persistence true
persistence_location /home/mosquitto/db/
log_dest file /home/mosquitto/logs/mosquitto.log

listener 1995
# порт 1995 это безопасный порт TLS-PSK, для подключения клиент должен
# предоставить --psk-identity и --psk
psk_file /home/mosquitto/conf/pskfile.txt
# должен присутствовать psk_hint, или порт 1995 не сможет правильно работать
psk_hint hint

listener 1994
# порт 1994 это безопасный SSL-порт, для подключения клиент должен
# предоставить сертификат, выданный центром сертификации,
# которому доверяет сервер, например, ca.crt
cafile /home/mosquitto/certs/ca.crt
certfile /home/mosquitto/certs/server.crt
keyfile /home/mosquitto/certs/server.key

Файл pwdfile.txt из этого примера это файл паролей, зашифрованный так же, как файл паролей Linux. Его можно создать, воспользовавшись утилитой mosquitto_passwd, которая поставляется с Mosquitto. Файл pskfile.txt это файл паролей, который предварительно размещён на клиентах и на сервере. Он используется для организации соединения с использованием TLS-PSK. Это обычный текстовый файл, каждая строка которого содержит одну пару вида «пользователь: пароль». Пароли, которые используются для TLS-PSK, не чувствительны к регистру и могут содержать только шестнадцатеричные символы. Вот примеры файла паролей и PSK-файла.

/* пример файла паролей Mosquitto */
user:$6$Su4WZkkQ0LmqeD/X$Q57AYfcu27McE14/MojWgto7fmloRyrZ7BpTtKOkME8UZzJZ5hGXpOea81RlgcXttJbeRFY9s0f+UTY2dO5xdg==
/* пример PSK-файла Mosquitto */
user1:deadbeef
user2:DEADBEEF0123456789ABCDEF0123456789abcdef0123456789abcdef0123456789abcdef
<h1>Встраиваем MQTT в сенсорный узел</h1>

Так как Intel Edison – это Linux-система, которая может исполнять программу (скетч) для Arduino как один из своих процессов, можно воспользоваться пакетом Mosquitto MQTT, который мы уже подготовили. В пределах скетча доступны средства программирования Linux для вызова «mosquitto_sub» и «mosquitto_pub». У такого подхода есть несколько преимуществ.

  1. Не нужно переписывать MQTT-клиенты для Arduino. Пакет Mosquitto MQTT – это широко известный и хорошо протестированный код. Нам лишь нужно создать класс-обёртку для того, чтобы можно было воспользоваться им в Arduino-скетче.

  2. Клиенты Mosquitto MQTT поддерживают безопасные SSL-соединения. В то же время, маломощный микроконтроллер в обычном Arduino не соответствует вычислительным требованиям SSL.

  3. Если обнаружится более совершенный MQTT-пакет или выйдет новая версия того, что уже применяется, можно будет повторно использовать большую часть существующего кода, дополнив его поддержкой новшеств.

Класс MQTTClient


Так как Intel Edison работает под управлением полнофункциональной ОС Linux, Arduino-программе доступны все средства разработки под Linux. Создадим класс-обёртку, MQTTClient, которая будет задействована в скетче. В методах MQTTClietn используем системные вызовы Linux для вызова mosquitto_sub и mosquitto_pub, которые, в свою очередь, будут заниматься задачами связи по протоколу MQTT. Вот описание класса MQTTClent, ограниченное минимумом необходимых возможностей.

// Файл: MQTTClient.h
/*
  Минимальный MQTT-клиент, использующий mosquitto_sub и mosquitto_pub
  Исходим из того, что сервер Mosquitto MQTT уже установлен 
  mosquitto_pub/sub находятся в пути поиска файлов
*/

#ifndef __MQTTClient_H__
#define __MQTTClient_H__

#include <Arduino.h>
#include <stdio.h>

enum security_mode {OPEN = 0, SSL = 1, PSK = 2};

class MQTTClient {

  public:
    MQTTClient();
    ~MQTTClient();
    void    begin(char * broker, int port, security_mode mode, 
                  char* certificate_file, char *psk_identity, char *psk);
    boolean publish(char *topic, char *message);
    boolean subscribe(char* topic, void (*callback)(char *topic, char* message));
    boolean loop();
    boolean available();
    void    close();

  private:
    void           parseDataBuffer();
    FILE*          spipe;
    char           mqtt_broker[32];
    security_mode  mode;
    char           topicString[64];
    char           certificate_file[64];
    char           psk_identity[32];
    char           psk_password[32];
    int            serverPort;
    char           *topic;
    char           *message;
    boolean         retain_flag;
    void           (*callback_function)(char* topic, char* message);
    char           dataBuffer[256];
};
#endif

Для того, чтобы этим классом воспользоваться, надо начать с создания объекта MQTTClient, затем вызвать MQTTClient::begin() для инициализации различных переменных, которые будут использовать MQTTClient::subscribe() и MQTTClient::publish() для подключения к MQTT-брокеру. Вот описание этих переменных.

  • mqtt_broker: имя MQTT-брокера. Это может быть IP-адрес или DNS-имя. В данном случае используем «localhost» так как брокер работает на той же плате Intel Edison, что и скетч. Если Edison настроен на использование статического IP-адреса, его можно использовать вместо «localhost».

  • serverPort: используемый порт. Мы назначили Mosquitto-брокеру три разных порта, каждый – с собственной политикой безопасности:

  • 1883 – это порт MQTT по умолчанию, он открыт для всех клиентов.

  • 1994 настроен как безопасный SSL-порт. Клиенты, которые подключаются к такому порту, должны обладать SSL-сертификатами, выданными центром сертификации, который выпустил сертификат сервера.

  • 1995 – это безопасный порт TLS-PSK. Для доступа к нему нужно имя пользователя и пароль, который заранее известен и клиентской, и серверной частям системы.

  • mode: режим безопасности, который следует использовать. В данном примере предусмотрены три режима: OPEN, SSL и PSK. В зависимости от выбранного режима безопасности, в других аргументах метода передаётся сертификат или имя пользователя и пароль. Аргументы, которые не используются в выбранном режиме безопасности, игнорируются.

Для того чтобы подписаться на тему, вызывают метод MQTTClient:subscribe() с именем темы и функцией обратного вызова, которая будет запущена при поступлении нового сообщения. Для публикации сообщения с некоторой темой, вызывают MQTTClient:publish() с указанием имени темы и сообщения.

И в методе подписки на сообщения, и в методе их публикации, формируется соответствующая команда, которая вызывает либо mosquitto_sub, либо mosquitto_pub и использует методы аргумента и сохранённые параметры. Затем открывается канал Linux, команда выполняется, результаты возвращаются по каналу в скетч.

При публикации канал закрывается сразу же после отправки сообщения. При подписке канал необходимо держать открытым для того, чтобы по нему можно было получить новые данные. Метод MQTTClient:loop() создан для периодической проверки наличия данных в канале и вызова функций обратного вызова для обработки новых сообщений. Так как канал Linux будет заблокирован, если при проверке в нём ничего не окажется, мы сделали каналы неблокируемыми. Таким образом, если при проверке в методе MQTTClient:loop() окажется, что канал пуст, произойдёт немедленный возврат из метода. Ниже, в псевдокоде, показано типичное использование класса MQTTClient.

#include <MQTTClient.h>
#define SAMPLING_PERIOD   100
#define PUBLISH_INTERVAL  30000

MQTTClient mqttClient;

void setup() {
   mqttClient.begin("localhost",1833,PSK,NULL,"psk_user","psk_password");
   mqttClient.subscribe("edison/#",myCallBack);
}

void myCallBack(char* topic, char* message) {
   // проверка наличия подходящих тем, команд, полезная нагрузка
}

unsigned long publishTime = millis();
void loop() {
   mqttClient.loop();        // проверка на наличие новых сообщений
   if (millis() > publishTime) {
       publishTime = millis() + PUBLISH_INTERVAL;
       mqttClient.publish("edison/sensor1","sensor1value");
       mqttClient.publish("edison/sensor2","sensor2value");
   }
   delay(SAMPLING_PERIOD);
}

Переменная «SAMPLING_PERIOD» задаёт частоту, с которой будет проверяться наличие новых сообщений. Значение этой переменной следует выбирать так, чтобы сообщения, которые должны вызывать какие-либо действия со стороны Arduino-программы, прибывали не слишком поздно и не теряли смысла.

Большую часть рабочего времени метод MQTTClietn:loop() будет завершаться очень быстро, поэтому частый вызов этого метода создаст очень небольшую нагрузку на систему. Интервал, с которым публикуются сообщения, задан с помощью переменной «PUBLISH_INTERVAL». Длительность интервала должна соответствовать частоте, с которой датчики публикуют данные. Например, датчик температуры может публиковать сведения о температуре в помещении раз в минуту, если его показатели используются в информационных целях. А вот датчику дыма есть смысл публиковать данные раз в несколько секунд для того, чтобы у подписчика, ожидающего его сообщений, в случае срабатывания датчика, был шанс что-то предпринять до того, как станет слишком поздно.

Вот реализация класса MQTTClient.

// Файл: MQTTClient.cpp
#include "MQTTClient.h"
#include <fcntl.h>

/*======================================================================
  Конструктор/Деструктор
========================================================================*/
MQTTClient::MQTTClient()
{
}
MQTTClient::~MQTTClient()
{
  close();
}
void MQTTClient::close()
{
  if (spipe) {
    fclose(spipe);
  }
}
/*========================================================================
  Инициализация. Сохранённые переменные будут использованы в вызовах подписки на темы и публикации сообщений
==========================================================================*/
void MQTTClient::begin(char *broker, int port, security_mode smode, 
                       char* cafile, char *user, char *psk)
{
  strcpy(mqtt_broker, broker);
  serverPort = port;
  mode = smode;
  if (mode == SSL) {
    strcpy(certificate_file, cafile);
  }
  else if (mode == PSK) {
    strcpy(psk_identity, user);
    strcpy(psk_password, psk);
  }
  Serial.println("MQTTClient initialized");
  Serial.print("Broker: "); Serial.println(mqtt_broker);
  Serial.print("Port:   "); Serial.println(serverPort);
  Serial.print("Mode:   "); Serial.println(mode);
}
/*=======================================================================
  Подписка на тему, (*callback) это функция, которую нужно вызвать, когда
  клиент получит сообщение.
=========================================================================*/
boolean MQTTClient::subscribe(char* topic, 
                              void (*callback)(char* topic, char* message))
{
  char cmdString[256];
  
  if (mqtt_broker == NULL) {
    return false;
  }
  if (topic == NULL) {
    return false;
  }
  
  callback_function = callback;
  switch(mode) {
    case OPEN:
      sprintf(cmdString, 
              "mosquitto_sub -h %s -p %d -t %s -v",
              mqtt_broker, serverPort, topic);
      break;
    case SSL:
      sprintf(cmdString, 
              "mosquitto_sub -h %s -p %d -t %s -v --cafile %s", 
               mqtt_broker, serverPort, topic, certificate_file);
      break;
    case PSK:
      sprintf(cmdString, 
              "mosquitto_sub -h %s -p %d -t %s -v --psk-identity %s --psk %s",
              mqtt_broker, serverPort, topic, psk_identity, psk_password);
      break;
    default:
      break;
  }
  if ((spipe = (FILE*)popen(cmdString, "r")) != NULL) {
    // нужно установить неблокирующий режим чтения канала
    int fd    = fileno(spipe);
    int flags = fcntl(fd, F_GETFL, 0);
    flags |= O_NONBLOCK;
    fcntl(fd, F_SETFL, flags);
    strcpy(topicString, topic);
    return true;
  }
  else {
    return false;
  }
}
/*====================================================================
  Проверяем, есть ли данные в канале, 
  если есть, разбираем тему и сообщение и выполняем функцию обратного вызова.
  Возвращаем управление, если канал пуст.
======================================================================*/
boolean MQTTClient::loop()
{
  if (fgets(dataBuffer, sizeof(dataBuffer), spipe)) {    
    parseDataBuffer();    
    callback_function(topic, message);
  }
}
/*====================================================================
  Публикуем сообщение с заданной темой.
======================================================================*/
boolean MQTTClient::publish(char *topic, char *message)
{
  FILE*   ppipe;
  char    cmdString[256];
  boolean retval = false;
  if (this->mqtt_broker == NULL) {
    return false;
  }
  if (topic == NULL) {
    return false;
  }
  switch (this->mode) {
    case OPEN:
      sprintf(cmdString,
              "mosquitto_pub -h %s -p %d -t %s -m \"%s\" %s",
              mqtt_broker, serverPort, topic, message, retain_flag?"-r":"");
      break;
    case SSL:
      sprintf(cmdString,
              "mosquitto_pub -h %s -p %d --cafile %s -t %s -m \"%s\" %s",
               mqtt_broker, serverPort, certificate_file, 
               topic, message, retain_flag?"-r":"");
      break;
    case PSK:
      sprintf(cmdString,
          "mosquitto_pub -h %s -p %d --psk-identity %s --psk %s -t %s -m \"%s\" %s",
              mqtt_broker, serverPort, psk_identity, psk_password, 
              topic, message, retain_flag?"-r":"");
      break;
  }
  if (!(ppipe = (FILE *)popen(cmdString, "w"))) {
    retval = false;
  }
  if (fputs(cmdString, ppipe) != EOF) {
    retval = true;
  }
  else {
    retval = false;
  }
  fclose(ppipe);
  return retval;
}
/*======================================================================
  Разбираем данные из буфера, помещаем их в буферы с темой и 
  сообщением. Разделителем является первый пробел.
  Если в исходном буфере удаётся распознать лишь одну строку, считаем, 
  что это сообщение и устанавливаем буфер темы в NULL
========================================================================*/
void MQTTClient::parseDataBuffer()
{
  topic   = dataBuffer;
  message = dataBuffer;
  while((*message) != 0) {
    if ((*message) == 0x20) {
      // заменяем первый пробел символом NULL
      (*message) = 0;
      message++;
      break;
    }
    else {
      message++;
    }
  }
  if (strlen(message) == 0) {
    topic   = NULL;
    message = dataBuffer;
  }  
}
<h2>Сенсорный узел</h2>

В одной из предыдущих публикаций мы рассказывали о сборке сенсорного узла с датчиками и актуаторами. А именно, вот из чего состоит устройство, которое тогда получилось создать:

  1. Датчик движения для обнаружения перемещений.
  2. Датчик температуры, который позволяет измерять окружающую температуру.
  3. Датчик освещённости, измеряющий освещённость.
  4. Командная кнопка, которая позволяет распознавать целенаправленные действия пользователя. В данном случае – нажатие на кнопку.
  5. Красный светодиод, который включается и выключается в ответ на нажатия на кнопку.
  6. Зелёный светодиод, который включается, когда срабатывает датчик движения.


Сенсорный узел на базе Intel Edison

Используя MQTT, будем периодически публиковать показатели различных датчиков, передавая сообщения брокеру Mosquitto, который выполняется на той же системе Intel Edison. Подпишемся на все темы, то есть, на «edison/#». Кроме того, внесём некоторые изменения в логику работы сенсорного узла:

  1. Командная кнопка больше не будет включать и выключать красный светодиод. Теперь нажатие на неё будет служить сигналом к изменению значения логической переменной, которое будет передано подписчикам MQTT.

  2. Красный светодиод будет включаться и выключаться с помощью MQTT-сообщений. Подобный механизм может играть роль модуля доступа для осветительного прибора, которым можно управлять через интернет.

  3. Поведение зелёного светодиода можно будет настраивать с помощью MQTT. А именно, можно будет сделать так, чтобы он включался при срабатывании датчика движения, либо всё время оставался выключенным.

Вот основной код скетча.

// Файл: MQTT_IoT_Sensor.ino

/******************************************************************************
    Сенсорный узел для интернета вещей, построенный на базе Intel Edison 
******************************************************************************/

#include <stdio.h>
#include <Arduino.h>

#include "sensors.h"
#include "MQTTClient.h"

// глобальные переменные
unsigned long          updateTime = 0;
// переменные датчика движения
volatile unsigned long activityMeasure;
volatile unsigned long activityStart;
volatile boolean       motionLED = true;
unsigned long          resetActivityCounterTime;

// переменные кнопки
boolean                toggleLED = false;
volatile unsigned long previousEdgeTime = 0;
volatile unsigned long count = 0;
volatile boolean       arm_alarm = false;

// MQTT-клиент
#define SECURE_MODE     2
MQTTClient              mqttClient;
char                    fmtString[256];     // вспомогательная переменная
char                    topicString[64];    // тема для публикации
char                    msgString[64];      // сообщение

/*****************************************************************************
  Настройка
******************************************************************************/
void setup() {
  Serial.begin(115200);
  delay(3000);
  Serial.println("Ready");

  pinMode(RED_LED,    OUTPUT);
  pinMode(GREEN_LED,  OUTPUT);
  pinMode(BUTTON,     INPUT_PULLUP);
  pinMode(PIR_SENSOR, INPUT);

  // Прерывание кнопки. Вызывает прерывание при отпускании кнопки
  attachInterrupt(BUTTON,     buttonISR, RISING);  

  // Прерывание датчика движения. Нужны оба значения для нахождения ширины импульса
  attachInterrupt(PIR_SENSOR, pirISR,    CHANGE);
  digitalWrite(RED_LED,  HIGH);
  digitalWrite(GREEN_LED,LOW);
  resetActivityCounterTime = 0; 
 
  // инициализация MQTTClient
#if ( SECURE_MODE == 0 )
  Serial.println("No security");
  mqttClient.begin("localhost", 1883, OPEN, NULL, NULL, NULL);
#elif ( SECURE_MODE == 1 )
  Serial.println("SSL security");
  mqttClient.begin("localhost", 1994, SSL, 
                   "/home/mosquitto/certs/ca.crt", NULL, NULL);
#elif ( SECURE_MODE == 2 )
  Serial.println("TLS-PSK security");
  mqttClient.begin("localhost", 1995, PSK, NULL, "user", "deadbeef");
#endif

  // подписываемся на все подразделы темы edison
  mqttClient.subscribe("edison/#", mqttCallback);
  mqttClient.publish("edison/bootMsg","Booted");
  digitalWrite(RED_LED, LOW);
}
/**************************************************************************
  Функция обратного вызова MQTT
**************************************************************************/
void mqttCallback(char* topic, char* message)
{
  sprintf(fmtString, "mqttCallback(), topic: %s, message: %s",topic,message);
  Serial.print(fmtString);
  // проверим тему
  if (strcmp(topic,"edison/LED") == 0) {
    // затем исполним подходящую команду
    if (message[0] == 'H') {
      digitalWrite(RED_LED, HIGH);
      toggleLED = false;
    }
    else if (message[0] == 'L') {
      digitalWrite(RED_LED, LOW);
      toggleLED = false;
    }
    else if (message[0] == 'B') {
      toggleLED = true;
    }
  }
  if (strcmp(topic, "edison/motionLED") == 0) {
    // обратите внимание на то, что в конце сообщения имеется дополнительный 
    // символ возврата каретки.
    // используем strncmp для того, чтобы убрать последний символ
    if (strncmp(message, "OFF", 3) == 0) {
      digitalWrite(GREEN_LED, LOW);
      motionLED = false;
    }
    else if (strncmp(message, "ON", 2) == 0) {
      motionLED = true;
    }
  }
}
/***********************************************************************
  Главный цикл
***********************************************************************/
void loop() {
  
    // проверяем, есть ли новые сообщения от mqtt_sub
    mqttClient.loop();
    
    if (millis() > resetActivityCounterTime) {
      resetActivityCounterTime = millis() + 60000;
      // публикуем данные об активности, зафиксированной датчиком движения
      sprintf(msgString,"%.0f",100.0*activityMeasure/60000.0);
      mqttClient.publish("edison/ActivityLevel",msgString);
      activityMeasure = 0;
    }
    if (millis() > updateTime) {
      updateTime = millis() + 10000;    
      // публикуем данные о температуре
      sprintf(msgString,"%.1f",readTemperature(TEMP_SENSOR));
      mqttClient.publish("edison/Temperature",msgString);
      
      // публикуем показатели датчика освещённости
      sprintf(msgString,"%d",readLightSensor(LIGHT_SENSOR));
      mqttClient.publish("edison/LightSensor",msgString);
      
      // публикуем arm_alarm
      sprintf(msgString,"%s", (arm_alarm == true)? "ARMED"  : "NOTARMED");
      mqttClient.publish("edison/alarm_status", msgString);
    }
    
    if (toggleLED == true) {
      digitalWrite(RED_LED, digitalRead(RED_LED) ^ 1);
    }
    delay(100);
}

Большая часть этой программы понятна и без объяснений. Так, код работы с датчиками перемещён, для улучшения читабельности программы, в отдельные модули, sensor.h и sensor.cpp.

Установка переменной SECURE_MODE в Arduino-программе позволяет поддерживать различные режимы безопасности. Функция обратного вызова, mqttCallback(), вызывается каждый раз, когда поступает новое сообщение от MQTT-брокера. Тема, на которую мы подписались, и текст сообщения, передаются функции обратного вызова. Там мы их анализируем и, если оказалось, что при получении сообщения нужно некое заранее заданное действие, это действие выполняем.

Функция обратного вызова – это главный механизм, реализующий управление IoT-датчиками извне. В данном случае сообщения с темой «edison/LED» используются для включения или выключения красного светодиода, а так же для перевода его в режим мигания. Сообщения с темой «edison/motionLED» применяются для настройки поведения зелёного светодиода. Он либо включается при срабатывании датчика движения, либо всё время остаётся выключенным.

// Файл: sensors.h
//
#define USE_TMP036     0

#define RED_LED       10            // Красный светодиод
#define GREEN_LED     11            // Зелёный светодиод
#define BUTTON        13            // Командная кнопка с нагрузочным резистором на 10K
#define PIR_SENSOR    12            // Инфракрасный датчик движения
#define LIGHT_SENSOR  A0            // датчик освещённости
#define TEMP_SENSOR   A1            // датчик температуры TMP36 или LM35

#define MIN_PULSE_SEPARATION     200   // для исключения дребезга кнопки
#define ADC_STEPSIZE             4.61  // в мВ, для конверсии данных о температуре.

#if (USE_TMP036 == 1)
#define TEMP_SENSOR_OFFSET_VOLTAGE       750
#define TEMP_SENSOR_OFFSET_TEMPERATURE   25
#else // LM35 temperature sensor
#define TEMP_SENSOR_OFFSET_VOLTAGE        0
#define TEMP_SENSOR_OFFSET_TEMPERATURE    0
#endif

// Глобальные переменные
extern unsigned long          updateTime;
// Переменные датчика движения
extern volatile unsigned long activityMeasure;
extern volatile unsigned long activityStart;
extern volatile boolean       motionLED;
extern unsigned long          resetActivityCounterTime;

// Переменные кнопки
extern boolean                toggleLED;
extern volatile unsigned long previousEdgeTime;
extern volatile unsigned long count;
extern volatile boolean       arm_alarm;
float readTemperature(int analogPin);
int   readLightSensor(int analogPin);
void  buttonISR();
void  pirISR();

// Файл: sensors.cpp
#include <Arduino.h>
#include "sensors.h"
/***********************************************************************************
  Датчик движения
  Каждый раз, когда датчик фиксирует перемещение, выход переводится в состояние HIGH и остаётся в нём всё время, пока длится движение.
  Когда движение прекращается, выход остаётся в состоянии HIGH ещё 2-3 секунды, затем переводится в состояние LOW.
  Подсчитываем время нахождения выхода датчика в состоянии HIGH, это служит мерой оценки длительности зафиксированных движений.
  Главный цикл сообщает об уровне активности в предыдущем временном интервале в виде процентной величины
************************************************************************************/
void pirISR()
{
  int pirReading;
  unsigned long timestamp;
  timestamp = millis();
  pirReading = digitalRead(PIR_SENSOR);
  if (pirReading == 1) {
    // отмечаем начало импульса
    activityStart = timestamp;
  }
  else {
    int pulseWidth = timestamp-activityStart;
    activityMeasure += pulseWidth;
  }
  // когда движение зафиксировано, включаем зелёный светодиод
  if (motionLED == true) {
    digitalWrite(GREEN_LED, pirReading);
  }
}
/************************************************************************
  возврат показателей датчика освещённости
************************************************************************/
int readLightSensor(int sensorPin)
{
  return analogRead(sensorPin);
}
/***********************************************************************
  возврат данных о температуре в градусах Фаренгейта
***********************************************************************/
float readTemperature(int sensorPin)
{
  int   sensorReading;
  float temperature;
  sensorReading = analogRead(sensorPin);
  // конвертируем в милливольты
  temperature = sensorReading * ADC_STEPSIZE;
  // И датчик LM35, и TMP036, обладают одинаковыми характеристиками: 
  // 10 мВ на градус Цельсия
  // Смещение напряжения у LM35 – 0 мВ, у TMP036 - 750 мВ
  // Смещение температуры у LM35 – 0 градусов Цельсия, у TMP036 – 25 градусов.
  temperature = (temperature - TEMP_SENSOR_OFFSET_VOLTAGE)/10.0 +
                 TEMP_SENSOR_OFFSET_TEMPERATURE;
  // конвертируем в градусы фаренгейта
  temperature = temperature * 1.8 + 32.0;        
  return temperature;
}
/*************************************************************************
  Обработчик прерываний для кнопки
**************************************************************************/
void buttonISR()
{
  // если текущее нажатие оказалось слишком близко к предыдущему, считаем это дребезгом
  if ((millis()-previousEdgeTime) >= MIN_PULSE_SEPARATION) {
    arm_alarm = !arm_alarm;
    Serial.print("Alarm is: "); 
    if (arm_alarm == true) {
      Serial.println("ARMED");
    }
    else {
      Serial.println("NOT ARMED");
    }
    count++;
    Serial.print("Count: "); Serial.println(count);
  }
  previousEdgeTime=millis();
}

Тестирование


Для тестирования сенсорного узла, который теперь вполне вписывается в концепцию интернета вещей, понадобится использовать MQTT-клиент для подписки на темы, сообщения с которыми публикует устройство, и ещё один клиент – для публикации сообщений, на которые устройство будет реагировать.

К Edison можно подключиться по SSH и воспользоваться командами mosquitto_sub/pub для локального наблюдения за сенсорным узлом и для организации управления. Можно использовать и другую хост-систему, на которой установлен пакет Mosquitto.
Итак, для тестирования сенсорного узла выполним следующее.

На клиенте, который будет принимать сообщения, подпишемся на все темы:

$> mosquitto_sub –h ipaddr –p 1883 –t edison/# -v

Здесь ipaddr – это IP-адрес платы Intel Edison. Если команда выполняется непосредственно на Edison, то в качестве IP-адреса нужно использовать «localhost». В данном случае для подключения к брокеру используем открытый порт, 1883. Можно использовать и порт 1994, тогда понадобится SSL-сертификат. Для работы с портом 1995, который поддерживает PSK, нужны будут имя пользователя и пароль. После успешной подписки на тему «edison/#» на экране должны появиться сообщения с показателями от датчиков и другие данные, которые опубликовал соответствующий клиент.

На клиенте, который занимается публикацией данных, опубликуем сообщение с темой «edison/LED», которое позволит управлять красным светодиодом, или с темой «edison/motionLED», с помощью которого можно задавать режимы работы зелёного светодиода.

$> mosquitto_pub –h ipaddr –p 1883 –t Edison/LED –m {H, L, B}
$> mosquitto_pub –h ipaddr –p 1883 –t Edison/motionLED –m {ON, OFF}

Красный светодиод в ответ на опубликованные сообщения (H, L, B) должен включаться, выключаться или мигать.
Для того, чтобы управлять зелёным светодиодом, настраивать то, как он реагирует на срабатывание датчика движения, используются сообщения ON и OFF.

Мы подготовили видео, в котором показан процесс тестирования.

Выводы


Вы узнали о том, как взять сравнительно сложную программную систему, Mosquitto MQTT, и наладить работу с ней из скетча с помощью механизмов программирования для Linux на плате Intel Edison. Тот же принцип поможет найти подход к множеству библиотек, разработанных для платформы Arduino и рассчитанных на поддержку различных датчиков. Используя Linux, которая, в данном случае, является базой для Arduino-программ, можно легко задействовать пакеты с открытым кодом, созданные для Linux.
Поделиться с друзьями
-->

Комментарии (2)


  1. nicelight_nsk
    13.05.2016 00:35

    не совсем понятно. система работает только на жлелезе intel edisson?


  1. CGen
    13.05.2016 12:09
    +1

    Я правильно понимаю, что Arduino эмулируется каким-то Linux-процессом?