Web проти SCADA. Частина 3. Віддалене управління через web-браузер

Отже, з моніторингом ми минулого разу розібралися, тому сьогодні будемо вирішувати завдання віддаленого управління і конфігурації.

Як ви пам'ятаєте, для прикладу ми взяли в якості контролера шлюз UART-to-I2C / SPI / 1W . Останню версію того, що у нас вийшло (саморобний web-сервер на C ++ Builder, що дозволяє віддалено моніторити підключення до шлюзу далласовскіе термометри і дискретні I / O), можна скачати ось тут . Її-то ми і будемо доповнювати можливостями управління і конфігурації.

Тепер давайте подумаємо, що нам потрібно для вирішення нашої задачі? Ну, очевидно, потрібно мати можливість відправляти з web-сторінки команди сервера на перемикання I / O або оновлення списку підключених датчиків або ... Коротше, головне в усьому цьому - мати можливість віддалено відправляти команди сервера з web-сторінки.

Хм, але ж ми вже маємо таку можливість. Дійсно, коли ми через параметри GET-запиту запитуємо у сервера стан I / O або значення виміряних термометрами температур, ми фактично даємо сервера команди відправити нам ці значення. Тобто зараз нам потрібно просто додати ще кілька параметрів-команд, прописати реакцію сервера на ці команди і модифікувати web-сторінку таким чином, щоб нові параметри додавалися в GET-запит не по часу (кожну секунду, дві, три і так далі), а з якихось інших подій (після натискання кнопок, по перевищенню температурою певних значень ...) Все інше ми вміємо.

Отже, спочатку модифікуємо шаблон віддається сервером сторінки.

Там, де в шаблоні описуються глобальні змінні, додамо ще кілька (ну як кілька, ще 12 штук до тих трьох, які вже є):

// глобальні змінні var top_http; // тут будемо зберігати покажчик на об'єкт XMLHttpRequest var status; // ця змінна потрібна щоб пізнати, що відповідь отримана і оброблений var elements_col; // кількість змінних, які ми будемо запитувати / отримувати var swio0input; // команда перемикання io0 на вхід var swio0output; // команда перемикання io0 на вихід var swio1input; // команда перемикання io1 на вхід var swio1output; // команда перемикання io1 на вихід var swio2input; // команда перемикання io2 на вхід var swio2output; // команда перемикання io2 на вихід var setio0low; // команда установки io0 в 0 var setio0hi; // команда установки io0 в 1 var setio1low; // команда установки io1 в 0 var setio1hi; // команда установки io1 в 1 var setio2low; // команда установки io2 в 0 var setio2hi; // команда установки io2 в 1

// глобальні змінні var top_http; // тут будемо зберігати покажчик на об'єкт XMLHttpRequest var status; // ця змінна потрібна щоб пізнати, що відповідь отримана і оброблений var elements_col; // кількість змінних, які ми будемо запитувати / отримувати var swio0input; // команда перемикання io0 на вхід var swio0output; // команда перемикання io0 на вихід var swio1input; // команда перемикання io1 на вхід var swio1output; // команда перемикання io1 на вихід var swio2input; // команда перемикання io2 на вхід var swio2output; // команда перемикання io2 на вихід var setio0low; // команда установки io0 в 0 var setio0hi; // команда установки io0 в 1 var setio1low; // команда установки io1 в 0 var setio1hi; // команда установки io1 в 1 var setio2low; // команда установки io2 в 0 var setio2hi; // команда установки io2 в 1

Для того, щоб сторінка виглядала більш читабельно, додамо в секцію head глобальний стиль для таблиць, а так само окремий стиль для осередків нової таблиці, що імітують кнопки (робити кнопки через теги input або button вже не модно, ага; стилі і javascript дозволяють зробити кнопки практично з будь-якого елементу):

<! - додамо глобальний стиль для таблиць і для кнопок -> <style type = "text / css"> td {border: 1px solid blue; padding: 10px; } Td .button {background -color: # 40C781; box -shadow: 0 - 3px # 35A76E inset; } Td .button: hover {background -color: # 35A76E; } Td .button: active {background -color: # 21935A; box -shadow: 0 3px # 21935A inset; } </ Style>

<! - додамо глобальний стиль для таблиць і для кнопок -> <style type = "text / css"> td {border: 1px solid blue; padding: 10px; } Td.button {background-color: # 40C781; box-shadow: 0 -3px # 35A76E inset; } Td.button: hover {background-color: # 35A76E; } Td.button: active {background-color: # 21935A; box-shadow: 0 3px # 21935A inset; } </ Style>

Додамо в шаблон саму нову таблицю:

<P> Таблиця управління I / O: </ p> <table style = "border: 1px solid blue"> <tr> <td> Tag </ td> <td> Configuration </ td> <td> Value </ td> </ tr> <tr> <td rowspan = "2"> io0 </ td> <td class = "button" onclick = "sw1 ()"> switch to input </ td> <td class = "button "onclick =" set1 () "> set to 1 </ td> </ tr> <tr> <td class =" button "onclick =" sw2 () "> switch to output </ td> <td class =" button "onclick =" set2 () "> set to 0 </ td> </ tr> <tr> <td rowspan =" 2 "> io1 </ td> <td class =" button "onclick =" sw3 () "> switch to input </ td> <td class =" button "onclick =" set3 () "> set to 1 </ td> </ tr> <tr> <td class =" button "onclick =" sw4 ( ) "> switch to output </ td> <td class =" button "onclick =" set4 () "> set to 0 </ td> </ tr> <tr> <td rowspan =" 2 "> io2 </ td> <td class = "button" onclick = "sw5 ()"> switch to input </ td> <td class = "button" onclick = "set5 ()"> set to 1 </ td> </ tr> <tr> <td class = "button" onclick = "sw6 ()"> switch to output </ td> <td class = "button" onclick = "set6 ()"> set to 0 </ td> </ Tr> </ table>

<P> Таблиця управління I / O: </ p> <table style = "border: 1px solid blue"> <tr> <td> Tag </ td> <td> Configuration </ td> <td> Value </ td> </ tr> <tr> <td rowspan = "2"> io0 </ td> <td class = "button" onclick = "sw1 ()"> switch to input </ td> <td class = "button "onclick =" set1 () "> set to 1 </ td> </ tr> <tr> <td class =" button "onclick =" sw2 () "> switch to output </ td> <td class =" button "onclick =" set2 () "> set to 0 </ td> </ tr> <tr> <td rowspan =" 2 "> io1 </ td> <td class =" button "onclick =" sw3 () "> switch to input </ td> <td class =" button "onclick =" set3 () "> set to 1 </ td> </ tr> <tr> <td class =" button "onclick =" sw4 ( ) "> switch to output </ td> <td class =" button "onclick =" set4 () "> set to 0 </ td> </ tr> <tr> <td rowspan =" 2 "> io2 </ td> <td class = "button" onclick = "sw5 ()"> switch to input </ td> <td class = "button" onclick = "set5 ()"> set to 1 </ td> </ tr> <tr> <td class = "button" onclick = "sw6 ()"> switch to output </ td> <td class = "button" onclick = "set6 ()"> set to 0 </ td> </ tr > </ table>

У секцію head, туди, де у нас описані яваскрипт, напишемо обробники подій onclick для кнопок (для елементів таблиці, що імітують кнопки). Ці обробники будуть встановлювати в true значення заведених нами раніше змінних (таким чином ми надалі зможемо визначати - було натискання на кнопку чи ні):

function sw1 () {swio0input = true; } Function sw2 () {swio0output = true; } Function sw3 () {swio1input = true; } Function sw4 () {swio1output = true; } Function sw5 () {swio2input = true; } Function sw6 () {swio2output = true; } Function set1 () {setio0hi = true; } Function set2 () {setio0low = true; } Function set3 () {setio1hi = true; } Function set4 () {setio1low = true; } Function set5 () {setio2hi = true; } Function set6 () {setio2low = true; }

function sw1 () {swio0input = true; } Function sw2 () {swio0output = true; } Function sw3 () {swio1input = true; } Function sw4 () {swio1output = true; } Function sw5 () {swio2input = true; } Function sw6 () {swio2output = true; } Function set1 () {setio0hi = true; } Function set2 () {setio0low = true; } Function set3 () {setio1hi = true; } Function set4 () {setio1low = true; } Function set5 () {setio2hi = true; } Function set6 () {setio2low = true; }

У шаблоні залишилося тільки переписати функцію формування запиту. Перепишемо її таким чином, щоб у разі, якщо було натискання на кнопку, - в запит додавалася відповідна команда сервера:

function sendTop () // В цій функції ми формуємо запит і відправляємо його на сервер {status = 0; // скидаємо статус top_http = getXmlHttpRequestObject (); // викликаємо функцію створення об'єкта XMLHttpRequest top_http .onreadystatechange = handleTop; // чіпляємо на onreadystatechange обробник відповіді var url = '? GetTemp = true & GetIO = true'; // рядок запиту if (swio0input == true) url = url + '& io0sw = 1'; // якщо було натискання - if (swio0output == true) url = url + '& io0sw = 0'; // додаємо команду в запит if (swio1input == true) url = url + '& io1sw = 1'; if (swio1output == true) url = url + '& io1sw = 0'; if (swio2input == true) url = url + '& io2sw = 1'; if (swio2output == true) url = url + '& io2sw = 0'; if (setio0low == true) url = url + '& io0set = 0'; if (setio0hi == true) url = url + '& io0set = 1'; if (setio1low == true) url = url + '& io1set = 0'; if (setio1hi == true) url = url + '& io1set = 1'; if (setio2low == true) url = url + '& io2set = 0'; if (setio2hi == true) url = url + '& io2set = 1'; top_http .open ( 'GET', url, true); // налаштовуємо асинхронний запит з адресою url top_http .setRequestHeader ( 'If-Modified-Since "," 0 "); // додаємо заголовки top_http .setRequestHeader ( 'Cache-Control', 'no-cache'); top_http .send (null); // відправляємо запит}

function sendTop () // В цій функції ми формуємо запит і відправляємо його на сервер {status = 0; // скидаємо статус top_http = getXmlHttpRequestObject (); // викликаємо функцію створення об'єкта XMLHttpRequest top_http.onreadystatechange = handleTop; // чіпляємо на onreadystatechange обробник відповіді var url = '? GetTemp = true & GetIO = true'; // рядок запиту if (swio0input == true) url = url + '& io0sw = 1'; // якщо було натискання - if (swio0output == true) url = url + '& io0sw = 0'; // додаємо команду в запит if (swio1input == true) url = url + '& io1sw = 1'; if (swio1output == true) url = url + '& io1sw = 0'; if (swio2input == true) url = url + '& io2sw = 1'; if (swio2output == true) url = url + '& io2sw = 0'; if (setio0low == true) url = url + '& io0set = 0'; if (setio0hi == true) url = url + '& io0set = 1'; if (setio1low == true) url = url + '& io1set = 0'; if (setio1hi == true) url = url + '& io1set = 1'; if (setio2low == true) url = url + '& io2set = 0'; if (setio2hi == true) url = url + '& io2set = 1'; top_http.open ( 'GET', url, true); // налаштовуємо асинхронний запит з адресою url top_http.setRequestHeader ( 'If-Modified-Since "," 0 "); // додаємо заголовки top_http.setRequestHeader ( 'Cache-Control', 'no-cache'); top_http.send (null); // відправляємо запит}

Ми забули ще один важливий момент. Змінні swio і setio потрібно ще й якось скидати, інакше після першого і єдиного натискання на кнопку, відповідна команда буде додаватися в запит при всіх наступних викликах функції sendTop.

Можна, наприклад, скидати їх після того, як запит буде відправлено, - в цьому випадку не гарантується, що сервер цю команду виконає, оскільки можливо, що цей запит до сервера і не долетить.

Можна скидати ці змінні після того, як отримали позитивну відповідь від сервера (код 200 - Ok!), Тоді команда буде надсилатися до тих пір, поки сервер її не виконає (якщо перший запит до сервера не долетів - команда буде відправлена ​​в наступному запиті, якщо і він не долетів, тоді в наступному - і так далі, і скасувати цю команду буде не можна).

Можна, до речі, взагалі, при натисканні на кнопку не додавати команду в періодично формуються запити, а відправляти на сервер окремий запит, який містить лише цю саму команду.

Можна придумати ще купу різних варіантів, але ми будемо використовувати другий. Для цього перепишемо код обробника функції handleTop наступним чином:

function handleTop () // обробник відповіді сервера {if (top_http .readyState == 4) // якщо запит виконаний (4 - стан complete) {if (top_http .status == 200) // якщо статус відповіді - 200 (Ok! ) {swio0input = false; // скидаємо натискання кнопок swio0output = false; swio1input = false; swio1output = false; swio2input = false; swio2output = false; setio0low = false; setio0hi = false; setio1low = false; setio1hi = false; setio2low = false; setio2hi = false; var value = top_http .responseText; // отримуємо в змінну текст відповіді var ArrVal = value .split ( ';'); // поділяємо відповідь на масив параметрів var obj_tag; // тут буде покажчик на контейнер var i = 0; // це просто лічильник elements_col = ArrVal [0] + 6; // кількість елементів, які потрібно // обробити: кількість датчиків + // 6 - для I / O (3 - конф, 3 - засувки) while (i & lt; elements_col) {// перевіряємо, чи є на сторінці контейнер з потрібним id ? obj_tag = document .getElementById (ArrVal [2 * i + 1]); if (obj_tag) // якщо є {// міняємо текст всередині нього на прийнятий від сервера obj_tag .innerHTML = ArrVal [2 * i + 2]; } I = i + 1; // перевіряємо наступний елемент} status = 1; // міняємо статус}}}

function handleTop () // обробник відповіді сервера {if (top_http.readyState == 4) // якщо запит виконаний (4 - стан complete) {if (top_http.status == 200) // якщо статус відповіді - 200 (Ok! ) {swio0input = false; // скидаємо натискання кнопок swio0output = false; swio1input = false; swio1output = false; swio2input = false; swio2output = false; setio0low = false; setio0hi = false; setio1low = false; setio1hi = false; setio2low = false; setio2hi = false; var value = top_http.responseText; // отримуємо в змінну текст відповіді var ArrVal = value.split ( ';'); // поділяємо відповідь на масив параметрів var obj_tag; // тут буде покажчик на контейнер var i = 0; // це просто лічильник elements_col = ArrVal [0] +6; // кількість елементів, які потрібно // обробити: кількість датчиків + // 6 - для I / O (3 - конф, 3 - засувки) while (i & lt; elements_col) {// перевіряємо, чи є на сторінці контейнер з потрібним id ? obj_tag = document.getElementById (ArrVal [2 * i + 1]); if (obj_tag) // якщо є {// міняємо текст всередині нього на прийнятий від сервера obj_tag.innerHTML = ArrVal [2 * i + 2]; } I = i + 1; // перевіряємо наступний елемент} status = 1; // міняємо статус}}}

Тепер потрібно переробити серверну частину, - перевірити, чи є в GET-запиті наші нові команди і написати обробники цих команд.

Відкриваємо в Builder-е наш проект і модифікуємо обробник події OnCommandGet. У секції, де ми переглядаємо по черзі всі распарсенние параметри GET-запиту, додаємо наступний код:

// обробники команд від дистанційних кнопок (з браузера) if (RequestInfo -> Params -> Names [i] == "io0sw") {if (RequestInfo -> Params -> Values ​​[ "io0sw"] == 1) IO0SwIn = true; else if (RequestInfo -> Params -> Values ​​[ "io0sw"] == 0) IO0SwOu = true; else break; } If (RequestInfo -> Params -> Names [i] == "io1sw") {if (RequestInfo -> Params -> Values ​​[ "io1sw"] == 1) IO1SwIn = true; else if (RequestInfo -> Params -> Values ​​[ "io1sw"] == 0) IO1SwOu = true; else break; } If (RequestInfo -> Params -> Names [i] == "io2sw") {if (RequestInfo -> Params -> Values ​​[ "io2sw"] == 1) IO2SwIn = true; else if (RequestInfo -> Params -> Values ​​[ "io2sw"] == 0) IO2SwOu = true; else break; } 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; }

// обробники команд від дистанційних кнопок (з браузера) if (RequestInfo-> Params-> Names [i] == "io0sw") {if (RequestInfo-> Params-> Values ​​[ "io0sw"] == 1) IO0SwIn = true; else if (RequestInfo-> Params-> Values ​​[ "io0sw"] == 0) IO0SwOu = true; else break; } If (RequestInfo-> Params-> Names [i] == "io1sw") {if (RequestInfo-> Params-> Values ​​[ "io1sw"] == 1) IO1SwIn = true; else if (RequestInfo-> Params-> Values ​​[ "io1sw"] == 0) IO1SwOu = true; else break; } If (RequestInfo-> Params-> Names [i] == "io2sw") {if (RequestInfo-> Params-> Values ​​[ "io2sw"] == 1) IO2SwIn = true; else if (RequestInfo-> Params-> Values ​​[ "io2sw"] == 0) IO2SwOu = true; else break; } 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-сторінка, що віддається http-сервером, виглядає так, як на картинці нижче, і найголовніше, - ми можемо перемикати I / O прямо з браузера (кнопками в таблиці управління).

Аналогічно можна прикрутити будь-яке інше управління (передавати на сервер з браузера інші команди, які будуть виконувати інші дії), сенс, я думаю, ви зрозуміли 😉

Отже, тепер у нас є і відображення і управління, проте виглядає наша сторінка досить убого. Таблиці не дозволяють візуально прив'язати відображаються дані до об'єкта управління. Щоб таку візуальну прив'язку здійснити, нам необхідно намалювати на web-сторінці об'єкт управління і на цьому малюнку показати розташування датчиків і перемикачів (так, як це роблять в справжніх Скад).

Ok, наступного разу ми саме цим і займемося, - придумаємо якийсь конкретний об'єкт управління, який буде управлятися нашим шлюзом, і зробимо для нього просунуту візуалізацію (тим більше сучасні web-технології дозволяють вирішити подібну задачу в браузері набагато ефективніше, ніж в традиційної Скаде).

  1. Частина 1. Протистояння неминуче, результат - передбачуваним.
  2. Частина 2. Простий віддалений моніторинг через web-браузер.
  3. Частина 3. Віддалене управління через web-браузер.
  4. Частина 4. Просунута візуалізація в web-браузері. АСУТП акваріума.
Тепер давайте подумаємо, що нам потрібно для вирішення нашої задачі?
Onreadystatechange = handleTop; // чіпляємо на onreadystatechange обробник відповіді var url = '?
Onreadystatechange = handleTop; // чіпляємо на onreadystatechange обробник відповіді var url = '?
Усе?

Дополнительная информация

rss
Карта