Сеть беспроводных устройств на Arduino

Микро-контроллер на платформе Arduino — отличная платформа для хобби-проектов различной степени сложности и полезности. Не буду утверждать, что платформа Arduino есть наилучший выбор для профессиональных решений (скорее соглашусь с обратным), но для моих любительских «поделок» в области домашней автоматики это оптимальный вариант. Т.е. контроллер хорош сам по себе, но, если к тому же он перестанет быть «самим по себе», а будет уметь «общаться» с себе подобными, при этом не обрастая дополнительными проводами, то его полезность и применимость может многократно вырасти. Итак, начнём строить наш домашний SkyNet…

Бюджетные радио-модули

За основу нашей сети возьмём бюджетные радио-модули, работающие на частоте 433.90MHz. Стоимость одного такого модуля составляет порядка $2.5, так что невелики затраты для того, чтобы организовать связь с внешним миром. Конечно, для связи можно использовать и готовые ethernet-модули и даже делать симбиоз с беспрводными роутерами на базе альтернативных прошивок, но во многих случаях проще и дешевле всё сделать на вот таких радио-модулях.

Передатчик:



Приёмник:



Качество работы и дальность связи этих модулей оставляет желать лучшего, и я бы не стал верить оптимистичным заявлениям продавцов о радиусе действия ">500m". В лучшем случае 100 метров на открытой местности, ну и гораздо меньше при наличии бетонных перегородок. Тем не менее, для квартиры или небольшого загородного участка их хватит. Можно использовать и более качественные (соответственно, более дорогие) радио-модули, поэтому статья может рассматриваться как идейная концепция, применимая к многим возможным вариантам реализации.

Важный момент: в данном руководстве я не буду рассматривать вариант создания сети с контролем качества передачи данных. Если сравнение с ethernet-протоколами можно считать уместным, то мы не будем строить сеть передачи TCP-пакетов, а, скорее, UDP.

Каждый из модулей подключается к контроллеру элементарно — питание через Vcc/Gnd и вывод Data подключается к свободному цифровому входу на микро-контроллере. Для повышения качества приёма/передачи рекомендуется дополнительно подключить антенну в виде провода размером 10-15 см. Кстати, дальность связи зависит ещё и от подающегося на модуль питания — если их запитать от 12В, то дальность и надёжность связи значительно возрастает.

Приёмник и передатчик подключенный к микро-контроллеру Arduino UNO R3:



Таким образом, мы сделали два устройства: первое — передатчик, который будет «вещать» в эфир какую-то информацию; второе — приёмник, который, соответственно, будет эфир «слушать». Далее, дело за тем, что бы и передача и приём были осмысленными и полезными для нас.

Библиотека VirtualWire

Чем хороша платформа Arduino, так это наличием огромного количества готовых библиотек для работы с всевозможными устройствами. Можно, конечно, с радио-модулями работать и без каких-либо библиотек, но тогда нужно разрабатывать свой протокол связи с контрольными суммами и прочими вещами. К счастью, есть замечательная библиотека VirtualWire, поддерживающая данные (и им подобные) радио-модули. С помощью этой библиотеки очень легко организовать передачу и приём небольших пакетов информации.

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

Код передатчика с использованием VirtualWire (из примеров использования библиотеки):
Посмотреть код
// transmitter.pde
//
// Simple example of how to use VirtualWire to transmit messages
// Implements a simplex (one-way) transmitter with an TX-C1 module
#include <VirtualWire.h>

void setup()
{
    Serial.begin(9600);   // Debugging only
    Serial.println("setup");
    // Initialise the IO and ISR
    vw_set_ptt_inverted(true); // Required for DR3100
    vw_setup(2000);      // Bits per sec
}

void loop()
{
    const char *msg = "hello";

    digitalWrite(13, true); // Flash a light to show transmitting
    vw_send((uint8_t *)msg, strlen(msg));
    vw_wait_tx(); // Wait until the whole message is gone
    digitalWrite(13, false);
    delay(200);
}


Код приёмника:
Посмотреть код
// receiver.pde
//
// Simple example of how to use VirtualWire to receive messages
// Implements a simplex (one-way) receiver with an Rx-B1 module
#include <VirtualWire.h>

void setup()
{
    Serial.begin(9600); // Debugging only
    Serial.println("setup");

    // Initialise the IO and ISR
    vw_set_ptt_inverted(true); // Required for DR3100
    vw_setup(2000);      // Bits per sec

    vw_rx_start();       // Start the receiver PLL running
}

void loop()
{
    uint8_t buf[VW_MAX_MESSAGE_LEN];
    uint8_t buflen = VW_MAX_MESSAGE_LEN;

    if (vw_get_message(buf, &buflen)) // Non-blocking
    {
        int i;
        digitalWrite(13, true); // Flash a light to show received good message
        // Message with a good checksum received, dump it.
        Serial.print("Got: ");       
        for (i = 0; i < buflen; i++)
        {
            Serial.print(buf[i], HEX);
            Serial.print(" ");
        }
        Serial.println("");
        digitalWrite(13, false);
    }
}


Протокол обмена данными

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

Я приведу ту структуру данных, которая показалась мне оптимальной при имеющихся возможностях оборудования. Итак, ниже по тексту список основных параметров, которые отправляются в эфир с каждым пакетом:

device_id — идентификатор устройства, отправившего пакет. Тип данных: unsigned int (длина 2 байта, диапазон значений от 0 до 65535) — как мне кажется, вполне достаточно для домашней сети.

destination_id — идентификатор устройства, кому предназначен пакет. Тип данных тот же, что и у device_id. Важно отметить, что пакеты всё равно будут получаться всеми приёмниками, но уже программой на самом приёмнике можно «отсекать» пакеты, которые устройству не предназначены. Так же можно принять в качестве правила то, что значение «0» в данном поле означает широковещательный пакет.

packet_id — идентификатор пакета. Тип тот же unsigned int. По замыслу, при отправке пакет «помечается» случайным числом, что может быть использовано для повторной отправки одного и того же пакета несколько раз с каким-то интервалом — ввиду ненадёжности протокола это имеет смысл, но принимающее устройство должно фильтровать повторные команды дабы не выполнять одно и то же действие в качестве реакции на пакет данных.

command — тип команды. Тип данных byte (длина 1 байт, диапазон значений от 0 до 255). Это так называемый «класс команды», а по сути информация о том, что за данные мы посылаем. Например, мы можем составить собственную таблицу команд, отведя для команды управления открытием/закрытием номер 10, а для команды передачи данных о температуре номер 15. Главное, чтобы эта таблица у нас была постоянной. А можно поступить ещё хитрее — подсмотреть возможные команды в том же протоколе ZWave и использовать у себя их таблицу, чтобы было всё «как у взрослых», и не нужно было заботиться о сохранности этих ценных сведений.

data — собственно данные. Тип данных int (длина 2 байта, диапазон значений от -32,768 до 32,767. В этом поле мы передаём непосредственно данные в виде одного числа. Мало? Ну мне показалось достаточным. Температуру можно передать (например усвловившись, что она умножена на 100), статус датчика движения — легко, команду для приёмника с реле — проще простого. Текстовые данные на внешний дисплей, конечно, не отправишь, но такой цели и не ставилось, а для моих настоящих и будущих устройств хватит за глаза и пары десятков чисел, чтобы описать все возможные команды.

В итоге мы имеем длину пакета равную 9 байт. Короткий пакет, на самом деле, очень хорошо — во-первых, меньше вероятность, что он по дороге «сломается»; во-вторых, меньше время на пересылку, что уменьшает вероятность совместного использования эфира несколькими устройствами. Кстати, последнее обстоятельства потребует «экономно», т.е. не часто слать информацию. При этом желательно, чтобы при переодической отправке показаний промежуток между сеансами несколько варьировался. Но это всё надо уже предусматривать при интеграции конкретного устройства. Как бы то ни было, я бы не советывал слишком налегать на универсальность структуры в ущерб минимального размера пакета передаваемых данных.

Итак, мы определились со структурой пакета, теперь нужно реализовать обмен. Тут нам на помощь приходит ещё одна полезная библиотека под названием EasyTransfer. Собственно, она работает «поверх» VirtualWire, позволяя описать структуру данных на приёмнике/передатчике и вести обмен уже не набором байт-кодов, а указанной структурой целиком.

В нашем случае структура данных будет иметь следующий вид:
struct SEND_DATA_STRUCTURE{
  unsigned int device_id;
  unsigned int destination_id;    
  unsigned int packet_id;
  byte command;
  int data;
};


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

Пару слов про поле device_id. Его можно задавать вручную для каждого устройства, но я пошёл по более простому пути — при первом запуске генерирую это значений случайным образом и записываю в энерго-независимую область памяти EEPROM. Вероятность того, что разные устройства получают одинаковые идентификаторы из дипазона значений поля unsigned int крайне мала и, опять же, в моём случае риск вполне оправдан.

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

Код передатчика:
Посмотреть код
#include <VirtualWire.h>
#include <EasyTransferVirtualWire.h>
#include <EEPROM.h> // эта библиотека нужна для работы с энерго-независимой памятью

const int led_pin = 13;
const int transmit_pin = 2;
unsigned int unique_device_id = 0;
unsigned int count = 1;

//create object
EasyTransferVirtualWire ET; 

struct SEND_DATA_STRUCTURE{
  //наша структура данны. она должна быть определена одинаково на приёмнике и передатчике
  //кроме того, размер структуры не должен превышать 26 байт (ограничение VirtualWire)
  unsigned int device_id;
  unsigned int destination_id;    
  unsigned int packet_id;
  byte command;
  int data;
};

//переменная с данными нашей структуры
SEND_DATA_STRUCTURE mydata;

//ниже пару функций для записи данных типа unsigned int в EEPROM
void EEPROMWriteInt(int p_address, unsigned int p_value)
      {
      byte lowByte = ((p_value >> 0) & 0xFF);
      byte highByte = ((p_value >> 8) & 0xFF);

      EEPROM.write(p_address, lowByte);
      EEPROM.write(p_address + 1, highByte);
      }

unsigned int EEPROMReadInt(int p_address)
      {
      byte lowByte = EEPROM.read(p_address);
      byte highByte = EEPROM.read(p_address + 1);

      return ((lowByte << 0) & 0xFF) + ((highByte << 8) & 0xFF00);
      }

void setup()
{
  // блок инициализации
  pinMode(led_pin, OUTPUT);

  ET.begin(details(mydata));
  vw_set_tx_pin(transmit_pin); //установка пина, к которому подключен data-вход передатчика
  vw_setup(2000);        //скорость передачи
  Serial.begin(9600);
  randomSeed(analogRead(0));


  // Читаем/записываем Device ID
  Serial.print("Getting Device ID... "); 
  unique_device_id=EEPROMReadInt(0);
  if (unique_device_id<10000 || unique_device_id>60000) {
   Serial.print("N/A, updating... "); 
   unique_device_id=random(10000, 60000);
   EEPROMWriteInt(0, unique_device_id);
  }
  Serial.println(unique_device_id);

}

void loop()
{
  mydata.device_id = unique_device_id;
  mydata.destination_id = 0;
  mydata.packet_id = random(65535);
  mydata.command = 0;
  mydata.data = count;

  digitalWrite(led_pin, HIGH); // включаем светодиод для отображения процесса передачи
  Serial.print("Transmitting packet ");   
  Serial.print(mydata.packet_id); 
  Serial.print(" device id ");   
  Serial.print(mydata.device_id);   
  Serial.print(" data: "); 
  Serial.print(mydata.data);
  Serial.print(" ... "); 
  ET.sendData(); // отправка данных

  digitalWrite(led_pin, LOW);
  Serial.println("DONE");
  delay(1000);
  count = count + 1;
}


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

Код приёмника:
Посмотреть код
#include <VirtualWire.h>
#include <EasyTransferVirtualWire.h>
#include <EEPROM.h>

const int led_pin = 13;
const int receive_pin = 2;
unsigned int unique_device_id = 0;

//create object
EasyTransferVirtualWire ET; 
char buf[120];

struct SEND_DATA_STRUCTURE{
  //наша структура данны. она должна быть определена одинаково на приёмнике и передатчике
  //кроме того, размер структуры не должен превышать 26 байт (ограничение VirtualWire)
  unsigned int device_id;
  unsigned int destination_id;  
  unsigned int packet_id;
  byte command;
  int data;
};

//переменная с данными нашей структуры
SEND_DATA_STRUCTURE mydata;

//ниже пару функций для записи данных типа unsigned int в EEPROM
void EEPROMWriteInt(int p_address, unsigned int p_value)
      {
      byte lowByte = ((p_value >> 0) & 0xFF);
      byte highByte = ((p_value >> 8) & 0xFF);

      EEPROM.write(p_address, lowByte);
      EEPROM.write(p_address + 1, highByte);
      }

unsigned int EEPROMReadInt(int p_address)
      {
      byte lowByte = EEPROM.read(p_address);
      byte highByte = EEPROM.read(p_address + 1);

      return ((lowByte << 0) & 0xFF) + ((highByte << 8) & 0xFF00);
      }

void setup()
{
    pinMode(led_pin, OUTPUT);
    Serial.begin(9600); // Debugging only

    ET.begin(details(mydata));
    // Initialise the IO and ISR
    vw_set_rx_pin(receive_pin);
    vw_setup(2000);      // Скорость приёма
    vw_rx_start();       // Запуск режима приёма
    
  // Device ID
  Serial.print("Getting Device ID... "); 
  unique_device_id=EEPROMReadInt(0);
  if (unique_device_id<10000 || unique_device_id>60000) {
   Serial.print("N/A, updating... "); 
   unique_device_id=random(10000, 60000);
   EEPROMWriteInt(0, unique_device_id);
  }
  Serial.println(unique_device_id);
  
}

void loop()
{
    if(ET.receiveData()) // получили пакет данных, обрабатываем
    {
        digitalWrite(led_pin, HIGH);
        Serial.print("Got: ");
        Serial.print("Device ID: ");
        Serial.print(mydata.device_id);
        Serial.print(" Destination ID: ");
        Serial.print(mydata.destination_id);        
        Serial.print(" Packet ID: ");
        Serial.print(mydata.packet_id);
        Serial.print(" Command: ");
        Serial.print(mydata.command);
        Serial.print(" Data: ");
        Serial.print(mydata.data);
        Serial.println();
        digitalWrite(led_pin, LOW);
    }
}


Ура! Наш Skynet в эфире! Уже можно делать много полезного, но нет предела совершенству… Двигаемся дальше.

Интеграция в MajorDoMo

Следующий этап — встраивание всего нашего «хозяйства» в более сложную среду управления Умным Домом. В данном случае, используется платформа MajorDoMo, но аналогичным образом может быть организована интеграция с любой другой системой.

Собственно, принцип интеграции в организации «моста» между компьютером и нашей радио-сетью. Ниже я привожу пример создания «слушающего моста», задача которого в том, чтобы «слушать» эфир и транслировать все принятые пакеты в среду MajorDoMo. Последняя, в свою очередь, уже будет заниматься их обработкой — реагировать какими-то действиями или же просто выводить получаемые данные в различных интерфейсах.

В панели управления сценариями создадим скрипт приёма сообщений под названием easyRF.
Код сценария:

$device_id=$params['did'];
$destination_id=$params['dest'];
$packet_id=$params['pid'];
$command_id=$params['c'];
$data=$params['d'];
say("От устройства $device_id пришёл пакет $packet_id с командой $command_id и данными $data");


После добавления данный код сразу можно вызвать по http-ссылке:
192.168.0.17/objects/?script=easyRF

(вместо 192.168.0.17 адрес вашего сервера)

Следующий шаг, это трансляция принимаемых данных от Arduino в систему MajorDoMo. Тут есть варианты — можно добавить на Arduino-приёмник модуль ethernet и сразу посылать http-запросы по сети, а можно подключить микро-контроллер по USB и воспользоваться программой ArduinoGW, которая «слушает» COM-порт и при наличии ключевой последовательности, соответствующей отправки http-запроса, сама переадресует его в сеть.

Воспользуемся вторым способом, т.к. он не требует дополнительного оборудования. В таком случае, код приёмника будет выглядеть вот так:
Посмотреть код
#include <VirtualWire.h>
#include <EasyTransferVirtualWire.h>
#include <EEPROM.h>

const int led_pin = 13;
const int receive_pin = 2;
unsigned int unique_device_id = 0;

//create object
EasyTransferVirtualWire ET; 
char buf[120];

struct SEND_DATA_STRUCTURE{
  //наша структура данны. она должна быть определена одинаково на приёмнике и передатчике
  //кроме того, размер структуры не должен превышать 26 байт (ограничение VirtualWire)
  unsigned int device_id;
  unsigned int destination_id;  
  unsigned int packet_id;
  byte command;
  int data;
};

//переменная с данными нашей структуры
SEND_DATA_STRUCTURE mydata;

//ниже пару функций для записи данных типа unsigned int в EEPROM
void EEPROMWriteInt(int p_address, unsigned int p_value)
      {
      byte lowByte = ((p_value >> 0) & 0xFF);
      byte highByte = ((p_value >> 8) & 0xFF);

      EEPROM.write(p_address, lowByte);
      EEPROM.write(p_address + 1, highByte);
      }

unsigned int EEPROMReadInt(int p_address)
      {
      byte lowByte = EEPROM.read(p_address);
      byte highByte = EEPROM.read(p_address + 1);

      return ((lowByte << 0) & 0xFF) + ((highByte << 8) & 0xFF00);
      }

void setup()
{
    pinMode(led_pin, OUTPUT);
    Serial.begin(9600); // Debugging only

    ET.begin(details(mydata));
    // Initialise the IO and ISR
    vw_set_rx_pin(receive_pin);
    vw_setup(2000);      // Bits per sec
    vw_rx_start();       // Start the receiver PLL running
    
  // Device ID
  Serial.print("Getting Device ID... "); 
  unique_device_id=EEPROMReadInt(0);
  if (unique_device_id<10000 || unique_device_id>60000) {
   Serial.print("N/A, updating... "); 
   unique_device_id=random(10000, 60000);
   EEPROMWriteInt(0, unique_device_id);
  }
  Serial.println(unique_device_id);
  
}

void loop()
{
    if(ET.receiveData())  // получили пакет данных, обрабатываем
    {
        digitalWrite(led_pin, HIGH);
        Serial.print("Got: ");
        Serial.print("Device ID: ");
        Serial.print(mydata.device_id);
        Serial.print(" Destination ID: ");
        Serial.print(mydata.destination_id);        
        Serial.print(" Packet ID: ");
        Serial.print(mydata.packet_id);
        Serial.print(" Command: ");
        Serial.print(mydata.command);
        Serial.print(" Data: ");
        Serial.print(mydata.data);
        Serial.println();
        digitalWrite(led_pin, LOW);
        sprintf(buf, "GET /objects/?script=easyRF&did=%u&dest=%u&pid=%u&c=%u&d=%i HTTP/1.0", (int)mydata.device_id, (int)mydata.destination_id, (int)mydata.packet_id, (int)mydata.command, (int)mydata.data);
        Serial.println(buf); // выводим строку со ссылкой для HTTP-запроса (здесь может быть добавлено использование ethernet-shield-а
        Serial.println();        
    }
}


Вот и всё! Создаём устройства, добавляем радио-модули и настраиваем взаимодействия всего и вся.

Дальнейшее развитие

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

Приведу несколько мыслей, пришедших в голову:

* Создание узлов «надёжного» обмена (используем на одном устройстве и приёмник и передатчик и организуем обмен пакетами с контролем доставки, для контроля доставки выделяем отдельный класс команд)
* Используем более дорогие и надёжные радио-модули
* Реализуем процедуру «привязки» одного устройства к другому без необходимости изменения кода (перевод в режим «привязки» двух устройств и запись в EEPROM парного устройства)

На этом совсем всё. Спасибо за внимание!
Поделиться публикацией
Комментарии 21
    0
    А ссылку на модули за $2.5? Или они в оффлайне куплены?
    +4
    Вы не смотрели в сторону NRF24L01+?
    Даже по цене получается дешевле, + это RX/TX.
      +4
      Такая же мысль возникла… Вот десять штук NRF24L01+ за $9.38, итого одна штука — $0.93. Я именно у этого продавца заказывал, все в порядке. И да, это трансивер (т.е. прием+передача).
        0
        Здорово! Я думаю, что никаких проблем использовать их подобным же образом возникнуть не должно. Раз это трансиверы, то сразу можно реализовать и контроль приёма.
      0
      NRF24L01 конечно неплохая вещица, правда один знакомый с ними намучился пока свой «умный» дом такими «обтягивал», сперва мешался WLAN (одна частота): на расстоянии уже 10м не видят друг друга, вылечил вроде сменой канала.
      Потом батарейки садились быстро (тот же WLAN наводил, и тинька по SPI все время просыпалась), чем и как лечил не помню, но вроде вылечил таки.
      У 433MHz вроде дальнбойность посерьезней и наводок меньше должно быть.
        0
        Кстати, я их видел и в виде готовых плат, подобно тем что в статье, например тут (если вдруг не хочется чипы паять).
      0
      Например, вот. Только чуть больше ($2.70)
        0
        Уже месяц жду такие модули. Дешевле, да и «повкуснее», как мне кажется.
          0
          Пока ссылку искал, выше уже написали про них…
          0
          Меня удивляет отсутствие в широкой продаже модулей Arduino на основе чипов RFR2.
          Может сделать пробную партию?
            +7
            Я у себя дома реализовал управление нагрузками (свет/розетки) именно с помощью NRF24L01+. Только я еще смотрел на компактность, поэтому у меня есть «центральный» модуль, UNO + Ethernet Shield + NRF24L01+, который раздает всем команды и принимает от всех ответы, а конечные устройства реализованы на Nano + NRF24L01+. Управление всей системой — вэбстраница или с iPhone. Программа на последний — своя, чуть позже напишу статью про свою систему, там есть несколько интерестных решений, как мне кажется.
              +3
              Ждем статью!
              +1
              Вид, конечно, у сборки получается совсем не айс.
              Не идет ни в какое сравнение с Колибри habrahabr.ru/post/164215/
                0
                А существуют ли в природе готовые блоки для управления нагрузкой (220v) с двунаправленным радиоканалом?

                Очень не хочется собирать самому из блока питания, микроконтроллера, радио-модуля и реле.
                  0
                  У китайцев есть, но там блок + радио-пульт и какая там система команд непонятно, потеряешь пульт — что делать тоже непонятно. Зато дешевые.
                    0
                    Системы команд там нет
                    Просто 20 бит число передают.
                    0
                    ZWave же :)
                    отличный протокол кстати, хоть и не очень бюджетный. как раз в процессе интеграции этого класса устройств в MajorDoMo.
                    0
                    С noolite не будут конфликтовать? Там такая же несущая.
                      0
                      Возможно… Собственно, они и друг с другом конфликтовать будут при одновременной передаче, но ввиду того, что время передачи очень маленькая и использовать я предполагаю их для некритичных данных (мониторинг температуры), то проблем не должно быть. Так же выше в комментариях есть несколько предложений по улучшению надёжности и использованию других компонентов для организации сети.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое