javascript

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

  • суббота, 8 апреля 2017 г. в 03:14:57
https://habrahabr.ru/post/325942/
  • JavaScript
  • HTML
  • C++




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

Надеюсь, данные изыскания помогут вам в решении поставленных задач!