Разработка устройства для IoTSchool на базе микроконтроллера Arduino

Цель работы

Научиться разрабатывать устройства, совместимые с концепцией IoT от IoTSchool

Задачи работы

  1. Установка библитеки для протокола взаимодействия с приложением IoTSchool.
  2. Разработка скетча.
  3. Проверка взаимодействия с устройством через монитор последовательного порта.

Инструменты для выполения работы

  1. Компьютер с подключением к сети Internet.
  2. Плата Arduino с USB выходом (например, Arduino Uno).

Теоретическая часть

Для упрощения разработки устройств IoT и унификации их подключения к платформе IoTSchool был разработан простой текстовый протокол для взаимодействия устройства с компьютером через различные каналы связи, предназначенные для последовательной передачи информации. Протокол предназначен для организации взаимодействия двух устройств (точка-точка) посредством обмена простыми текстовыми сообщениями. Использование текстовых сообщений упрощает разработку и отладку устройства, так как позволяет взаимодействовать с ним без специализированного ПО из командной строки или монитора последовательного порта. Так же была разработана библиотека для Arduino IDE, реализующая данный протокол.

Выполнение работы

Установка библитеки для протокола взаимодействия с приложением IoTSchool

  1. Скачиваем архив с исходными кодами по адресу https://github.com/ooolms/wl_iot_framework (кнопка "Clone or download" на странице) или клонируем репозиторий с помощью git.
  2. Разархивируем этот архив, должна появиться папка wl_iot_framework.
  3. Устанавливаем библиотеку через менеджер библиотек: выбираем пункт меню "Скетч -> Управление библиотеками -> Добавить .ZIP библиотеку" и находим архив ARpc.zip внутри папки wl_iot_framework в подпапке ArduinoIdeLibrary.
В папке docs архива есть описание протокола
 

Разработка скетча

В рамках работы будет создан скетч, позволяющий мигать светодиодом из интерфейса приложения IoTSchool и передающий раз в пол-секунды "измерения" (сгенерированный двумерный сигнал (sin(t);cos(t)) ). Так же у устройства будет еще один "датчик" - счетчик миганий светодиодом.
Для взаимодействия с компьютером через последовательный порт мы создадим объект класса ARpc и определим для него две callback-функции - одна для обработки команд от ПК, вторая для отправки сообщений на компьютер. Данные функции будут вызываться самой библиотекой при необходимости. Для однозначной идентификации устройства так же необходимо указать идентификатор и имя устройства. А для обеспечения возможность управления устройством необходимо разработать xml-описание панели управления устройством.
Создаем новый скетч и сохраняем под именем IotDeviceTest. Проверяем, что правильно указана плата и порт. Подключаем к скетчу библиотеку ARpc (Скетч -> Подключить библиотеку -> ARpc). В начале файла должен был появиться нужный #include <ARpcDevice.h>.
Генерируем уникальный идентификатор в формате UUID (например, можно воспользоваться сервисом https://www.uuidgenerator.net/version4, при открытии страницы вверху будет готовый UUID). Добавляем две глобальных переменных для идентификатора и имени устройства:

const char *deviceName="led_blink_test";//имя устройства
const ARpcUuid deviceId("{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}");//идентификатор устройства
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx заменяем на полученный UUID, фигурные скобки должны остаться.
Так же определяем дополнителные глобальные переменные:
int ledPin=13;//пин светодиода
uint32_t blinksCount=0;//число миганий
Для того, чтобы в дальнейшем для устройства был доступен интерфейс управления устройством, необходимо разработать xml-описание. Подробно про это можно почитать в pdf-описании (ссылка из теоретической части). Для нашего сценария необходима одна кнопка, передающая на контроллер команду "blink", которая описывается следующим образом:
const char *interfaceStr="<controls><group title=\"Device controls\"><control title=\"Blink\" command=\"blink\"/></group></controls>";
Так же для получения данных с устройства необходимо подготовить описание датчиков. Для нашего устройства оно выглядит вот так:
const char *sensorsDef="<sensors>"
"<sensor name=\"blinks_count\" type=\"u32_sv\"/>"//датчик blinks_count
"<sensor name=\"sin_x\" type=\"f32_sv_d2\"/>"//датчик sin_x (двумерный)
"</sensors>";

Определяем класс для передачи сообщений к ПК через последовательный порт:

class WriteCallback
    :public ARpcIWriteCallback
{
public:
    virtual void writeData(const char *data,unsigned long sz)
    {
        Serial.write(data,sz);
    }
    virtual void writeStr(const char *str)
    {
        Serial.print(str);
    }
    virtual void writeStr(const __FlashStringHelper *str)
    {
        Serial.print(str);
    }
}wcb;

Объект этого класса используется, когда библиотеке нужно передать какие-либо данные от устройства. В данном случае это данные, которые мы увидим в мониторе порта. Обратите внимание, что не используется println, так как эта функция добавляет лишний перевод строки, который будет мешать нормальной обработке сообщений.

Далее мы объявляем объект класса ARpc и передаем ему ссылки на созданные выше переменные и функции:

ARpcDevice dev(300,&wcb,&deviceId,deviceName);

Определяем класс обработки команд, передаваемых устройству. Библиотека будет использовать объект этого класса, когда на устройство будут приходить команды, например, введенные нами в мониторе порта. Функция-обработчик принимает команду, аргументы команды и количество аргументов:

class CommandCallback
    :public ARpcIDevEventsCallback
{
public:
    virtual void processCommand(const char *cmd,const char *args[],unsigned char argsCount)
    {
        if(strcmp(cmd,"blink")==0)//команда blink, проверяем что есть аргумент
        {
            digitalWrite(13,HIGH);
            
delay(500);
    
        digitalWrite(13,LOW);
            ++blinksCount;
            dev.disp().writeMeasurement("blinks_count",String(blinksCount).c_str());
            dev.disp().writeOk();
        }
        else dev.disp().writeErr("Unknown cmd");//неизвестная команда
    }
}ccb;
Здесь мы обрабатываем одну команду - "blink", при приходе которой мигаем штатным светодиодом на 13 порту и передаем новое "измерение" счетчика миганий.

Далее подготавливается функция для генерации отсчетов sin и cos

int t=0;
float sVal[2];
void writeSinVal()
{
    sVal[0]=sin(0.1*t);
    sVal[1]=cos(0.1*t);
    dev.disp().writeMeasurementB("sin_x",sVal,2);
    ++t;
}

300 - размер буфера для одного сообщения. Невозможно передать на контроллер сообщение размера больше указанного.

Размер буфера нужно подбирать, исходя из доступного объема памяти. На микроконтроллерах с большим объемом памяти можно использовать больший размер буфера.

Проихводим инициальзацию пина и последовательного порта в функции setup() и установить описание датчиков и интерфейса управления:

void setup()
{
    Serial.begin(9600);
    pinMode(ledPin,OUTPUT);
    dev.disp().installDevEventsHandler(&ccb);
    dev.disp().setControls(interfaceStr);
    dev.disp().setSensors(sensorsDef);
}

И наконец, в функции loop() необходимо проверять последовательный порт на наличие новых данных, передавать их объекту parser, сгенерировать новый отсчет sin, после чего сделать задержку на пол-секунды, чтобы отсчеты не генерировались слишком часто.

void loop()
{
    while(Serial.available())
        dev.putByte(Serial.read());
    writeSinVal();
    delay(500);
}
Загружаем полученный скетч на микроконтроллер.

Проверка взаимодействия с устройством через монитор последовательного порта.

Открываем монитор порта. В нем должны регулярно появляться сообщения "meas" с новыми значениями sin и cos.

Проверяем, чтобы внизу было выбрано "Новая строка", а не "Нет конца строки".
Пишем в поле ввода "identify" и нажимаем Отправить. В ответ должно появиться сообщение deviceinfo.

В этом сообщении должны быть идентификатор и имя устройства, указанные в скетче.
Дополнительное задание: передать сообщение на устройство с командой мигания светодиодом "blink", запросить с устройства список датчиков и описание интерфейса управления.