Наш магазин на eBay Наш магазин на AliExpress Наш канал в telegram

Web против SCADA. Часть 4. Продвинутая визуализация в web-браузере. АСУТП аквариума

В прошлый раз мы остановились на том, что я обещал вам показать возможности современных 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. Переделки коснутся трёх вещей:

  1. Теперь мы заранее знаем количество датчиков и нам не нужно править шаблон в программе, нужно просто прочитать его из файла и отдать браузеру.
  2. Отдавать будем не html-страничку, а прямо SVG, — так будет меньше тормозов и не будет лишней разметки (а браузер и так всё поймёт).
  3. Уберём возможность конфигурировать выходы из браузера, оставим только управление защёлками.

Открываем наш проект в 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-картинку сразу добавлены скрипты для масштабирования, а также скрипты для анимации кнопок и отслеживания их нажатия. Чтобы увидеть возможности масштабирования картинки — попробуйте поиграться с размерами окна браузера, периодически нажимая кнопку «обновить».

ASU with WEB drag the picture with mouse ##.### Water temperature Lamp On Off Heater On Off Air On Off ##.### Air temperature

По сути, всё, что нам осталось, — это прикрутить к нашему рисунку движок, который будет периодически опрашивать сервер и в зависимости от принятых данных изменять на страничке свойства разных объектов (то есть создавать интерактив). Например, если аэратор выключен, то и пузырьки нам не нужны, а при включении света — хорошо бы сделать лампу жёлтой.

Вообще-то говоря, мы подобный интерактив уже делали в прошлых частях при помощи 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&amp;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. Часть 1. Противостояние неизбежно, результат — предсказуем.
  2. Часть 2. Простой удалённый мониторинг через web-браузер.
  3. Часть 3. Удалённое управление через web-браузер.
  4. Часть 4. Продвинутая визуализация в web-браузере. АСУТП аквариума.

Добавить комментарий