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

от автора

В 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 я использовал следующее описание:
www.nginx.com/resources/wiki/start/topics/examples/phpfastcgionwindows

Несложная минимальная настройка 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.

ссылка на оригинал статьи https://habr.com/ru/post/502496/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *