Простой 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 я использовал следующее описание.

    Несложная минимальная настройка 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.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 1

      +1
      Спасибо!

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое