Отправка и приём сообщений, описание

О программировании Arduino, использование библиотек, примеры и общие вопросы программирования.
Ответить
Mikhail72
Сообщения: 18
Зарегистрирован: 04 окт 2017, 06:02
Откуда: Тюмень
Контактная информация:

Отправка и приём сообщений, описание

Сообщение Mikhail72 »

В этой теме будет описываться всё, что связано отправкой и приемом сообщений.

Итак, первый пост хочу начать с описания принципа работы отправки сообщений в MySensors, т.к. понятной для любителя информации на родном языке мало, поэтому эта тема остаётся не раскрытой. В следствие чего при создании сенсоров может возникать неправильная отработка алгоритма. В MyS существует два понятия подтверждения: первое подтверждение отправки (no Ack), второе подтверждение доставки (Ack).
Подтверждение отправки присутствует по умолчанию, функция bool send(); возвращает true если, как трактуют разработчики, сообщение достигло промежуточного узла, которым может выступать репитер или гейт. Существует несколько способов обработки этой функции, но самая простая val = send(msg.set(lightLevel)); а далее прописываете свои условия в зависимости от значения переменной bool val(true или false).
Подтверждение доставки включается добавлением true, в функцию bool send(); например вот так send(msg.set(lightLevel), true); В таком случае получатель должен послать отправителю подтверждающее сообщение. И для того чтобы обработать это сообщение необходимо в функцию void receive(const MyMessage &message) добавить

Код: Выделить всё

void receive(const MyMessage &message){
  if (message.isAck()){
    bool ask = true;
  }
}
Ну и далее использовать полученное значение по вашему усмотрению.
На изображении показан лог обмена сообщениями
log общ.jpg
log общ.jpg (121.37 КБ) 22663 просмотра
Если вдаваться в подробности суть обмена состоит в следующем, гейт получив сообщение от отправителя пересылает его получателю и возвращает его же отправителю для подтверждения отправки, если в сообщении стоит флаг подтверждения доставки, перед отправкой обратно флаг снимается, чтобы не попасть в замкнутый цикл, а получатель получив сообщение с флагом отсылает его обратно для подтверждения доставки.
P.S. Если кто-то в это не верит читайте библиотеку и проводите тесты.
Mikhail72
Сообщения: 18
Зарегистрирован: 04 окт 2017, 06:02
Откуда: Тюмень
Контактная информация:

Re: Отправка и приём сообщений, описание

Сообщение Mikhail72 »

Едем дальше, по просьбе трудящихся, приведу примеры как правильно использовать подтверждение. Примеры для стандартного применения, т.е. когда общение идёт через гейт, либо репитер.
В своей системе сбора, хранения и обработки информации по протоколу MyS вы можете использовать до 255 Нод(Микроконтроллер(Arduino)+Радио(NRF24l01)) и когда их много сообщения без подтверждения могут не доходить до получателя, т.к. может начаться одновременная передача с нескольких нод и конечный узел не получит данные из-за "каши" в эфире. Вопрос по очерёдности отправки сообщений остаётся открытый, т.к. для этого необходимо использовать прерывание с выхода IRQ, а то что ноды вещают одновременно установлено опытным путём.
И так у вас есть датчик движения или датчик открытия двери или подобные и вы хотите отправить с него данные в контроллер (например MajorDoMo), всё что идёт до loop() я не рассматриваю, это можно найти в примерах. Рассмотрим пример когда датчик на батарейном питании и соответственно большую часть времени спит, а для пробуждения будем использовать прерывание.

Код: Выделить всё

void loop() {
int8_t slumber = smartSleep(digitalPinToInterrupt(DIGITAL_INPUT_SENSOR), CHANGE, SLEEP_TIME * 1000); // Засыпаем
 if (slumber == 0){                                            // Если проснулись по прерыванию
  bool send_data = send(motionmsg.set(DIGITAL_INPUT_SENSOR));  // Отправка сосотояния датчика
  wait(100, 1, V_STATUS);                                      // Ждём подтверждения отправки
  if (send_data == 1){                                         // Если сообщение отправлено
    Serial.println("Message sent");                            // Вывести в сериал "Сообщение отправлено"
  }
Этот код будет отправлять сообщения и вы даже увидите в мониторе порта, что сообщение отправлено, но если у вас много нод и совпадёт одновременная передача, то сообщение может и не дойти до гейта, а для этого необходимо дописать повторные отправления

Код: Выделить всё

while (send_data == 0) {                                     // Пока статус отправки ложь
      count++;                                                 // Включаем счётчик
      Serial.print("Sending a message, try No.");              // Выводим в монитор порта попытку отправки
      Serial.println(count);                                   // и её номер
      send_data = send(motionmsg.set(DIGITAL_INPUT_SENSOR));   // Отправляем сообщение
      wait(1000, 1, V_STATUS);                                 // Ждём подтверждения отправки
      if (send_data == 1){                                     // Если сообщение отправлено
        Serial.println("Message sent");                        // Вывести в сериал "Сообщение отправлено"
      if ((count ==  3 )&&(send_data == 0)){                   // Если сделано 3 попытки и нет подтверждения отправки
        count = 0;                                             // Обнуляем счётчик
        send_data = 1;                                         // Выходим из цикла
        Serial.println("Send failed");                         // Выводим сообщение "Отправка не удалась"
        }
        }
   }
Если вы отсылаете на гейт, то в принципе этого достаточно, но если получатель у вас нода, которая по датчику движения включает свет или тревогу, то этого кода уже не достаточно, т.к. при проблемах с последней в мониторе порта датчика движения вы увидите, что сообщение отправлено и оно действительно ушло, но только либо до гейта, либо до репитера. А вот тут то и нужно использовать подтверждение доставки(флаг Ack) немного изменяем код

Код: Выделить всё

void loop() {
int8_t slumber = smartSleep(digitalPinToInterrupt(DIGITAL_INPUT_SENSOR), CHANGE, SLEEP_TIME * 1000); // Засыпаем
 if (slumber == 0){                                            // Если проснулись по прерыванию
  bool send_data = send(motionmsg.set(DIGITAL_INPUT_SENSOR), true);  // Отправка сосотояния датчика c подтверждением
  wait(100, 1, V_STATUS);                                      // Ждём подтверждения отправки
  if ((send_data == 1)&&(Ack == 1)){                            // Если сообщение отправлено и доставлено
    Ack = 0;                                                   // Сбрасываем флаг доставки
    Serial.println("Message sent and delivered");              // Вывести в сериал "Сообщение отправлено и доставлено"
  } else {                                                     // Иначе
  while ((send_data == 0) || (Ack == 0)) {                     // Пока статус отправки или доставки ложь
      count++;                                                 // Включаем счётчик
      Serial.print("Sending a message, try No.");              // Выводим в монитор порта попытку отправки
      Serial.println(count);                                   // и её номер
      send_data = send(motionmsg.set(DIGITAL_INPUT_SENSOR),true); // Отправляем сообщение повторно
      wait(1000, 1, V_STATUS);                                 // Ждём подтверждения отправки
      if ((send_data == 1)&&(Ack == 1)){                        // Если сообщение отправлено и доставлено
        Ack = 0;                                               // Сбрасываем флаг доставки
        count = 0;                                             // и счётчик
        Serial.println("Message sent and delivered");          // Вывести в сериал "Сообщение отправлено и доставлено"
      if ((count ==  3 )&& (Ack == 0)){                        // Если сделано 3 попытки и нет подтверждения доставки
        count = 0;                                             // Обнуляем счётчик
        Ack = 1;
        send_data = 1;                                         // Выходим из цикла
        Serial.println("Delivery failed");                     // Выводим сообщение "Доставка не удалась"
        }
        }
   }
  }
 }
 if (slumber == -1){                                            // Если проснулись по таймеру
 wait(1000);                                                   // Ждём для установления связи и выравнивания заряда АКБ
 int sensorValue = analogRead(BATTERY_SENSE_PIN);              // Делаем замер АКБ
 int batteryPcnt = sensorValue / 10;
 if (oldBatteryPcnt != batteryPcnt) {
    sendBatteryLevel(batteryPcnt);                             // Отправляем и надеемся, что дойдёт :-)
    oldBatteryPcnt = batteryPcnt;
    }
}
}
void receive(const MyMessage &message)                         // Функция обработки входящих сообщений
{
if (message.isAck()){                                          // Если пришло сообщение подтверждающее доставку
  Ack = 1;                                                     // Устанавливаем флаг доставки
}
void sendHeartbeat();                                          // отправляем время работы со старта в мс. для подтверждения что жив
И теперь вы можете быть уверены, что сообщение достигнет адресата. :)
Последний раз редактировалось Mikhail72 24 окт 2017, 11:32, всего редактировалось 2 раза.
Mikhail72
Сообщения: 18
Зарегистрирован: 04 окт 2017, 06:02
Откуда: Тюмень
Контактная информация:

Re: Отправка и приём сообщений, описание

Сообщение Mikhail72 »

Что такое конструктор сообщений и как им пользоваться.

Код: Выделить всё

// Setters for building message "on the fly"
MyMessage& setType(uint8_t type);
MyMessage& setSensor(uint8_t sensor);
MyMessage& setDestination(uint8_t destination);
 
// Setters for payload
MyMessage& set(void* payload, uint8_t length);
MyMessage& set(const char* value);
MyMessage& set(uint8_t value);
MyMessage& set(float value, uint8_t decimals);
MyMessage& set(unsigned long value);
MyMessage& set(long value);
MyMessage& set(unsigned int value);
MyMessage& set(int value);
 
// Getter for ack-flag. Returns true if this is an ack message.
bool isAck() const;
 
// Getters for picking up payload on incoming messages
char* getStream(char *buffer) const;
char* getString(char *buffer) const;
const char* getString() const;
void* getCustom() const;
uint8_t getByte() const;
bool getBool() const;
float getFloat() const;
long getLong() const;
unsigned long getULong() const;
int getInt() const;
unsigned int getUInt() const;
В постах выше была описана передача сообщений с ноды на гейт, но еще возникает необходимость передавать сообщения с ноды на ноду и обмениваться не только перемененными типа bool, для этого и существуют настройки приведённые выше. Setters для исходящих сообщений, Getters для входящих.
Если вы хотите отправить сообщение c ноды на ноду, вы должны написать send(msg.setDestination(ID ноды).setSensor(ID сенсора).set(Данные)); Типы данных представлены в Setters for payload, для отправки, например, сообщения с типом данных float и подтверждением доставки нужно писать send(msg.setDestination(ID ноды).setSensor(ID сенсора).set((Данные),decimals),true); где decimals - количество знаков после запятой. Для отправки строки Alarm запись будет выглядеть send(msg.setDestination(ID ноды).setSensor(ID сенсора).set(String("Alarm").c_str()),true); или String val = String("Alarm"); send(msg.setDestination(ID ноды).setSensor(ID сенсора).set(val.c_str()),true); но нужно учитывать, что длинна строки ограничена размером буфера.
Для обработки входящих сообщений используйте Getters, в посту выше уже описывалось как использовать bool isAck() const; остальное для получения входящих данных, например, вам нужно получить данные типа float или int или string для этого необходимо записать

Код: Выделить всё

void receive(const MyMessage &message)
{
if (message.type ==  V_TEMP) {
	float temp = message.getFloat();
}
if (message.type ==  V_DISTANCE) {
	int dist = message.getInt();
}
if (message.type ==  V_TEXT) {
	string txt = message.getString();
}
}
Вроде бы зависимость понятна. В место и совместно с message.type можно использовать message.sensor или message.sender и сравнивать их с номерами либо сенсора, либо отправителя.
Теперь я думаю каждый сможет конструировать, отправлять и принимать сообщения, а также пользоваться подтверждением отправки и доставки. Если вспомню еще что-нибудь интересное по этой теме, то напишу ещё пост, но скорее всего в дальнейшем речь пойдёт о не стандартном подходе использования MyS.
Mikhail72
Сообщения: 18
Зарегистрирован: 04 окт 2017, 06:02
Откуда: Тюмень
Контактная информация:

Re: Отправка и приём сообщений, описание

Сообщение Mikhail72 »

Для целостности выше описанной картины решил выложить готовый пример, скетч написан для спящей ноды (id_103), которая по нажатию кнопки просыпается и отправляет на ноду-реле (id_104) команды для включения и выключения света, также используется таймер для отсылки каждый час заряда батареи и heartbeat на контроллер. Скетч рабочий, но приведён именно в такой компоновке для примера. Для корректной работы, в случае использования обычной кнопки, необходимо предусмотреть аппаратную защиту от дребезга контактов. Программную, в данном случае, я не знаю как правильно написать и возможна ли она вообще в данном примере? Если есть реально работающий программный метод для этого случая, напишите его реализацию.

Код: Выделить всё

#define MY_RADIO_NRF24
#define MY_DEBUG 
#define MY_NODE_ID 103

#include <SPI.h>
#include <MySensors.h>

int BATTERY_SENSE_PIN = A0;         // Аналоговый пин к которому подключен + батареи
unsigned long SLEEP_TIME = 3600;    // Время сна, в секундах
#define DIGITAL_INPUT_SENSOR 2      // Вход с прерыванием для датчика или кнопки
#define CHILD_ID_STATE 0            // Id сенсоры ноды (0-254)

int oldBatteryPcnt = 0;
bool Ack, err;
byte count;
bool state = 1;
int8_t slumber;

MyMessage msg_state(CHILD_ID_STATE, V_STATUS);

void setup() {
#if defined(__AVR_ATmega2560__)
  analogReference(INTERNAL1V1);
#else
  analogReference(INTERNAL);
#endif
  pinMode(DIGITAL_INPUT_SENSOR, INPUT_PULLUP);

}
void presentation()  {
  
  sendSketchInfo("Button", "1.0");
  present(CHILD_ID_STATE, S_BINARY);
  }
void loop() {
slumber = smartSleep(digitalPinToInterrupt(DIGITAL_INPUT_SENSOR), RISING, SLEEP_TIME * 1000); // Засыпаем
 if (slumber == 0){                                            // Если проснулись по прерыванию
  send(msg_state.setDestination(104).setSensor(CHILD_ID_STATE).set(state), true);  // Отправка состояния датчика c подтверждением на ноду 104
  wait(50, 1, V_STATUS);                                       // Ждём подтверждения отправки
  wait(500, 1, V_STATUS);                                      // Ждём подтверждения доставки
  while (Ack == 0) {                                           // Пока статус доставки ложь
      count++;                                                 // Включаем счётчик повторных отправок
      Serial.print("Sending a message, try No.");              // Выводим в монитор порта попытку отправки
      Serial.println(count);                                   // и её номер
      send(msg_state.setDestination(104).setSensor(CHILD_ID_STATE).set(state), true); // Отправляем сообщение повторно
      wait(50, 1, V_STATUS);                                   // Ждём подтверждения отправки
      wait(1000, 1, V_STATUS);                                 // Ждём подтверждения доставки
      if (count ==  3){                                        // Если сделано 3 попытки
        err = 1;                                               // Устанавливаем флаг ошибки
        count = 0;                                             // Обнуляем счётчик
        Ack = 1;                                               // Выходим из цикла
        }
      }
  if ((Ack == 1) && (err == 1)){                               // Если выражение истина
    Serial.println("Delivery failed");                         // Выводим сообщение "Доставка не удалась"
    Ack = 0;                                                   // Сбрасываем флаг доставки
    err = 0;                                                   // И ошибки
  } else if ((Ack == 1) && (err == 0 )){                       // Иначе если выражение истина
           state = !state;                                     // Меняем статус на противоположный для следующей отправки
           Serial.println("Message sent and delivered");       // Вывести в сериал "Сообщение отправлено и доставлено"
           Ack = 0;                                            // Сбрасываем флаг
           count = 0;                                          // Обнуляем счётчик
           }
 }
  if (slumber == -1){                                           // Если проснулись по таймеру
 wait(1000);                                                   // Ждём для установления связи и выравнивания заряда АКБ
 int sensorValue = analogRead(BATTERY_SENSE_PIN);              // Делаем замер АКБ
 int batteryPcnt = sensorValue / 10;
 if (oldBatteryPcnt != batteryPcnt) {
    sendBatteryLevel(batteryPcnt);                             // Отправляем и надеемся, что дойдёт :-)
    oldBatteryPcnt = batteryPcnt;
    }
}

}
void receive(const MyMessage &message)                         // Функция обработки входящих сообщений
{
if (message.isAck()){                                          // Если пришло сообщение подтверждающее доставку
  Ack = 1;                                                     // Устанавливаем флаг доставки
}
}
Ну и скетч для реле

Код: Выделить всё

#define MY_NODE_ID 104
#define MY_RADIO_NRF24
#define MY_DEBUG

#include <MySensors.h>
#include <SPI.h>

bool state = 0;
void setup()
{
pinMode(4, OUTPUT);
}
void loop(){
digitalWrite(4, state);
}
void receive(const MyMessage &message)
{
if (message.type ==  V_STATUS) {
	state = message.getBool();
}
}
void presentation()
{
sendSketchInfo("Relay", "1.0");
wait (20);
present (0,  S_BINARY);
}
Mikhail72
Сообщения: 18
Зарегистрирован: 04 окт 2017, 06:02
Откуда: Тюмень
Контактная информация:

Re: Отправка и приём сообщений, описание

Сообщение Mikhail72 »

Теперь о нестандартном использовании MySensors. Но для начала, спасибо, Berk за находку этого функционала и Sergey2055 за тестирование в боевых условиях. Принцип работы этого скетча такой же как и в предыдущем сообщении, но есть интересные плюшки:
1. Отправка идёт по короткому пути, напрямую с ноды на ноду, минуя промежуточные узлы, таким образом увеличивается быстродействие и снижается нагрузка на сеть, т.к. вместо 5 сообщений с подтверждением доставки всего 2.
2. Подтверждением доставки является удачная отправка.
3. Передача сообщений между этими нодами работает даже при отсутствии гейта, если гейт включается в работу, то без проблем получает данные о заряде АКБ от ноды-кнопки.
4. В отличие от пассивного режима ноды регистрируются на гейте и эта информация отображается на контроллере (MajorDoMo).
Единственное что не тестировалось в таком исполнении, это подписи и шифрование, т.к. нет подопытных с таким функционалом :)
Кнопка

Код: Выделить всё

#define MY_TRANSPORT_WAIT_READY_MS 1000 //Ожидание готовности транспорта перед запуском setup и loop, если транспорт не готов
#define MY_RADIO_NRF24
#define MY_DEBUG 
#define MY_NODE_ID 103
#define MY_PARENT_NODE_ID 104           // Родитель для этой ноды
#define MY_PARENT_NODE_IS_STATIC        // В случае если родитель пропал не ищем другого

#include <SPI.h>
#include <MySensors.h>

int BATTERY_SENSE_PIN = A0;         // Аналоговый пин к которому подключен + батареи
unsigned long SLEEP_TIME = 3600;    // Время сна, в секундах
#define DIGITAL_INPUT_SENSOR 2      // Вход с прерыванием для датчика или кнопки
#define CHILD_ID_STATE 0            // Id сенсоры ноды (0-254)

int oldBatteryPcnt = 0;
bool s, err;
byte count;
bool state = 1;
int8_t slumber;

MyMessage msg_state(CHILD_ID_STATE, V_STATUS);

void setup() {
transportSwitchSM(stReady);                                   // Запуск транспорта принудительно
#if defined(__AVR_ATmega2560__)
  analogReference(INTERNAL1V1);
#else
  analogReference(INTERNAL);
#endif
  pinMode(DIGITAL_INPUT_SENSOR, INPUT_PULLUP);

}
void presentation()  {
  _transportConfig.parentNodeId = 0;                           // Конфигурируем отправку на контроллер
  sendSketchInfo("Button", "2.0");
  wait(20);
  present(CHILD_ID_STATE, S_BINARY);
  }
void loop() {
slumber = sleep(digitalPinToInterrupt(DIGITAL_INPUT_SENSOR), FALLING, SLEEP_TIME * 1000); // Засыпаем
 if (slumber == 0){                                            // Если проснулись по прерыванию
  _transportConfig.parentNodeId = 104;                         // Конфигурируем отправку на реле
  s = send(msg_state.setDestination(104).setSensor(CHILD_ID_STATE).set(state));  // Отправка сосотояния датчика на ноду 104
  wait(100, 1, V_STATUS);                                      // Ждём подтверждения доставки
  while (s == 0) {                                             // Пока статус доставки ложь
      count++;                                                 // Включаем счётчик повторных отправок
      Serial.print("Sending a message, try No.");              // Выводим в монитор порта попытку отправки
      Serial.println(count);                                   // и её номер
      _transportConfig.parentNodeId = 104;                     // Конфигурируем отправку на реле
      s = send(msg_state.setDestination(104).setSensor(CHILD_ID_STATE).set(state)); // Отправляем сообщение повторно
      wait(1000, 1, V_STATUS);                                 // Ждём подтверждения доставки
      if (count ==  3){                                        // Если сделано 3 попытки
        err = 1;                                               // Устанавливаем флаг ошибки
        count = 0;                                             // Обнуляем счётчик
        s = 1;                                                 // Выходим из цикла
        }
     }
  }
  if ((s == 1) && (err == 1)){                                 // Если выражение истина
    Serial.println("Delivery failed");                         // Выводим сообщение "Доставка не удалась"
    s = 0;                                                     // Сбрасываем флаг доставки
    err = 0;                                                   // И ошибки
  } else if ((s == 1) && (err == 0 )){                         // Иначе если выражение истина
           state = !state;                                     // Меняем статус на противоположный для следующей отправки
           Serial.println("Message sent and delivered");       // Вывести в сериал "Сообщение отправлено и доставлено"
           s = 0;                                              // Сбрасываем флаг
           count = 0;                                          // Обнуляем счётчик
           }
 if (slumber == -1){                                           // Если проснулись по таймеру
  wait(1000);                                                  // Ждём для установления связи и выравнивания заряда АКБ
  int sensorValue = analogRead(BATTERY_SENSE_PIN);             // Делаем замер АКБ
  int batteryPcnt = sensorValue / 10;
  if (oldBatteryPcnt != batteryPcnt) {
    _transportConfig.parentNodeId = 0;                         // Конфигурируем отправку на контроллер
    sendBatteryLevel(batteryPcnt);                             // Отправляем на контроллер и надеемся, что дойдёт :-)
    oldBatteryPcnt = batteryPcnt;
    }
}

}
Реле

Код: Выделить всё

#define MY_TRANSPORT_WAIT_READY_MS 1000
#define MY_NODE_ID 104
#define MY_PARENT_NODE_ID 0
#define MY_PARENT_NODE_IS_STATIC
#define MY_RADIO_NRF24
#define MY_DEBUG

#include <MySensors.h>
#include <SPI.h>

bool state = 0;
void setup()
{
transportSwitchSM(stReady);
pinMode(4, OUTPUT);
}
void loop(){
digitalWrite(4, state);
}
void receive(const MyMessage &message)
{
if (message.type ==  V_STATUS) {
	state = message.getBool();
}
}
void presentation()
{
sendSketchInfo("Relay", "2.0");
wait (20);
present (0,  S_BINARY);
}
ahelper
Сообщения: 7
Зарегистрирован: 21 янв 2018, 16:28

Re: Отправка и приём сообщений, описание

Сообщение ahelper »

Gate периодически в эфир отправляет какие то данные? Если да, то как получить эти данные? Имею ввиду сообщения Internal.
Berk
Сообщения: 81
Зарегистрирован: 11 окт 2017, 22:05

Re: Отправка и приём сообщений, описание

Сообщение Berk »

Вы имеете в виду I_DISCOVER_REQUEST? Просто не совсем ясно... Давайте сначала опишите суть того что вы хотите сделать.. лучше наверное отдельным топиком.
Ответить