Web приложение реального времени для простых устройств

от автора

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