22 May

Простой Web-доступ к VI приложениям LabVIEW в PHP через ActiveX Server

PHPLabVIEWIndustrial ProgrammingIOT
В LabVIEW уже много лет существует возможность «прикрутить» Web к VI приборам без каких-либо сложных настроек публикации и серверов со стороны LabVIEW, используя только втроенный сервер ActiveX. Не является исключением и LabVIEW 2020 Community edition.

Для LabVIEW на данный момент момент существует несколько способов публикации виртуальных приборов в Web, требующих разного уровня знаний и предоставляющих разные возможности. В этой статье я не собираюсь их описывать, но хочу познакомить вас с нестандартным использованием встроенного в LabVIEW сервера ActiveX/COM для организации Web доступа к VI, а также управления самой средой LabVIEW. Хотя ActiveX/COM уже старая, но еще продолжающая жить в Windows технология, но именно через встроенный ActiveX сервер можно легко организовать управление LabVIEW и VI приборами, в том числе через Web.

Первое что нужно сделать, это включить в LabVIEW этот самый ActiveX сервер, делается это в настройках среды: Tools->Options->VI Server, флажок ActiveX.


Проверить, что сервер включен и к нему есть доступ, можно простым скриптом на VBScript. Нужно создать на рабочем столе текстовой файл labview_test.vbs и наполнить его следующим содержимым:

Dim obj
Set obj = CreateObject("LabVIEW.Application")
'Dim vi
'Set vi = obj.GetVIReference("C:\Users\Dell\Desktop\LabVIEW Web ActiveX\ActiveX Server Executable _LV2012_NI Verified\Executable as ActiveX Server\ActiveX Server.vi")
WScript.Echo(obj.AppName & " ver: " & obj.Version)
'WScript.Echo(vi.GetControlValue("Count"))
'Set vi = Nothing
Set obj = Nothing

Перед выполнением скрипта запустите среду LabVIEW. Впрочем, будет работать и без предварительного запуска среды. На время выполнения скрипта будет запущен экземпляр LabVIEW как ActiveX/COM сервер, а по завершении скрипта экземпляр будет закрыт, так что придется подождать, пока все это «загрузится и выгрузится». В выводе labview_test.vbs будет имя корневого приложения и его версия.


Далее я создал простой VI прибор «ActiveX Server.vi». В нем содержится несколько контролов и вспомогательных функций. Этот VI мы будем загружать и управлять им.


От LabVIEW нам больше ничего не потребуется. Теперь можно приступать к слоям Web.

Тернистый путь


Сначала я немного поэкспериментировал с штатным Windows Web-сервером Microsoft IIS. Пробовал создавать страницы ASP на VBScript приблизительно следующего содержания:

<% @language = "vbscript" %>
<html><body>
<p>ASP can output HTML tags as well as plain text</p>
<%
	Dim obj
	Set obj = CreateObject("LabVIEW.Application")
	response.write(obj.AppName & " ver: " & obj.Version & "<br>" & vbCr)
	Dim vi
	Set vi = obj.GetVIReference("C:\Users\Dell\Desktop\LabVIEW Web ActiveX\ActiveX Server Executable _LV2012_NI Verified\Executable as ActiveX Server\ActiveX Server.vi")
	response.write(vi.GetControlValue("Count") & vbCr)
	set vi = Nothing
	set obj = Nothing
%>
</body></html>

Метод GetVIReference() загружает VI в память и устанавливает с ним связь. Основной параметр: абсолютный путь к выбранному VI.

Вывод скрипта в браузер:

LabVIEW.exe ver: 20.0
123 

Правда, пришлось немного повозиться с настройками пула приложений IIS и анонимной проверки пользователей, которые я настроил на текущего пользователя Windows.

Я решил не углублятся в ASP и переключился на PHP. Для IIS настроил PHP FastCGI демон. Настройки не привожу, они не важны для основной части этой статьи. В PHP также удалось получить доступ к COM объекту LabVIEW, по типу:

$obj = new COM('LabVIEW.Application');

В обоих случаях при запущенной среде LabVIEW и открытом в ней VI (ActiveX Server.vi). При запросе PHP (ASP) скрипта параллельно запускался (и значительное время) новый экземпляр LabVIEW.exe, далее в нем c помощью метода GetVIReference() загружался свой экземпляр «ActiveX Server.vi». По завершении работы PHP скрипта экземпляр LabVIEW закрывался. Т.е. тут не было никакого пересечения с уже запущенным экземпляром среды LabVIEW. С помощью известной утилиты Process Explorer это хорошо наблюдается. «Игра» с настройками пула приложений IIS тоже не дала особого результата. Для себя я сделал вывод, что IIS работает как системный демон от имени system, и поэтому создается отдельный экземпляр LabVIEW.exe, привязанный к контексту system, и переиспользование уже открытого экземпляра от имени пользователя Windows мне не удастся получить.

Тогда возникла мысль попробовать сторонний web-сервер, запущенный в простом режиме приложения от имени текущего пользователя. Выбор пал на NGINX, притом я его уже использовал в качестве обратного proxy для LabVIEW WebServices.

NGINX


Берем доступную текущую версию nginx под Windows. На данный момент nginx-1.17.10. Для связи PHP с NGINX я использовал следующее описание.

Несложная минимальная настройка NGINX. У меня файл: c:\nginx-1.17.10\conf\nginx.conf
Добавление листинга корневого каталога в браузер:

nginx.conf:

location / {
	root   html;
	index  index.html index.htm;
	autoindex on;
}

Включение PHP через FastCGI в корневом каталоге сервера:

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
location ~ \.php$ {
	root           html;
	fastcgi_pass   127.0.0.1:9000;
	fastcgi_index  index.php;
	fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
	include        fastcgi_params;
}

PHP


Берем актуальную версию PHP для Windows. Я использовал php-7.4.5-nts-Win32-vc15-x64.zip, располагаться она у меня будет в c:\php-7.4.5-nts-Win32-vc15-x64

Переименовываем и настраиваем php.ini (который php.ini-development из архива). Вносим следующие изменения:

php.ini:

short_open_tag = On
html_errors = On
error_reporting = E_ALL & ~E_NOTICE
extension_dir = "ext"
extension=gd2
extension=php_com_dotnet.dll

Тут подключается библиотека GD для работы с изображениями (если нужно получать какие-то изображения из LabVIEW) и модуль php_com_dotnet.dll для работы с ActiveX/COM объектами в PHP.

В процессе работы с COM в PHP обнаружился неприятный баг при работе со строками (VT_BSTR), содержащими в теле 0x0 символы. Решается он заменой php_com_dotnet.dll на перекомпилированный с исправлением. Описание бага и патч с исправлением можно найти тут. К сожалению, он до сих пор официально не исправлен в PHP. Я пересобрал php_com_dotnet.dll (для php-7.4.5-nts-Win32-vc15-x64), исправленный php_com_dotnet.dll можно найти по ссылке. Руководство для самостоятельной сборки PHP и расширений можно найти тут.

По умолчанию NGINX будет запущен на 80 TCP порту, PHP FastCGI демон на порту 9000, проверьте, что нет других работающий приложений, использующих эти порты.

Запуск и остановку NGINX и PHP FastCGI демона можно организовать разными способами. У меня для нужд отладки оформились вот такие cmd скрипты: запускающий/перезапускающий в фоне (без открытых окон демонов) start-restart-all.cmd и останавливающий kill-all.cmd, которые я положил в каталог NGINX. Используется Run Hidden Console утилита, взятая из описания.

start-restart-all.cmd:

rem @echo off
set PHP_FCGI_MAX_REQUESTS=0
@echo Shutting down servers...
taskkill /f /IM nginx.exe
taskkill /f /IM php-cgi.exe
@timeout 1
@echo Starting servers...
@rem start /b /D "C:\php-7.4.5-nts-Win32-vc15-x64" php-cgi.exe -b 127.0.0.1:9000
RunHiddenConsole.exe "C:\php-7.4.5-nts-Win32-vc15-x64\php-cgi.exe" -b 127.0.0.1:9000
start /b /D "c:\nginx-1.17.10\" nginx.exe
@timeout 3

kill-all.cmd:

taskkill /f /IM nginx.exe
taskkill /f /IM php-cgi.exe
pause

Хочу обратить внимание на переменную окружения PHP_FCGI_MAX_REQUESTS. По умолчанию она равна 500. И через 500 запросов демон PHP FastCGI завершит свою работу, поэтому у себя для отладки я отключил этот счетчик. Вот цитата из документации для размышления:

This PHP behavior can be disabled by setting PHP_FCGI_MAX_REQUESTS to 0, but that can be a problem if the PHP application leaks resources. Alternatively, PHP_FCGI_MAX_REQUESTS can be set to a much higher value than the default to reduce the frequency of this problem.

Я написал 2 тестовых скрипта PHP labview.php, labview_png.php, которые необходимо разместить в корне web-сервера C:\nginx-1.17.10\html
labview.php — это основной скрипт примера
labview_png.php — возвращает изображение PNG из читаемой из LabVIEW ActiveX сервера строки типа VT_BSTR.

labview.php
<?php
if(strpos(exec('tasklist /FI "IMAGENAME eq LabVIEW.exe" /NH'), 'LabVIEW.exe') === false)
	exit("Не запущен LabVIEW.exe");?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<title>LabVIEW PHP COM example</title>
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
	<script>
		// setTimeout(function(){
		//	window.location.reload(1);
		// }, 3000);
		// setInterval(function() {
		//	var myImageElement = document.getElementById('myImage');
		// 	myImageElement.src = 'labview_png.php?rand=' + Math.random();
		//}, 200);
	 
		$(document).ready(function(){
			setInterval(function(){
				$("#png").attr('src', 'labview_png.php?rand=' + Math.random());
				$("#auto").load(location.href + " #auto");
			}, 1000);
		});
</script>
	
</head> 
<body>
<?php
//phpinfo();
echo '_GET val: ';
foreach ($_GET as $key => $value)
	echo "$key=$value, ";
echo '<br>', PHP_EOL;

echo '_POST val: ';
foreach ($_POST as $key => $value)
	echo "$key=$value, ";
echo '<br>', PHP_EOL;

define('FPStateInfo', ['Invalid', 'Standard', 'Closed', 'Hidden', 'Minimized', 'Maximized']);
define('ExecStateInfo', ['eBad 0 VI has errors; it cannot run', 'eIdle 1 VI is not running, but the VI is in memory.', 'eRunTopLevel 2 VI is running as a top-level VI', 'eRunning 3 VI is running as a subV']);

$obj = new COM('LabVIEW.Application');
//com_print_typeinfo($obj);

$vi = $obj->GetVIReference('C:\Users\Dell\Desktop\LabVIEW Web ActiveX\ActiveX Server Executable _LV2012_NI Verified\Executable as ActiveX Server\ActiveX Server.vi');

//$vi->OpenFrontPanel();

echo '<form action="" method="post">';
echo '<input type="button" value="Refresh page" onClick=\'window.location.href=window.location.href\'>', PHP_EOL;

$fpstate = $vi->FPState();
$vistate = $vi->ExecState();

if ($_POST['action']==='run_vi' && $vistate <= 1) {
	$vi->Run(true); // async Boolean If TRUE, you do not need to wait for the VI to finish running. The default is FALSE.
} elseif ($_POST['action']==='stop_vi' && $vistate > 1) {
	//$vi->SetControlValue('stop', true);
	//sleep(1);
	$vi->Abort();
} elseif ($_POST['action']==='open_fp' && $fpstate==2) {
	$vi->OpenFrontPanel();
} elseif ($_POST['action']==='close_fp' && $fpstate!=2) {
	$vi->CloseFrontPanel();
}

if ($_POST['Count2']) {
	$vi->SetControlValue('Count2', $_POST['Count2']);
}

echo '<h3>SetControlValue(\'Count2\'):</h3>', PHP_EOL;
echo '<input onchange="this.form.submit()" type="number" name="Count2" value="', $vi->GetControlValue('Count2'), '">', PHP_EOL;

echo '<div id="auto">';

echo '<h3>AppName / Version:</h3>', PHP_EOL;
echo $obj->AppName(), ' / ', $obj->Version(), '<br>', PHP_EOL;

echo '<h3>ExportedVIs:</h3>', PHP_EOL;
foreach ($obj->ExportedVIs() as $value)
	echo $value, '<br>', PHP_EOL;

echo '<h3>FPState:</h3>', PHP_EOL;
$fpstate = $vi->FPState();
echo $fpstate, ', ', FPStateInfo[$fpstate], PHP_EOL;

echo '<button name="action" type="submit" value="open_fp">OpenFrontPanel</button>', PHP_EOL;
echo '<button name="action" type="submit" value="close_fp">CloseFrontPanel</button>', PHP_EOL;

echo '<h3>ExecState:</h3>', PHP_EOL;
$vistate = $vi->ExecState();

if ($vistate > 1) {
	echo '<font color="blue">', $vistate, ', ', ExecStateInfo[$vistate], '</font>', PHP_EOL;
} else {
	echo $vistate, ', ', ExecStateInfo[$vistate], PHP_EOL;
}

echo '<button name="action" type="submit" value="run_vi">Run VI</button>', PHP_EOL;
echo '<button name="action" type="submit" value="stop_vi">Abort VI</button>', PHP_EOL;
echo '</form>', PHP_EOL;

echo '<h3>GetControlValue(\'Count\') / GetControlValue(\'Count2\'):</h3>', PHP_EOL;
echo $vi->GetControlValue('Count'), ' / ', $vi->GetControlValue('Count2'), PHP_EOL;
//echo $vi->SetControlValue('Count2', $vi->GetControlValue('Count')+1), PHP_EOL;

echo '<h3>Array1:</h3>', PHP_EOL;
foreach ($vi->GetControlValue('Array1') as $value)
	echo $value, '<br>', PHP_EOL;

//$png_data = new variant(null, VT_UI1);
//$png_data = variant_set_type($vi->GetControlValue('png data'), VT_UI1);
//echo variant_cast($vi->GetControlValue('png1'), VT_BSTR), PHP_EOL;
//echo mb_strlen($vi->GetControlValue('String1')), PHP_EOL;
//echo variant_get_type($vi->GetControlValue('png1')), PHP_EOL;

echo '<h3>PNG data:</h3>', PHP_EOL;
$png_data = $vi->GetControlValue('PNG data');
echo 'PNG size:' , strlen($png_data), '<br>', PHP_EOL;


echo '</div>';

if ($vistate > 1 && $fpstate!=2) {
	echo '<img src="labview_png.php" id="png">';
}

// variant_set_type($variant, VT_BSTR)
//$png_data = variant_cast($vi->GetControlValue('png data'), VT_U1);


//echo  variant_get_type($png_data), PHP_EOL;
echo $vi->SetControlValue('String1', "123\x00555321");
//com_print_typeinfo($vi);
$obj = null;
?>
</body>
</html>


labview_png.php
<?php
if(strpos(exec('tasklist /FI "IMAGENAME eq LabVIEW.exe" /NH'), 'LabVIEW.exe') === false)
	exit("Не запущен LabVIEW.exe");
$obj = new COM('LabVIEW.Application');
$vi = $obj->GetVIReference('C:\Users\Dell\Desktop\LabVIEW Web ActiveX\ActiveX Server Executable _LV2012_NI Verified\Executable as ActiveX Server\ActiveX Server.vi');

$data = $vi->GetControlValue('PNG data');

$im = imagecreatefromstring($data);
if ($im !== false) {
    header('Content-Type: image/png');
    imagepng($im);
    imagedestroy($im);
}
else {
    echo 'Произошла ошибка.';
}
$obj = null;
?>


Выполнять скрипты лучше при запущенной среде LabVIEW, в этом случае скрипты будут переиспользовать уже открытый экземпляр LabVIEW. А не создаваться и закрывать COM экземпляр при каждом вызове скрипта. В моем скрипте используется немного AJAX и «перезапуск», а не переиспользование LabVIEW выльется в «черепаший марафон» последовательный запусков и завершений labview.exe.

Видеообзор:


Appendix. Конфигурация NGINX в качестве обратного proxy с HTTP Basic access authentication для работы с WebServices LabVIEW


Некоторое время назад я немного экспериментировал с WebServices LabVIEW (по правде сказать на довольно старой версии LabVIEW). Тогда обнаружил, что у страниц (ресурсов WebServices) нет никакого простого разграничения доступа. Предлагалось настраивать пользователей в Application Server и использовать «мертвый» Microsoft Silverlight. А мне нужен был какой-нибудь простой вариант, типа проверки пароля HTTP Basic access authentication.

Я воспользовался NGINX и настроил его в качестве обратного web proxy c включенной проверкой auth_basic. Используя приведенные ниже настройки, при обращении на адрес http://server_name:5500 после ввода пароля пользователь получает доступ к WebService приложению, работающему по адресу http://127.0.0.1:8001/webservice1/.
Защищаются все ресурсы приложения webservice1.

nginx.conf:

server {
    listen       5500;
    server_name  localhost;
    location / {
        auth_basic "Unauthorized";
        auth_basic_user_file htpasswd;
        root html;
        #autoindex on;
        #index index.html index.htm;
        proxy_pass http://127.0.0.1:8001/webservice1/;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

и файл htpasswd с паролями пользователей:

admin:{PLAIN}1

Развивая эту мысль дальше, можно включить доступ к proxy NGINX по HTTPS, а от NGINX к LabVIEW оставить HTTP.
Tags:LabVIEWPHPActiveXNGINX
Hubs: PHP LabVIEW Industrial Programming IOT
+12
685 14
Comments 1
Popular right now