В рамках второго этапа олимпиады необходимо разработать устройство для интернета вещей, совместимое с архитектурой Alterozoom IoT. Для этого устройство должно взаимодействовать с элементами архитектуры посредством определенного протокола (ссылка). Данный протокол обеспечивает взаимодействие с устройством путем обмена которкими текстовыми сообщениями определенного формата. В данном документе описан полный процесс от разработки устройства до подключения его к облаку через приложение для ПК IotSchool. Так же кроме приложения возможно использование локального сервера (можно почитать здесь, информация может понадобится на следующиъ этапах).

Разработка устройства

Для облегчения процесса разработки устройства на микроконтроллерах семейства Arduino и других, поддерживающих тот же API и среду разработки Arduino IDE (например, ESP8266, в частности, NodeMCU), предоставляются библиотеки ARpc и ARpcESP8266WiFiDevice. Первая может быть использована для разработки любого устройства, вторая - узкоспециализированная для разработки устройств на базе микроконтроллеров семейства ESP8266, работающих по Wi-Fi в сетях TCP/IP.
Для установки библиотеки необходимо произвести следующие шаги:

  1. Выгрузить исходники проекта командой "git clone https://github.com/ooolms/wl_iot_framework.git" или скачать .zip архив со страницы по адресу https://github.com/ooolms/wl_iot_framework (Кнопка Clone or download, затем кнопка Download ZIP) и распаковать его. После этого на компьютере появится папка wl_iot_framework.
  2. Зайти в wl_iot_framework\ArduinoIdeLibrary и скопировать папки ARpc и ARpcESP8266WiFiDevice в папку Мои Документы\Arduino\libraries

Для выполнения задания второго этапа необходимо разработать устройство, взаимодействующее с приложением для ПК через USB порт. Для этого проще всего воспользоваться примером из библиотеки ARpc. Открываем пример в Arduino IDE (Файл -> Примеры -> ARpc -> SimpleTemperatureSensor) и сохраняем его под другим названием. Ниже мы подробно разберем этот пример.

Подключаем заголовочный файл библиотеки.

#include <ARpcDevice.h>


Задаем имя устройства, под которым оно будет отображаться в интерфейсах. Генерируем уникальный идентификатор в формате UUID (например, можно воспользоваться сервисом https://www.uuidgenerator.net/version4, при открытии страницы вверху будет готовый UUID). xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx заменяем на полученный UUID, фигурные скобки должны остаться.

const char *deviceName="some_device_name";//имя устройства
const ARpcUuid deviceId("{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}");//идентификатор устройства


Задаем переменную для пина, с которого будут считываться данные. В нашем примере это аналоговый пин A0, к которому подключен абстрактный температурный датчик с аналоговым выходом, диапазону напряжений на входе A0 от 0 до 5В соответствует диапазон температур от 0 до 50 градусов цельсия.

int sensPin=A0;//пин датчика


Для получения данных с устройства необходимо подготовить описание датчиков. Для нашего устройства оно выглядит вот так:

const char *sensorsDef="<sensors>"
"<sensor name=\"temp\" type=\"f32_sv\"/>"//температурный датчик
"</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);

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

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

void setup()
{
    Serial.begin(9600);
    dev.disp().setSensors(sensorsDef);
    dev.resetStream();
}


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

unsigned long newTime=0,prevSinTime=0;
void loop()
{
    //проверяем, нет ли данных в Serial
    while(Serial.available())
        dev.putByte(Serial.read());//если данные есть, передаем в объект библиотеки
    newTime=millis();//берем текущее значение millis()
    if((newTime-prevSinTime)>=10000)//если оно на 10000 мсек больше предыдущего
    {
        float v=(float)analogRead(A0)*50.0/1023.0;//считываем значение с АЦП и нормируем его так, чтобы получить от 0 до 50 градусов
        dev.disp().writeMeasurement("temp",String(v).c_str());
        prevSinTime=newTime;
    }
    delay(1);
}


Для проверки можно открыть монитор порта, в котором регулярно должны появляться сообщения с заголовком meas. Если подключить к Arduino потенциометр (крайние ножки к gnd и vcc, среднюю к A0), можно имитировать изменение температуры.

Подключение разработанного устройства к приложению для ПК

Для того, чтобы в приложении иметь возможность работать с IoT частью, необходима специальная лицензия. Получить лицензию как участнику олимпиады можно, отправив запрос организаторам и указав в запросе свой email. Лицензию нужно вручную положить в папку C:/Users/<usernname>/.IotSchoolApp/module_licenses/
Папка появляется после первого запуска приложения.

Отключаем Arduino от ПК и запускаем приложение IoTSchool, открываем в верхнем меню раздел "IoT devices" (выделенная иконка на скриншоте ниже).


В левом верхнем углу открывшегося поля содержится список всех системных устройств. Так как в системе могут быть и другие устройства (модемы, телефоны и т.д.), приложение автоматически не подключает устройства. В левом нижнем углу список подключенных и идентифицированных устройств, справа - список хранилищ данных для датчиков с устройств. Внизу выводятся информационные сообщения.
Подключаем к компьютеру наше устройство, в списке системных устройств нажимаем кнопку Update. В списке появится еще одно новое устройство, выбираем его и нажимаем Identify. В случае успешной идентификации устройство появится в списке идентифицированных устройств. Если у устройства есть датчики, рядом с названием появится стрелка, при нажатии на которую развернется список датчиков устройства.


Выделяем датчик "temp" устройства и нажимаем кнопку "Add storage for sensor". Появится диалоговое окно настройки параметров хранилища.


Выбираем "Last N Values" и ниже вводим число сохраняемых значений "100 values". Справа выбираем пункт "Add global time on client".
Нажимаем Ok, в правом списке должно появиться хранилище для выбранного датчика. Открываем хранилище двойным кликом и наблюдаем изменяющийся график с данными.

 

Привязка хранилища к удаленному серверу

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

Например, для подключения к серверу платформы IotSchools нужно выбрать тип сервиса alterozoom и выбрать радиокнопку "use current application account". После нажатия на Ok данные начнут поступать на удаленный сервер. Для того, чтобы в этом убедиться, можно воспользоваться web интерфейсом.

Web интерфейс сервиса IotSchool

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

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

Интерфейс страницы "Мои документы" разделен по горизонтали на навигационную часть и часть веб-редактора.

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

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

Веб-редактор (правая часть страницы "Мои документы") позволяет размещать в документе изображения, веб-ссылки, списки, таблицы, iframe-ы, HTML-вставки, Диалоги, а также интерфейсы IoT-устройств. 

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

Раздел Команды (Commands) позволяет распечатать документ. 

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

Работа с IoT устройствами через Web-интерфейс

1. Просмотр списка и изменение параметров устройств

После подключения одного или нескольких устройств к серверу iot.alterozoom.com в интерфейсе редактора документов (в меню вверху экрана) появится ссылка "Мои устройства".

При нажатии на эту ссылку будет показан список всех подключенных устройств и всех компонентов каждого устройства.

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

2. Добавление данных, измеренных компонентом устройства (сенсором), в документ.
2.1. Нужно выбрать документ.
2.2. В панели HTML-редактора нужно нажать кнопку "IoT component".


2.3. В открывшемся меню из выпадающих списков нужно выбрать устройство "Device" и компонент устройства "Component". Нажать кнопку "Ok".


2.4. В содержимом документа должа появится область серого цвета, обозначающая место расположение графика с данными.
2.5. Нужно сохранить текущий документ и нажать кнопку "Просмотр".
2.6. В открывшейся вкладке браузера должен быть показан документ, содержащий график с данными выбранного компонента.

Пример документа: https://iot.alterozoom.com/ru/documents/6108.html
 

Описание списка датчиков

Каждое устройство может содержать несколько датчиков разных типов. У каждого датчика в пределах устройства должно быть уникальное название. При разработке устройства описание списка датчиков задается в xml или json формате. В описанном выше примере есть один единственный датчик с названием temp и типом f32_sv.
Значение датчика может передаваться с устройства в бинарном и текстовом виде, для этого используются соответствующие функции writeMeasurementB и writeMeasurement. При передаче значения в бинарном виде очень важно, чтобы используемая переменная языка С++ имела соответствующий тип.
Тип датчика представляет собой несколько ключей, разделенных символом "_". Ключи описывают: тип передаваемых значений, размерность датчика, количество значений, которые могут быть в одном сообщении, наличие или отсутствие метки времени.
Ключи, описывающие тип чисел в измерениях:

Ключ

Описание

f32

Вещественное число одинарной точности согласно стандарту IEEE 754. В языке C/C++ — тип float.

f64

Вещественное число двойной точности согласно стандарту IEEE 754. В языке C/C++ — тип double.

s8

Целое число со знаком размером 8 бит (1 байт). Значения от -128 до 128

u8

Целое число без знака размером 1 байт. Значения от 0 до 255

s16

Целое число со знаком размером 2 байта. Значения от -32768 до 32767

u16

Целое число без знака размером 2 байта. Значения от 0 до 65535

s32

Целое число со знаком размером 4 байта. Значения от -2147483648 до 2147483647

u32

Целое число без знака размером 4 байта. Значения от 0 до 4294967295

s64

Целое число со знаком размером 8 байт. Значения от -9223372036854775808 до 9223372 036854775807

u64

Целое число без знака размером 8 байт. Значения от 0 до 18446744073709551615

txt

Вместо чисел передается текст в кодировке UTF-8. не передается в бинарном виде.

Ключ, описывающий размерность отсчета:

Ключ

Описание

dXX

XX — размерность отсчета, целое число >=1. Например, d2, d3, d45. Если ключ не указан, размерность по-умолчанию — 1.

Ключи, описывающие количество значений в одном измерении:

Ключ

Описание

sv

Сокращение от "single value".

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

pv

Сокращение от "packet value".

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

Ключи, описывающие временную метку измерения:

Ключ

Описание

lt

Временная метка локального времени устройства. Может быть в любых единицах измерения (секунды, милли-, микро- или наносекунды, тики и т. д.).

gt

Временная метка глобального времени — количество миллисекунд, прошедшее с 01.01.1970г.

nt

Временная метка отсутствует. Вариант по-умолчанию, если ни один из ключей не указан.

 

Разработка интерфейса управления устройством и управление из приложения

Каждое устройство может при необходимости обрабатывать задаваемые разработчиком команды. Каждая команда может иметь 0 или более параметров. Для того, чтобы при использовании устройства можно было управлять им без дополнительных усилий, при разработке можно задать описание некоторого интерфейса управления. В этом интерфейсе команды сгруппированы в группы, которые в свою очередь так же могут быть вложены друг в друга. В группе указывается, как будут размещены ее элементы - по вертикали или по горизонтали. При описании параметра команды задается тип элемента, с помощью которого пользователь будет задавать значение параметра, например обычное полее ввода, или чекбокс, или группа радиокнопок и т.д.
При разработке устройства описание интерфейса управления представляется в виде строки языка С в xml или json формате.
Описание тегов и их атрибутов:

Тег Описание Где встречается Атрибуты
controls Корневой тег xml документа    
group Группа Должен быть первым и единственным дочерним тегом для тега controls, так же может быть дочерним тегом другого тега group title - название группы
layout - размещение элементов по горизонтали и вертикали, может принимать значение h или v; если не задан, v - по-умолчанию.
control Команды Является дочерним тегом для тега group title - название команды (отображается в UI)
command - команда, которая будет передана устройству (может вообще не совпадать с title)
layout - размещение элементов-параметров команды, работает как собветствующий атрибут группы
force_button - если равен 1, будет принудительно отображаться кнопка отправки команды
button_text - текст для кнопки отправки команды
param Параметр команды Является дочерним тегом для тега control, если у команды есть параметры title - название команды (отображается в UI)
type - тип элемента(ов) ui, используемого для задания параметра
layout - размещение элементов ui, если для параметра будет сгенерирован не один элемент, например список радиокнопок
attributes Свойства элемента ui для параметра команды Является дочерним тегом для тега param, не обязателен Зависят от тип параметра

Примечание: если у команды один параметр, по-умолчанию кнопка отправки команды отображаться не будет, команда будет отправляться при изменении состояния элемента ui, соответствующего параметру. Если это не удобно, для тега control можно задать атрибут force_button.
Типы параметров и возможные свойства

Тип параметра

Возможные свойства

Элемент UI

checkbox

onValue — значение, передаваемое, когда чекбокс включен (по умолчанию "1")

offValue — значение, передаваемое, когда чекбокс выключен (по умолчанию "0")

Чекбокс, checkable кпопка

text_edit

placeholder — текст, отображаемый, если поле пустое

Поле ввода (с кнопкой отправки справа, если параметр у контрола один)

select

values — список значений, разделенных символом "|" (если отсутствует, всегда отправляется "0")

titles — список отображаемых названий для значений, разделенных символом "|"

Выпадающий список

slider

min — минимальное значение (по умолчанию 0)

max — минимальное значение (по умолчанию 1023)

step — величина шаг (по умолчанию 1)

layout — вид слайдера ("h" — горизонтальный, "v" - вертикальный)

Слайдер

dial

min — минимальное значение (по умолчанию 0)

max — минимальное значение (по умолчанию 1023)

step — величина шаг (по умолчанию 1)

"Крутилка"

radio

values - список значений, разделенных символом "|" (если отсутствует или пустой, элемент не отображается, передается значение "0")

titles — список отображаемых названий для значений, разделенных символом "|"

Группа радиокнопок, из которых может быть нажата только одна

hidden

value — отправляемое значение ("0", если отсутствует)

Не отображается в интерфейсе


Пример описания интерфейса управления для некого упрощенного "кондиционера", выполняющего две команды - включить/выключить и установить температуру в пределах от 18 до 25 градусов:

<controls>
    <group>
        <control title="On/off" command="power">
            <param title="on" type="checkbox"/>
        </control>
        <control title="Set temperature" command="set_temp">
            <param title="temperature" type="slider">
                <attributes min="18" max="25"/>
            </param>
        </control>
    </group>
</controls>


В виде строковой переменной языка С:

const char *controlsStr="<controls>"
    "<group>"
        "<control title=\"On/off\" command=\"power\">"
            "<param title=\"on\" type=\"checkbox\"/>""
       "</control>"
       "<control title=\"Set temperature\" command=\"set_temp\">"
            "<param title=\"temperature\" type=\"slider\">"
                "<attributes min=\"18\" max=\"25\"/>"
            "</param>"
        "</control>"
    "</group>"
"</controls>";


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

class CommandCallback
    :public ARpcIDevEventsCallback
{
public:
    //callback-функция, вызываемая библиотекой, когда на устройство приходит пользовательская команда
    virtual void processCommand(const char *cmd,const char *args[],unsigned char argsCount)
    {
        if(strcmp(cmd,"power")==0&&argsCount>=1)
        {
            if(strcmp(args[0],"1")==0)
            {
                //включаем кондиционер
            }
            else
            {
                //выключаем кондиционер
            }
            dev.disp().writeOk();
        }
        else if(strcmp(cmd,"set_temp")==0&&argsCount>=1)//команда get_blinks_count
        {
            int temperature=String(args[0]).toInt();
            //как-то устанавливаем температуру
            dev.disp().writeOk();
        }
    }
}ccb;


Далее в функции setup() нужно установить описание интерфейса и созданный обработчик

void setup()
{
    Serial.begin(9600);
    dev.disp().setSensors(sensorsDef);
    dev.disp().setControls(controlsStr);
    dev.disp().installDevEventsHandler(&ccb);
    dev.resetStream();
}


Для управления устройством из приложения нужно подключить устройство к приложению, выбрать его в списке идентифицированных устройств и нажать кнопку "Control device"