Устройства взаимодействуют с локальным сервером по специальному протоколу (связь точка-точка и устройства на общей шине). Для разработки устройств была разработана специальная библиотека на языке С++, исходно предназначенная для Arduino IDE, но легко переносимая и на другие контроллеры(отсутствуют зависимости от специфичных библиотекк Arduino). Библиотека предназначена для микроконтроллеров с сильно ограниченными ресурсами, таких как AVR или STM. Функциональность библиотеки можно условно поделить на две части: инструменты для разработки единичного устройства, напрямую подключающегося к серверу (например, по COM поверх USB или по TCP/IP) и инструменты, предназначенные для разработки сети устройств, объединенных по принципу общей шины (например, хаб, подключающийся к серверу по USB, и устройства, объединенные в шину по RS-485).

Разработка единичного устройства

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

Обработка сообщений и команд

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

Обработка сообщений производится объектом класса ARpcRealDeviceMessageDispatch, доступным с помощью метода ARpcDevice::disp(). Так же этот объект позволяет формировать различные сообщения.

Список датчиков и описание интерфейса управления - строки языка С (const char*), содержащие соответствующие описания в xml или json формате, как описано в протоколе взаимодействия между устройствами. Эти строки должны быть глобальными переменными, так как библиотека их не копирует.

При обработке команды нужно обязательно вернуть сообщение ok или err, вызвав соответствующий метод ARpcRealDeviceMessageDispatch::writeOk() или ARpcRealDeviceMessageDispatch::writeErr(). Если выполнение команды занимает значительное время, необходимо регулярно отправлять сообщение синхронизации с помощью метода ARpcRealDeviceMessageDispatch::writeCmdSync().

Измерения передаются с помощью методов ARpcRealDeviceMessageDispatch::writeMeasurement и ARpcRealDeviceMessageDispatch::writeMeasurementB. Первый метод передает данные в текстовом виде, для его использования необходимо преобразовывать числа в строки. Второй метод предназначен для передачи данных в бинарном виде.

Состояние устройства

Метод ARpcDevice::state() возвращает объект класса ARpcState, представляющий состояние устройства. Состояние устройства подразделяется на две группы: состояние интерфейса управления и дополнительное состояние. Состояние интерфейса управления указывает, в каких позициях должны находиться элементы в интерфейсе управления, чтобы соответствовать текущим параметрам работы устройства.

Пример:
есть команда set_speed, устанавливающая скорость вращения вентилятора в диапазоне от 0 до 100. Этой команде соответствует элемент управления типа слайдер в интерфейсе управления. Тогда если соответствующее состояние задано, у пользователя при открытии интерфейса управления слайдер будет находиться в позиции, соответствующей текущей установленной скорости вращения на устройстве.

 

Перед использованием состояние устройства нужно инициализировать. Для инициализации состояния интерфейса управления нужно воспользоваться методом ARpcState::prepareCommands(count), затем методом ARpcState::prepareCommand(index,name,parametersCount) для каждой команды. Для инициальзации дополнительного состояния нужно вызвать метод ARpcState::prepareAdditionalParameters(count), затем метод ARpcState::prepareAdditionalParameter(index,name) для каждого доп. параметра.

Пример:
для указанной выше команды (если она одна) последовательность вызовов будет следующая:
ARpcState *state=....;//запрашиваем указатель на объект-состояние
state->prepareCommands(1);
state->prepareCommand(0,"set_speed",1);

При изменении состояния устройства нужно вызвать метод ARpcState::setCommandParamState или ARpcState::setAdditionalParamState. Библиотека сохранит измененное состояние и сгенерирует соответствующее нотификационное сообщение.

Обработка broadcast уведомлений от сервера

Для обработки широковещательных уведомлений от локального сервера предназначен класс ARpcSrvReady. На данный момент объект этого класса можно использовать для обработки широковещательных UDP уведомлений в локальной IP сети. Сервер рассылает регулярные оповещения на UDP порт 4081, данные передаются в объект класса ARpcSrvReady с помощью метода ARpcSrvReady::putByte(), обработка обнаруженных и успешно разобранных сообщений происходит через интерфейс ARpcISrvReadyCallback. Если на устройстве есть энергонезависимая память, можно настроить его так, например, чтобы оно подключалось только к конкретному локальному серверу.

Список датчиков и описание интерфейса управления

Для описания списка датчиков и интерфейса управления используется строка в xml или json формате. Для разработки описаний можно использовать утилиту ARpcUiGen или же сформировать список вручную согласно описанию из документации.

Пример кода тестового устройства на МК Arduino Uno или Leonardo

//подключаем библиотеку ARpc
#include <ARpcDevice.h>

int ledPin=13;//пин светодиода
unsigned int blinksCount=0;//число миганий
const char *deviceName="led_blink_test";//имя устройства
const ARpcUuid deviceId("f84526c15e88431581f8f7da45daa09d");//идентификатор устройства

//Описание интерфейса управления
const char *interfaceStr=
"<controls>"
//начало основной группы команд
"<group title=\"Device controls\">"
//команда мигания светодиодом blink
"<control command=\"blink\" title=\"blink\">"
//параметр команды #0 - время горения светодиода
"<param type=\"slider\" title=\"delay\"><attributes max=\"1000\" min=\"100\"/></param>"
"</control>"
//команда get_blinks_count - заставляет устройство сгенерировать измерение,
//содержащее количество миганий светодиодом с момента запуска устройства
"<control command=\"get_blinks_count\" title=\"get_blinks_count\"/>"
"</group></controls>";

//Описание датчиков
const char *sensorsDef="<sensors>"
//первый датчик - blinks_count, содержит количество миганий светодиодом с момента запуска устройства
"<sensor name=\"blinks_count\" type=\"f32_sv\"/>"
//второй датчик - sin_x, содержит двумерное значение со значениями sin и cos, меняющимися со временем
"<sensor name=\"sin_x\" type=\"f32_sv_d2\"/>"
"</sensors>";

class WriteCallback
    :public ARpcIWriteCallback
{
public:
    //callback-функции, вызываемые библиотекой, когда нужно передать данные от устройства
    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;

//объект парсера ARpc, 300 - объем буфера для сообщения
ARpcDevice dev(300,&wcb,&deviceId,deviceName);

class CommandCallback
    :public ARpcIDevEventsCallback
{
public:
    //мигание светодиодом заданное время
    void blink(int dl)
    {
        digitalWrite(ledPin,HIGH);
        delay(dl);
        digitalWrite(ledPin,LOW);
    }

    //callback-функция, вызываемая библиотекой, когда на устройство приходит пользовательская команда
    virtual void processCommand(const char *cmd,const char *args[],unsigned char argsCount)
    {
        if(strcmp(cmd,"blink")==0&&argsCount>=1)//команда blink, проверяем что есть аргумент
        {
            //аргумент - время горения светодиода в мс
            int dl=String(args[0]).toInt();
            //правим - от 100 до 1000 мс
            if(dl<100)dl=100;
            else if(dl>1000)dl=1000;
            //мигаем
            blink(dl);
            //инкрементируем число миганий
            ++blinksCount;
            //выдаем новое измерение количества миганий
            dev.disp().writeMeasurement("blinks_count",String(blinksCount).c_str());
            //сообщаем об успешном выполнении команды
            dev.disp().writeOk();
        }
        else if(strcmp(cmd,"get_blinks_count")==0)//команда get_blinks_count
        {
            //выдаем измерение количества миганий
            dev.disp().writeMeasurement("blinks_count",String(blinksCount).c_str());
            //сообщаем об успешном выполнении команды
            dev.disp().writeOk();
        }
        else dev.disp().writeErr("Unknown cmd",cmd);//неизвестная команда
    }
}ccb;

//setup
void setup()
{
    Serial.begin(9600);//запускаем Serial
    pinMode(ledPin,OUTPUT);//настраиваем пин для мигания
    dev.disp().installDevEventsHandler(&ccb);//устанавливаем обработчик команд
    dev.disp().setControls(interfaceStr);//указываем строку с описанием интерфейса управления
    dev.disp().setSensors(sensorsDef);//указываем строку с описанием сенсоров
}

//генерация отсчетов sin и cos
int t=0;
float sinCos[2];
void writeSinVal()
{
    //Генерируем строки со значениями
    sinCos[0]=sin(0.1*t);
    sinCos[1]=cos(0.1*t);
    //отправляем измерение
    dev.disp().writeMeasurementB("sin_x",sinCos,2);
    //увеличиваем "время"
    ++t;
}

void loop()
{
    //проверяем, нет ли данных в Serial
    while(Serial.available())
        dev.putByte(Serial.read());//если данные есть, передаем в объект библиотеки
    writeSinVal();//генерируем следующий отсчет sin и cos
    delay(500);//пауза пол-секунды
}