logo
+7 (951) 999-89-94
428003, г. Чебоксары, ул. Федора Гладкова, д.9, оф.319

Yotster Lite — Пример LoRa Duplex


В этой статье мы продолжим изучение программирования беспроводного LoRa контроллера Yotster Lite и разберём создание дуплексного (duplex) приёмопередатчика LoRa пакетов. Дуплексного — это значит одновременно передающего и принимающего беспроводные пакеты.

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

Если вы ещё не знакомы с документацией и руководствами по контроллеру Yotster Lite, то рекомендуем перед чтением этой статьи сначала ознакомиться с Обзором, Спецификациями и подключением и Основами программирования Yotster Lite по указанным ссылкам.

Дуплексный режим работы

Для этого урока нам понадобятся две платы Yotster Lite, обе одновременно будут работать и передатчиками LoRa пакетов и их приёмниками. Каждая плата будет постоянно отправлять в эфир LoRa пакеты и при этом принимать пакеты, приходящие от соседней платы.

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

Беспроводная адресация

Когда контроллер отправляет в эфир LoRa пакет, то этот пакет становится доступным всем LoRa приёмникам в пределах дальности действия передатчика. Поэтому, если вы используете больше двух приёмников и передатчиков, возникает необходимость введения адресации пакетов, чтобы контроллеры могли различать кому какой пакет предназначен.

В данном примере используется простая 1-байтовая адресация, когда адрес назначения пакета может принимать значения от 0 до 255. Также и каждый беспроводной контроллер должен иметь свой (self) уникальный адрес из того же диапазона от 0 до 255.

Для случая, когда пакет предназначен сразу всем контроллерам, в системе зарезервирован специальный адрес 255 (0xFF), называемый широковещательным (broadcast).

Скетч LoRa Duplex

Теперь давайте рассмотрим скетч LoRa Duplex сначала целиком, а затем подробно разберём все аспекты его работы. Вот полный код скетча:

/*
  LoRa Duplex
  (based on standard example of LoRa Library)
  (c)2020 Electromicro
  License: GNU GPL 2.1
  
  A simple example of LoRa Duplex communication.
*/

#include <SPI.h>
#include <LoRa.h>

#define SELF_ADDR 0xAA
#define BROADCAST 0xFF

byte destAddr = BROADCAST;
long lastSendTime = 0;
byte count = 0;
int interval = 2000;

// Setup

void setup() {
  Serial.begin(115200);
  Serial.println("Start LoRa Duplex");

  if (!LoRa.begin(868E6)) {
    Serial.println("LoRa init failed");
    while(true);
  }
}

// Send

void sendMessage(String mess) {
  LoRa.beginPacket();
    LoRa.write(destAddr);
    LoRa.write(SELF_ADDR);
    LoRa.write(count);
    LoRa.write(mess.length());
    LoRa.print(mess);
  LoRa.endPacket();
  
  count++;
}

void onSend() {
  String message = "Hello!";
  sendMessage(message);
  Serial.println("Sending " + message);
  lastSendTime = millis();
  interval = random(2000) + 1000;
}

// Receive

void onReceive(int packetSize) {
  if (packetSize == 0) {return;}

  byte recipient = LoRa.read();
  byte sender = LoRa.read();
  byte msgId = LoRa.read();
  byte msgLen = LoRa.read();

  String incoming = "";
  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }

  if (msgLen != incoming.length()) {
    Serial.println("Error: length does not match");
    return;
  }

  if (recipient != SELF_ADDR && recipient != BROADCAST) {
    Serial.println("Message is not for me");
    return;
  }

  Serial.println("Received from: 0x" + String(sender, HEX));
  Serial.println("Sent to: 0x" + String(recipient, HEX));
  Serial.println("Count: " + String(msgId));
  Serial.println("Message length: " + String(msgLen));
  Serial.println("Message: " + incoming);
  Serial.println();
} // onReceive( )

// Loop

void loop() {
  if (millis() - lastSendTime > interval) {
    onSend();
  }

  onReceive(LoRa.parsePacket());
}

Теперь подробно разберём его работу. В начале скетча подключаются библиотеки SPI.h (для работы с SPI) и LoRa.h (для поддержки специфических LoRa функций).

#include <SPI.h>
#include <LoRa.h> 

В строках

#define SELF_ADDR 0xAA
#define BROADCAST 0xFF

Задаётся собственный LoRa адрес контроллера (SELF_ADDR), в данном случае равный 0xAA и широковещательный адрес (BROADCAST), равный 0xFF.

Далее объявляются глобальные переменные

byte destAddr = BROADCAST;
long lastSendTime = 0;
byte count = 0;
int interval = 2000;

адреса назначения пакетов (destAddr), в данном случае адресом назначения является широковещательный адрес (BROADCAST), т. е. посылаемые пакеты будут приниматься всеми контроллерами; времени последней посылки пакета (lastSendTime); счётчика посланных пакетов (count) и интервала посылки LoRa пакетов в эфир (interval).

Функция setup()

В функции setup() производятся начальные настройки контроллера и инициализируются объекты. В нашем случае инициализируется Serial интерфейс на частоте 115200 и выводится надпись о старте

скетча (скорость в Serial Monitor тоже должна быть 115200).

  Serial.begin(115200);
  Serial.println("Start LoRa Duplex");

Далее инициализируется LoRa объект на частоте 868 МГц (868E6). В случае возникновения проблем выводится сообщение об ошибке и выполнение программы останавливается.

  if (!LoRa.begin(868E6)) {
    Serial.println("LoRa init failed");
    while(true);
  }

Затем, в случае успешной инициализации LoRa модуля, выполнение передаётся функции loop(), которая представляет собой бесконечный цикл и непрерывно, по кругу, выполняет содержащийся в ней код.

Функция loop()

Функция loop() очень простая и содержит всего несколько строк.

// Loop

void loop() {
  if (millis() - lastSendTime > interval) {
    onSend();
  }

  onReceive(LoRa.parsePacket());
}

Здесь производится только два действия:

1. Проверяется настало ли время посылки LoRa пакета

  if (millis() - lastSendTime > interval) {

и, если настало, то вызывается функция onSend() посылки пакета.

2. Постоянно вызывается функция

  onReceive(LoRa.parsePacket());

проверки не пришёл ли новый беспроводной пакет от других контроллеров.

Это и есть всё содержание работы функции loop(), которая бесконечно проверяет не нужно ли послать пакет в эфир и не пришёл ли новый пакет от других контроллеров.

Теперь давайте рассмотрим сам процесс посылки и приёма беспроводных LoRa пакетов нашим контроллером.

Посылка пакетов

Посылку пакетов осуществляют две функции: onSend() и sendMessage(), рассмотрим их работу подробнее.

onSend()

Эта функция делает три вещи: отдаёт команду на посылку пакета, выводит в сериал тестовое сообщение и устанавливает новый интервал для посылки следующего пакета.

void onSend() {
  String message = "Hello!";
  sendMessage(message);
  Serial.println("Sending " + message);
  lastSendTime = millis();
  interval = random(2000) + 1000;
}

Формируется текст сообщения для посылки «Hello!», вызывается функция sendMessage() и выводится тестовое сообщение.

  String message = "Hello!";
  sendMessage(message);
  Serial.println("Sending " + message);

Далее запоминается время последней посылки пакета и вычисляется интервал для следующей.

  lastSendTime = millis();
  interval = random(2000) + 1000;

Поскольку функция random(2000) возвращает значение в диапазоне от 0 до 1999, то результирующий интервал посылки будет находится в диапазоне от 1 до 3 секунд. В итоге, наш микроконтроллер будет передавать в эфир LoRa пакеты не с постоянным периодом, а с переменным в диапазоне от 1 до 3 секунд.

sendMessage()

sendMessage() — это «дочерняя» функция, вызываемая из onSend(), её работа заключается в формировании пакета с нашими данными и посылке этого пакета в эфир (плюс ещё увеличение на единицу счётчика при посылке каждого пакета).

void sendMessage(String mess) {
  LoRa.beginPacket();
    LoRa.write(destAddr);
    LoRa.write(SELF_ADDR);
    LoRa.write(count);
    LoRa.write(mess.length());
    LoRa.print(mess);
  LoRa.endPacket();
  
  count++;
}

Строка

  LoRa.beginPacket();

начинает формирование пакета, а строки

    LoRa.write(destAddr);
    LoRa.write(SELF_ADDR);
    LoRa.write(count);
    LoRa.write(mess.length());
    LoRa.print(mess);

формируют сам пакет (пока не посылая его в эфир). По порядку формирования: задаётся байт адреса назначения пакета (в нашем случае это широковещательный адрес), далее задаётся байт адреса отправителя, далее байт счётчика пакетов (поскольку переменная счётчика имеет тип byte, то она будет переполняться каждые 255 пакетов), затем байт длины сообщения (полезных данных) и в конце добавляется само сообщение.

Таким образом, сформированный нами пакет имеет довольно развитую структуру и содержит как саму полезную информацию, которую мы хотим передать, так метаданные об этой информации (адреса отправителя и получателя, счётчик (ID сообщения) и его длину).

Информация о длине сообщения может использоваться на стороне приёмника для контроля правильности передачи пакета — если указанная и реальная длины сообщения не совпадут, то пакет будет считаться испорченным при передаче.

Затем инструкция

  LoRa.endPacket();

завершает формирование пакета и посылает его в эфир. Ниже представлен скриншот интерфейса SDR приёмника на котором виден момент посылки в эфир LoRa пакета платой Yotster Lite.

Приём пакетов

Приём пакетов осуществляет функция onReceive(), рассмотрим её работу подробно.

onReceive()

Функция onReceive() принимает пакет, декодирует его, проверяет на ошибки и, в случае успеха, выводит его содержимое в Serial.

void onReceive(int packetSize) {
  if (packetSize == 0) {return;}

  byte recipient = LoRa.read();
  byte sender = LoRa.read();
  byte msgId = LoRa.read();
  byte msgLen = LoRa.read();

  String incoming = "";
  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }

  if (msgLen != incoming.length()) {
    Serial.println("Error: length does not match");
    return;
  }

  if (recipient != SELF_ADDR && recipient != BROADCAST) {
    Serial.println("Message is not for me");
    return;
  }

  Serial.println("Received from: 0x" + String(sender, HEX));
  Serial.println("Sent to: 0x" + String(recipient, HEX));
  Serial.println("Count: " + String(msgId));
  Serial.println("Message length: " + String(msgLen));
  Serial.println("Message: " + incoming);
  Serial.println();
} // onReceive( )

Рассмотрим работу этой функции. В случае отсутствия новых пакетов (packetSize == 0) строка

  if (packetSize == 0) {return;}

прерывает выполнение функции onReceive() — если нет новых пакетов, то и делать ничего не нужно.

Если размер пакета больше нуля, то далее, в блоке

  byte recipient = LoRa.read();
  byte sender = LoRa.read();
  byte msgId = LoRa.read();
  byte msgLen = LoRa.read();

происходит извлечение метаинформации (данных о самом пакете) из пришедшего пакета. Извлекается байт адреса назначения пакета, байт адреса отправителя, значение счётчика (ID пакета) и информация о длине полезного сообщения.

Далее из пакета извлекается полезная информация, которую нам передал другой контроллер.

  String incoming = "";
  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }

И помещается в переменную incoming.

Затем происходят две проверки. Первая — на соответствие длины полезного сообщения. Если длина не соответствует, то выводится сообщение об ошибке и выполнение функции прекращается — пришёл «битый» пакет.

  if (msgLen != incoming.length()) {
    Serial.println("Error: length does not match");
    return;
  }

И затем проверяется адрес назначения пакета. В нашем случае пакет широковещательный (BROADCAST) и в любом случае подходит для нашего контроллера.

  if (recipient != SELF_ADDR && recipient != BROADCAST) {
    Serial.println("Message is not for me");
    return;
  }

И окончательный аккорд функции onReceive() — вывод всей полученной информации в Serial.

  Serial.println("Received from: 0x" + String(sender, HEX));
  Serial.println("Sent to: 0x" + String(recipient, HEX));
  Serial.println("Count: " + String(msgId));
  Serial.println("Message length: " + String(msgLen));
  Serial.println("Message: " + incoming);
  Serial.println();

Сначала выводится вся метаинформация о пакете, а затем собственно само послание от другого контроллера.

На скриншоте красным выделена информация по пришедшим и декодированным LoRa пакетам, видно также и посланный пакет (строка «Sending Hello!»).

В данном случае пришедшая информация только выводится в Serial, но её также можно анализировать и использовать как данные (например от датчиков) или как команды, например, для включения и выключения реле.

LoRa Duplex в ваших проектах

Мы подробно рассмотрели работу двух беспроводных LoRa контроллеров Yotster Lite в режиме дуплексной передачи информации. Внимательно ознакомившись с этим руководством и попробовав на практике работу представленного здесь кода, вы сможете в дальнейшем с лёгкостью использовать эти знания в своих проектах с беспроводной передачей информации.

Ссылки по теме

Обзор LoRa контроллера Yotster Lite

Спецификации и подключение Yotster Lite

Программирование Yotster Lite

Где купить?

Yotster Lite в магазине «Electromicro»

Техническая поддержка

Мы очень внимательно относимся к потребностям наших клиентов и осуществляем техническую поддержку по всей выпускаемой продукции. Вы можете написать нам письмо с вашим вопросом или позвонить по телефону и специалист нашей компании проконсультирует вас и поможет решить вашу проблему.

  • Емейл для вопросов по нашей продукции: electromicro@bk.ru
  • Наш телефон: +7 (495) 997-37-74

Аналогичные товары