В прошлый раз мы остановились на том, что я обещал вам показать возможности современных web-технологий в части продвинутой визуализации систем управления. Именно об этом сегодня и пойдёт речь.
Зачем нужна красивая визуализация думаю никому объяснять не нужно, — она делает удобным и понятным взаимодействие системы управления и человека (оператора).
В классических скадах для визуализации обычно существует специальная графическая оболочка, позволяющая оператору не только видеть все датчики и исполнительные механизмы в привязке к объекту управления, но и интерактивно управлять процессом. Именно она подразумевается под красивой аббревиатурой HMI — human machine interface, что по-русски переводится как человеко-машинный интерфейс. Естественно каждая такая специальная оболочка лицензируется и естественно имеет специальный красивый ценник.
Ну, с интерактивным управлением из web-браузера мы ещё в прошый раз разобрались, так что нам, чтобы приблизить браузер к возможностям графических оболочек SCADA-систем, осталось только научиться рисовать в нём красивые «живые» картинки.
Как же мы будем это делать? Всё очень просто, — «используй силу, Люк» ( © Оби-Ван Кеноби). Нужную силу дал нам консорциум W3C, выпустив ещё в 2001-м году спецификацию 1.0 на стандарт SVG и сделав рекомендацией в 2011-м году спецификацию 1.1 (после чего поддержка этого стандарта стала появляться во многих современных браузерах).
SVG (Scalable Vector Graphics) — масштабируемая векторная графика. Эта штука позволяет отрисовывать в браузере любые картинки при помощи специальной разметки, подобной html (то есть «создание» рисунка может происходить прямо в блокноте). При этом картинки получаются отлично масштабируемыми (это же вектор!), плюс каждый элемент такой картинки (каждая линия, окружность, прямоугольник…) — это отдельный объект (соответственно, этими объектами можно манипулировать при помощи javascript).
Вообще, SVG — тема обширная и интересная, однако выходящая за рамки этой статьи (мы как-нибудь об этом отдельно поговорим), поэтому перейдём, наконец, к построению АСУТП.
Итак, пусть мы хотим с помощью шлюза UART-to-I2C/SPI/1W автоматизировать аквариум. При этом у нас есть два датчика температуры (TE1 — температура воды, TE2 — температура окружающего воздуха) и три дискретных выхода (IO0 — включение/выключение лампы подсветки, IO1 — включение/выключение нагревателя, IO2 — включение/выключение аэратора).
Последнюю версию проекта для управления шлюзом через web (в C++ Builder) можно скачать вот здесь. Эту версию мы и будем переделывать.
Для начала переделаем саму программу в С++ Builder. Переделки коснутся трёх вещей:
- Теперь мы заранее знаем количество датчиков и нам не нужно править шаблон в программе, нужно просто прочитать его из файла и отдать браузеру.
- Отдавать будем не html-страничку, а прямо SVG, — так будет меньше тормозов и не будет лишней разметки (а браузер и так всё поймёт).
- Уберём возможность конфигурировать выходы из браузера, оставим только управление защёлками.
Открываем наш проект в C++ Builder и меняем кусок кода, в котором мы загружали шаблон, на вот такой:
if(FileExists("visual.svg")) // если файл шаблона существует { Memo1->Lines->Clear(); Memo1->Lines->LoadFromFile("visual.svg"); // загружаем его в Memo Shablon=Memo1->Text; // и копируем в переменную Shablon } else // если нет - надо об этом сообщить { Shablon="<html><body><p>Not found shablon.svg</p></body></html>"; } |
В обработчике нажатия кнопки поиска устройств на шине 1W удаляем вот этот кусок кода:
// добавляем ячейки в html-шаблон int TempPos=Shablon.Pos("</table></body>"); // ищем таблицу для датчиков // и вписываем в неё ячейки для нового датчика Shablon.Insert("<tr><td>TE"+IntToStr(Dev)+ "</td><td id=TE"+IntToStr(Dev)+ "></td></tr>",TempPos); |
Поменяем обработчик команды запроса параметров IO (пусть команда вместо GetIO будет называться GetIOData), а также удалим лишние обработчики распарсенных команд из браузера (поскольку конфу мы решили не менять, то таких команд останется только три, вместо шести). Новый вариант этого куска кода в обработчике события CommandGet должен выглядеть так:
// если среди параметров есть запрос IO if(RequestInfo->Params->Names[i]=="GetIOData") { for(int j=0; j<3; j++) // добавляем теги и значения через ; { // значение TempStr=TempStr+";"+StringGrid2->Cells[0][j+1]+"val;"+ StringGrid2->Cells[2][j+1]; } } // обработчики команд от дистанционных кнопок (из браузера) if(RequestInfo->Params->Names[i]=="io0set") { if(RequestInfo->Params->Values["io0set"]==1) IO0Set1=true; else if(RequestInfo->Params->Values["io0set"]==0) IO0Set0=true; else break; } if(RequestInfo->Params->Names[i]=="io1set") { if(RequestInfo->Params->Values["io1set"]==1) IO1Set1=true; else if(RequestInfo->Params->Values["io1set"]==0) IO1Set0=true; else break; } if(RequestInfo->Params->Names[i]=="io2set") { if(RequestInfo->Params->Values["io2set"]==1) IO2Set1=true; else if(RequestInfo->Params->Values["io2set"]==0) IO2Set0=true; else break; } |
Кроме всего прочего, давайте добавим кнопку, с помощью которой можно обновить отдаваемую web-сервером страницу (чтобы каждый раз не перезапускать программу после правки шаблона). Её обработчик должен содержать следующий код:
if(FileExists("visual.html")) // если файл шаблона существует { Memo1->Lines->Clear(); Memo1->Lines->LoadFromFile("visual.svg"); // загружаем его в Memo Shablon=Memo1->Text; // и копируем в переменную Shablon } |
С программой на этом всё. Теперь нужно нарисовать сам шаблон в формате SVG. Описывать процесс рисования я не буду (читайте литературу, изучайте svg-разметку, тэги), скажу лишь, что этот шаблон я «нарисовал» полностью в блокноте всего за пару часов. Сам рисунок можно увидеть ниже (а можно его и сохранить прямо отсюда).
В эту SVG-картинку сразу добавлены скрипты для масштабирования, а также скрипты для анимации кнопок и отслеживания их нажатия. Чтобы увидеть возможности масштабирования картинки — попробуйте поиграться с размерами окна браузера, периодически нажимая кнопку «обновить».
По сути, всё, что нам осталось, — это прикрутить к нашему рисунку движок, который будет периодически опрашивать сервер и в зависимости от принятых данных изменять на страничке свойства разных объектов (то есть создавать интерактив). Например, если аэратор выключен, то и пузырьки нам не нужны, а при включении света — хорошо бы сделать лампу жёлтой.
Вообще-то говоря, мы подобный интерактив уже делали в прошлых частях при помощи AJAX (когда получали от сервера и выводили в таблицу значения температур, состояния защёлок и их конфигурацию), однако с SVG всё несколько сложнее (например, в SVG не будет работать innerHTML и некоторые другие вещи). Кроме того, создание объекта XMLHttpRequest отличается для разных браузеров. В связи с этим движок был переработан (за что отдельное спасибо таварищу Virtual-у и его светлой голове). В результате получился максимально кроссбраузерный код, работающий и в «лисе», и в «очке», и в «огрызках». Этот код выглядит следующим образом:
<script type="text/javascript"> <![CDATA[ var base_url='index.html'; var fetch_interval=1000; function handle_error(){return -1;} if (typeof getURL == 'undefined') { getURL = function(url, callback) { if (!url) throw 'No URL for getURL'; try { if (typeof callback.operationComplete == 'function') { callback = callback.operationComplete; } } catch (e) {} if (typeof callback != 'function') { throw 'No callback function for getURL'; } var http_request = null; if (typeof XMLHttpRequest != 'undefined') { http_request = new XMLHttpRequest(); } else if (typeof ActiveXObject != 'undefined') { try { http_request = new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) { try { http_request = new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {} } } if (!http_request) { throw 'Both getURL and XMLHttpRequest are undefined'; } http_request.onreadystatechange = function() { if (http_request.readyState == 4) { callback( { success : true, content : http_request.responseText, contentType : http_request.getResponseHeader("Content-Type") } ); } } http_request.open('GET', url, true); http_request.setRequestHeader('X-Requested-With', 'XmlHttpRequest'); http_request.send(null); } } function fetch_data() { var fetch_url=base_url; // здесь можно добавлять в url дополнительные параметры, // в зависимости от нажатия кнопок if (fetch_url) { getURL(fetch_url, plot_data); } else { handle_error(); } } function init() { fetch_data(); base_url='index.html?GetTemp=true&GetIOData=true'; intTimer = setInterval('fetch_data()', fetch_interval); } function plot_data(obj) { if (!obj.success) return handle_error(); // getURL failed to get data if (!obj.content) return handle_error(); // getURL get empty data, IE problem var ArrVal=obj.content.split(';'); // распарсиваем принятые данные в массив var obj_tag; // переменная для доступа к объектам // здесь можно в зависимости от того, что мы приняли, менять // свойства разных объектов // получаем доступ к объекту, свойства которого будем менять obj_tag=document.getElementById("имя объекта"); // так можно изменить текст obj_tag.firstChild.data = "Text"; // так можно поменять атрибуты, например цвет obj_tag.setAttribute("имя атрибута","значение"); } ]]> </script> |
Осталось только вмонтировать этот движок в нашу svg-картинку, описать в движке какие свойства и у каких объектов мы будем менять, а также описать какие дополнительные параметры мы будем добавлять в GET-запрос при нажатии на кнопки.
Итоговый вариант проекта можно скачать вот здесь.
А вот по этой ссылке можно посмотреть видео о том, как работает наша самодельная web-scada.
Ну а в следующий раз мы попробуем прикрутить к нашему проекту базу данных MySQL, организовать с её помощью хранение исторических данных и отрисовку трендов.
- Часть 1. Противостояние неизбежно, результат — предсказуем.
- Часть 2. Простой удалённый мониторинг через web-браузер.
- Часть 3. Удалённое управление через web-браузер.
- Часть 4. Продвинутая визуализация в web-браузере. АСУТП аквариума.