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

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

Для того, чтобы страничка выглядела более читабельно, добавим в секцию 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>

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

<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 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;	// меняем статус
		}
	}
}

Теперь нужно переделать серверную часть, — проверить, есть ли в 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;
}

Ну, что? Всё? Тогда компилируем и запускаем (проект со всеми внесёнными изменениями).

Вуаля, — теперь web-страничка, отдаваемая http-сервером, выглядит так, как на картинке ниже, и самое главное, — мы можем переключать I/O прямо из браузера (кнопками в таблице управления).

внешний вид web-страницы после внесённых изменений

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

Итак, теперь у нас есть и отображение и управление, тем не менее выглядит наша страничка довольно убого. Таблицы не позволяют визуально привязать отображаемые данные к объекту управления. Чтобы такую визуальную привязку осуществить, нам необходимо нарисовать на web-страничке объект управления и на этом рисунке показать расположение датчиков и переключателей (так, как это делают в настоящих скадах).

Ok, в следующий раз мы именно этим и займёмся, — придумаем какой-нибудь конкретный объект управления, который будет управляться нашим шлюзом, и сделаем для него продвинутую визуализацию (тем более современные web-технологии позволяют решить подобную задачу в браузере гораздо эффективнее, чем в традиционной скаде).

  1. Часть 1. Противостояние неизбежно, результат — предсказуем.
  2. Часть 2. Простой удалённый мониторинг через web-браузер.
  3. Часть 3. Удалённое управление через web-браузер.
  4. Часть 4. Продвинутая визуализация в web-браузере. АСУТП аквариума.

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