Часто приходится заниматься разработкой ПО для устройств контроля и управления. Как правило, это промышленные компьютеры с относительно невысокими аппаратно-вычислительными ресурсами, управление и мониторинг которых осуществляет клиентское ПО. Клиентская часть в виде отдельного приложения имеет недостатки: при обновлении ПО самого устройства, нужно обновлять всех клиентов, да и клиент обязан быть кроссплатформенным по хорошему. Возникла идея сделать клиентское приложение в виде web и желательно максимально быстро и не ресурсоемко. Надеюсь, эти изыскания помогут тем, кто думал о подобном.
Постановка задачи
И так, в наличие небольшой по ресурсам компьютер — будем называть его вычислитель (сервер), который управляет исполнительными механизмами, собирает данные, решает нужные и важные задачи. А еще их может быть несколько объединенных в сеть. ПО вычислителя низкоуровневое и написано на С++ и работает под операционкой (в моем случае Linux). И нужно извне управлять и мониторить все это через браузер (клиент).
И еще важный момент — сервер должен быть способным самостоятельно уведомлять клиента о событиях, а не только отвечать на запросы.
Примечание: Не ставлю целью описывать особенности применения и возможности используемых продуктов — это отдельная тема. Хочется рассказать что и для чего применялось и какой результат получился
Начало
Вычислителей может быть несколько и они взаимодействуют между собой по сети — здесь нашлось применение фреймворку удаленного вызова процедур Ice, а именно его версия для интернет-вещей IceE. Из исходников под нужную платформу собираем библиотеки, читаем документацию и вот сетевой обмен на уровне вызова функций работает! Но как оказалось, IceE позволяет работать и с javascript клиентами и работает через WebSocket. Ну вот решение найдено — осталось попробовать! Да и не только javascript, а и еще есть кое что.
Кратко о IceE
Сначала нужно описать взаимодействие которое хотим получить. Для этого используем специализированный язык slice. Вот пример того, что будем пробовать:
#pragma once
#include <Ice/Identity.ice>
// для с++ это namespace
module Remote {
// передаем нужные измерения - для с++ это будет vector<double>
sequence<double> Measurement;
// interface - это будет классом с двумя функциями - его реализует клиент (браузер)
interface CallbackReceiver
{
// сервер уведомляет клиента о новом значении - будет управлять progress-bar
void Callback(int num);
// сервер уведомляет клиента о новых измерениях - будет рисовать график
void SendData(Measurement m);
};
// этот класс реализует сервер для регистрации клиентов
interface CallbackSender
{
// клиент регистрируется на сервере для получения уведомлений
void AddClient(Ice::Identity ident);
};
};
На основе данного кода, средствами Ice, генерируются классы С++ для сервера и javascript код для web приложения.
Сервер
Основное — это реализовать класс удаленного взаимодействия — наследуем его от класса сгенерированного ранее.
//Remote::CallbackSender сгенерировал Ice
class ImplCallback: public Remote::CallbackSender {
public:
ImplCallback(const Ice::CommunicatorPtr& c) :
communicator { c } {
/* поток отправки событий клиенту*/
th = std::thread([this]() {
int count =0;
constexpr int sizeMeasurement=30;
/*typedef ::std::vector< ::Ice::Double> Measurement; - из сгенерированного класса*/
Measurement measurement(sizeMeasurement);
std::random_device r;
std::default_random_engine e1(r());
std::uniform_real_distribution<double> uniform_dist(-10, 10);
while(true)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lk(mut);
auto it = clients.begin();
auto itend=clients.end();
for(;it!=itend;)
{
try
{
/*передаем счетчик - на который реагирует progress-bar*/
(*it)->Callback(++count);
for(auto& m:measurement)
m=uniform_dist(e1);
/*передаем измерения - их на график*/
(*it)->SendData(measurement);
++it;
}
catch(const std::exception& ex) {
/*клиент отключился - удалим!*/
clients.erase(it++);
}
}
}
});
th.detach();
}
/*Эту функцию вызовет клиент для подключения*/
virtual void AddClient(const Ice::Identity& ident, const Ice::Current& current = ::Ice::Current()) override {
cout << "adding client `" << communicator->identityToString(ident) << "'" << endl;
std::lock_guard<std::mutex> lk(mut);
/*создаем прокси через который будем вызывать реализованные клиентом методы. И сохраняем его*/
CallbackReceiverPrx c = CallbackReceiverPrx::uncheckedCast(current.con->createProxy(ident));
clients.insert(c);
}
private:
/*всех подключившихся клиентов храним здесь*/
std::set<Remote::CallbackReceiverPrx> clients;
Ice::CommunicatorPtr communicator;
std::mutex mut;
std::thread th;
};
Осталось только все это запустить. Ниже приведена функция потока, выполняющего необходимые настройки и запуск системы Ice.
void ServerFun() {
Ice::CommunicatorPtr ic;
try {
/*инициализация Ice*/
ic = Ice::initialize();
/*создаем адаптер WebSocket на порту 20002*/
/*настройки удобнее хранить в специальном файле - но упростим для наглядности*/
Ice::ObjectAdapterPtr adapter2 = ic->createObjectAdapterWithEndpoints("Callback.Server", "ws -p 20002");
/*Добавлям адаптеру наш обработчик ImplCallback и назначаем ему идентификатор sender*/
adapter2->add(new ImplCallback(ic), ic->stringToIdentity("sender"));
/*и теперь все готово - запускаем!*/
adapter2->activate();
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
ic->shutdown();
ic->destroy();
} catch (const std::exception& ex) {
cout << ex.what() << endl;
if (ic) {
try {
ic->destroy();
} catch (const Ice::Exception& ex2) {
cout << ex2 << endl;
}
}
}
}
Вот и весь сервер. Проще сложно представить.
Клиент
Для упрощения разработки web приложения, используем bootstrap — содержит предопределенные стили, компоненты, компоновщики и много еще чего. Для привязки данных и реализации модели MVC применим AngularJS. И хочется графики порисовать для наглядности передачи массивов данных — нам поможет flotr2. Текст html пропустим — кроме размещения компонент и привязки данных там нет интересной информации Теперь на очереди javascript файл приложения:
"use strict"
var app = angular.module('webApp', []);
// angular контроллер нашего приложения
app.controller('webController', function myController($scope) {
//режим отрисовки графиков 1-линия 2-гистограмма 3-точки
$scope.mode = 1;
//progress-bar от 0 до 100
$scope.valuenow = 0;
//функции смены режимов графика - обработчики radio html страницы
$scope.mode1 = function() {
$scope.mode = 1;
}
var communicator = Ice.initialize();
// реализуем методы которые вызывает сервер
var CallbackReceiverI = Ice.Class(Remote.CallbackReceiver, {
//сервер управляет progress-bar
Callback : function(num, current) {
$scope.valuenow = num % 100;
$scope.$apply();
},
//сервер передает данные для графика
SendData: function(measurement){
var data, graph;
var container = document.getElementById('container');
data = [];
for (var i = 0; i <measurement.length; ++i) {
data.push([ i, measurement[i] ]);
}
//в зависимости от режима используем flotr2 для построения графиков.
if ($scope.mode == 1) {
graph = Flotr.draw(container, [ data ], {
colors : [ '#C0D800' ],
yaxis : {
max : 12,
min : -12
}
});
}
//else рисуем по другому ...
}
});
var proxy2 = communicator.stringToProxy("sender:ws -h localhost -p 20002");
//устанавливаем соединение с сервером и регистрируемся с помощью AddClient
Remote.CallbackSenderPrx.checkedCast(proxy2).then(function(pr2) {
communicator.createObjectAdapter("").then(function(adapter) {
var r = adapter.addWithUUID(new CallbackReceiverI());
proxy2.ice_getCachedConnection().setAdapter(adapter);
pr2.AddClient(r.ice_getIdentity());
//предотвратим закрытие соединения периодической отправкой Heartbeat
proxy2.ice_getCachedConnection().setACM(undefined, undefined, Ice.ACMHeartbeat.HeartbeatAlways);
});
});
});
Итог
Теперь запускаем приложение сервера и открываем браузером нашу html страницу и видим:
Обмен идет! Данные передаются!
И так, что использовалось:
В результате, используя указанный набор компонент, возможно достаточно быстро реализовать web приложение для контроля и управления нашим сервером, не особенно усложняя ПО сервера и выполняя взаимодействие с клиентом прямо из кода основного приложения.
Еще рассматривал вариант применения Wt. Тоже очень интересная вещь. Но мне кажется, что в рассматриваемом в данной статье решение больше гибкости по реализации самого клиентского ПО — можем применять любые необходимые нам средства для web разработки. Да и Ice уже использовался для сетевого обмена — пускай и здесь потрудится.
Надеюсь, данные изыскания помогут вам в решении поставленных задач!
Автор: magnum333