Приветствую Хабр! Часто приходится заниматься разработкой ПО для устройств контроля и управления. Как правило, это промышленные компьютеры с относительно невысокими аппаратно-вычислительными ресурсами, управление и мониторинг которых осуществляет клиентское ПО. Клиентская часть в виде отдельного приложения имеет недостатки: при обновлении ПО самого устройства, нужно обновлять всех клиентов, да и клиент обязан быть кроссплатформенным по хорошему. Возникла идея сделать клиентское приложение в виде 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 уже использовался для сетевого обмена — пускай и здесь потрудится.
Надеюсь, данные изыскания помогут вам в решении поставленных задач!
ссылка на оригинал статьи https://habrahabr.ru/post/325942/
Добавить комментарий