
Приветствую Хабр! Часто приходится заниматься разработкой ПО для устройств контроля и управления. Как правило, это промышленные компьютеры с относительно невысокими аппаратно-вычислительными ресурсами, управление и мониторинг которых осуществляет клиентское ПО. Клиентская часть в виде отдельного приложения имеет недостатки: при обновлении ПО самого устройства, нужно обновлять всех клиентов, да и клиент обязан быть кроссплатформенным по хорошему. Возникла идея сделать клиентское приложение в виде 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 уже использовался для сетевого обмена — пускай и здесь потрудится.
Надеюсь, данные изыскания помогут вам в решении поставленных задач!
Комментарии (10)
s60
08.04.2017 20:22OPC для меня звучит как приглашение в мир Windows).
пользуйте OPC UA… no problems на примере той же openSCADA…
s60
08.04.2017 20:43magnum333 вообще ваша идея интересна (сам подумываю про движок сбора данных отдельно, а мордочка HMI на web — ибо браузер уже есть, а web технологии весьма развиты в отличие от проприетарного мира промышленной автоматизации)
вы бы побольше конкретики добавили: что делали, для какой задачи, какие требования, что получилось в результате… фото того, о чем речь…magnum333
08.04.2017 22:49Да если честно, то пока все в разработке и тестировании еще. Фото точно не будет — не моя прихоть. Сначала стали применять сетевой обмен между серверами — wireshark все наглядно показывает что происходит в реальности — поэтому все довольно прозрачно. Потом возникла идея что грубо говоря используя web можно нарисовать что угодно (у меня аналогия QML от QT возникла почему то) — осталось данные ему передать. Есть WebSocket на наше счастье — почему бы и нет?
s60
09.04.2017 16:07я так понял вы данные прям c устройства тянете… какой протокол?
вообще из статьи непонятно сколько данных передаете, на каких скоростях…
статья на хабре предполагает, что другие смогут ченить полезное почерпнуть… а пока непонятно, что конкретно можно из статьи применить для своих задач… то что движок можно делать на c++, а морда на web это и так понятно… вопрос в конкретной реализации…magnum333
10.04.2017 18:06Измерения берем не по сетевому обмену а со встроенных АЦП грубо говоря. По сети обмен идет между вычислителями и клиентами (браузерами). Весь сетевой обмен посредством Ice на основе tcp. (можно udp). По поводу сколько передавать и на каких скоростях — но это в зависимости от желания и возможностей — ведь так мне кажется.
То что софт на с++ а морда web — да тут ничего нового нет. Но как и что использовать, что бы это было легко реализовано и при необходимости модернизировано при необходимости без особых мучений?
Предлагается использовать инструмент RPC (Ice) — который позволяет разрабатывать клиентов на разных языках (используем с++ и javascript). Удаленное взаимодействие идет через вызов функций — аргументы и возвращаемые значения автоматически сериализуются при передаче — работаем на более высоком уровне, чем socket. И далее предложен набор компонент для построения web — так, что бы воспользоваться элементами интерфейса и учесть особенности отображения различных браузеров и мобильных устройств — вот в этом и хотел показать конкретику.
San_tit
Для подобных задач правильнее было бы использовать OPC сервер для сбора данных и SCADA для GUI.
А то очень странно смотрится идея запуска web сервера на контроллере, да еще и на управляющем: банально, увеличение количества клиентов может отправить контроллер в аут.
magnum333
OPC для меня звучит как приглашение в мир Windows). Вот как можно кратко описать промышленный компьютер — который у нас является сервером: должен быть небольшим без вентиляторов и при этом не греться естественно. Время старта тоже критично. Жестких дисков нет — флешки памяти впаяны в материнскую плату. Процессор может быть смесью ПЛИС и ARM. Система может быть read-only в целом — что бы флеш память не деградировала. Поэтому либо Linux, либо Qnx работают на этом железе.
Да и не всегда есть возможность и необходимость использования сторонних SCADA систем. Когда была необходимость интеграции с другими SCADA — то использовался другой сервер более высокого уровня, который был классическим сервером в стойке — он работает с описанными серверами и «зеркалирует» их во внешний мир по различным необходимым протоколам.
Число клиентов заранее известно и ограниченно — это ведь в интернет не выходит — поэтому нагрузки нет особой, да и в основном клиенты как правило в режиме монитора работают.
F0iL
OPC UA — полностью платформонезависимый стандарт, есть реализации клиентов и серверов в т.ч. и под GNU/Linux.
magnum333
Не спорю — просто с OPC я работал во времена разработки под Windows. С Linux честно говоря и не пробовал да и идеи не возникало. OPC использовали когда была нужна тесная интеграция с другими системами, да и другие протоколы — Modbus, МЭК 104 можно применять. Мое субъективное мнение что OPC это довольно много всего уневерсального, за что приходится платить.
И допустим внедрили OPC в свое железо — а смотреть все и управлять тогда через стороний продукт SCADA? Это не всегда уместно как мне думается.
s60
про OPC UA напомнили в контексте
а не потому что обязательно применять