Привет, Хабр!

Меня зовут Валерий, и я проработал инженером на заводе шесть лет. Звучит как начало занятий по групповой психотерапии для выгоревших сотрудников, но о моём выгорании и причинах этого я расскажу как-нибудь в другой раз. В данной статье хочу поделиться историей о том, как я воплощал свой Pet-проект в рамках промышленного предприятия и что из этого вышло. Впереди много картинок, так что добро пожаловать под кат!

Добро пожаловать в наш «дружный коллектив»


Моя история начинается в 2016 году, когда мой коллега с соседнего предприятия предложил мне присоединиться к их дружной корпоративной семье в качестве инженера АСУ ТП. Недолго думая, я согласился, ожидая радужных перспектив и карьерного роста на новом предприятии. И мне кажется, что я сильно ошибался на тот момент.

На всё есть причина


Нельзя просто так взять и построить систему сбора данных: на всё должна быть причина. В период онбординга я наблюдал за процессами в нашем подразделении и процессами взаимодействия между подразделениями и менеджментом. Так как наше подразделение было связано непосредственно с техническим обслуживанием систем АСУ ТП и КИП и А, то также меня интересовал вопрос взаимодействия для организации ремонтных работ. В процессе передачи заявок на устранение неисправности или устранения сбоев систем АСУ ТП и КИП и А, очень часто не хватало данных от самой системы АСУ ТП (в виде логов параметров) для анализа ситуации и устранения неисправности.

Зачастую мы оперировали только описанием неисправности со слов технологического персонала, не имея возможности подтвердить или опровергнуть описанную неисправность данными со стороны системы автоматизации. Данная проблема усугублялась используемым программным обеспечением SCADA-системы, которая имела ряд особенностей, из-за которых в случае перезагрузки компьютера АРМ оператора мы теряли лог с данными.
Через три месяца после моего устройства в компанию увольняется ведущий инженер АСУ ТП, а меня назначают на его должность. С этого момента вопрос диагностических данных становится более острым, и я решил попытаться как-то решить данную проблему.

Анализ ситуации


Мой поверхностный анализ проблемы в плане получения данных от систем АСУ ТП выявил следующие проблемы:
  1. АРМ операторов не имеют подключения к локальной сети и не объедены в единую ЛВС;
  2. Отсутствие возможности удаленного подключения к компьютерам АРМ операторов;
  3. Отсутствие удаленной системы хранения технологических/технических данных.

Выше описанные проблемы в значительной степени снижают эффективность работы инженера в процессе поиска и устранения неисправности, так как уже на этапе получения диагностических данных мы имеем большие временные затраты, связанные с выездом на объект и других операций для получения диагностической информации на объекте.

GO/NO-GO


«С самого начала у меня была какая-то тактика. И я её придерживался»

Для начала нам хотелось убедиться, что некоторые вещи сработают. Первое, что нужно было сделать, — это подключить пару «подопытных» APM (автоматизированных рабочих мест) к локальной сети предприятия. Благо в операторских помещения имелись точки присутствия сети. Проконсультировавшись с отделом IT, я узнал, что для нужд АСУ ТП выделен отдельный VLAN, который мы можем использовать для своих целей, и любезно предоставили нам информацию о номерах портов сетевого оборудования и пула IP адресов. Мне всегда нравилось работать с отделом IT: мы всегда помогали друг другу при возникновении проблем.

Итак, для подключения компьютеров к сети всё готово! Для подключения мы выбрали один из «проблемных» объектов: цех обжига клинкера. Подключив компьютеры к сети, первым делом мы установили VNC сервер для возможности удаленного подключения (естественно, в пределах нашего VLAN). Ну и при первой же проблеме мы кайфанули от возможности удаленного подключения (как бы это странно не звучало).

Также мы решили протестировать стабильность передачи данных со SCADA системы, АРМ операторов на удаленный сервер. И «подручных» комплектующих был собран тестовый системный блок, который выполнял роль сервера. В процессе сборки кто-то заботливо дал ему имя «Доби», написав его маркером на приводе оптических дисков, а я, в свою очередь, дал ему фамилию. В итоге у нас получился системный блок с именем Доби CYBEREX.
Шутки шутками, но в итоге у нас был собран тестовый системный блок, куда я установил серверную операционную систему FreeBSD и развернул сервер баз данных MySQL, с помощью которого и планируем сохранять данные.

Чтобы не запутаться в том, что мы сделали, мы выбрали следующую структуру базы данных: на нашем объекте есть две технологические линии обжига — 205 и 206, соответственно, мы создаем две базы данных с названием данных объектов kiln205 и kiln206. Объект генерирует следующие типы данных: технологические, технические, и также эти данные разделяются по технологическим узлам линии. Соответственно, мы будем разделять эти данные по отдельным таблицам. По ходу статьи вы поймете, о чем шла речь.

Далее предстояла задача организации связи с нашим тестовым сервером из SCADA системы. В качестве программного обеспечения системы автоматизации, на тот момент, использовалось решение TraceMode 6 от компании «Ад Астра» — по первому слову от названия компании всё становится ясно :) Задача сводится к тому, что в проекте монитора реального времени, необходимо выполнить настройку подключения через Connector/ODBC, далее необходимо создать шаблоны связи с СУБД для конкретных таблиц и привязать необходимые аргументы передаваемых параметров. И не забыть настроить таймеры вызова данных шаблонов, в нашем случае периодичность вызова составляет одну секунду. Задача не сложная, но требует большого терпения, так как прописать более двухсот аргументов в шаблонах вызова — ещё та забава. В данной задаче мне очень сильно помог мой коллега по имени Руслан, за что я ему очень благодарен. Но прежде чем испытать наши задумки в реальных условиях производства, предварительно необходимо проверить наши изменения в симуляции на копии проекта реального объекта, что и было успешно сделано. Теперь у нас есть рабочая (тестовая) система хранения данных.

Ядро системы, разработка программного обеспечения


Одно дело — это хранить данные, но нам необходимо еще их как-то выводить для пользователя в удобном формате, с функциями анализа и другими «плюшками». Другими словами, необходимо разработать приложение для вывода, анализа и других манипуляций с данными. Тут на помощь мне пришел мой, на половину заброшенный Pet-проект, который также предназначался для сбора данных, но для метеостанций. «Чем завод хуже метеостанции?» — подумал я и решил продолжить разработку проекта, но с учетом особенностей объектов завода.

Мой проект базировался на следующем стеке технологий:
  • СУБД: MySQL;
  • Веб сервер: Apache HTTP Server;
  • Язык программирования: PHP.

Как можно видеть из стека, проект представляет собой web-приложение. На начальном этапе, когда набор отображаемых параметров был не велик и пользователю не предлагалось каких-либо персонализированных манипуляций с данными, а было просто доступно отображение на странице списка параметров с функцией вывода графика, но в дальнейшем «назрела» задача реализации функции персонализированных настроек системы. Для решения данной задачи были реализованы методы регистрации, авторизации и аутентификации пользователей.

«Хьюстон, у нас проблемы»


Как часто бывает, при реализации новых проектов возникают непредвиденные проблемы, которые связаны со скрытыми особенностями используемых систем. На этот раз возникла проблема с удаленным подключением к базе данных в SCADA TraceMode: при потере связи с удаленной базой данных возник баг с отображением параметров в мониторе реального времени, и устранить проблему можно было только при перезапуске МРВ. Эта проблема оказалась очередной особенностью используемой SCADA-системы и была связана с отсутствием многопоточности. Поэтому было принято решение изменить подход к сбору данных, теперь все данные записывались в локальную базу, а удаленный опрос данной базы инициализировался со стороны удаленного сервера нашей системы сбора данных. Данное решение имело дополнительный плюс: мы всегда могли дочитать данные, если по каким-то причинам предыдущие сессии обмена не состоялись.

Ниже приведена часть кода модуля опроса объектов:
PHP код части модуля опроса АРМ
<?php
//Скрипт сбора данных с баз АРМ операторов
//Автор творения: CYBEREX TECH
ini_set('mysql.connect_timeout','2');   
ini_set('max_execution_time', '5'); 
$hostI = str_replace(' ', '',$_POST["host"]);   // Получаем хост удаленной БД
$baseI = str_replace(' ', '',$_POST["base"]); // Получаем имя БД
$tabI = str_replace(' ', '',$_POST["tab"]);      // Получаем имя таблицы объекта
$start = microtime(true);                               // Получаем время старта скрипта
start();

      function get_column($base, $data_page){	        // Функция получения имен столбцов
	            include ("bd.php");
				if($base == "newpc"){                   //Исправляем косяки Руслана
				           $base = "pomol_dozatory";
						   $data_page = "newPC";
				         }
                $result=mysqli_query($connect,"SELECT * FROM usersensor WHERE data_page='$data_page' AND base ='$base' ORDER BY id ASC ;");// делаем выборку из таблицы
               while($row=mysqli_fetch_array($result)){                             // берем результаты из каждой строки в цикле и создаем массив данных
                   
                            $VARABLE[] = str_replace(' ', '',$row['VARABLE']);       
                   }
         mysqli_close($connect); 
	     return $VARABLE;
            }

     function write_data($database, $event, $time, $tabl, $data_event){		// функция записи в базу 
                    $sdd_db_host='127.0.0.1';
                    $sdd_db_user='base2';                                                            // логин доступ к базе данных
                    $sdd_db_pass='пароль';                                                         // пароль доступа к базе данных
				    $columns = implode(", ",$event);                                                 //преобразовываем массив в строку для MySQL запроса (столбцы)
				    $values  = implode(", ", $data_event);                                           //преобразовываем массив в строку для MySQL запроса (значения)
                    $connect = mysqli_connect($sdd_db_host, $sdd_db_user, $sdd_db_pass, $database ); 
				 global $start;
                 if ($connect){
	                $result4 = mysqli_query($connect, "SELECT * FROM {$tabl}  ORDER BY id DESC LIMIT 1"); 
            	    $myrow4=mysqli_fetch_array($result4);
             	if($myrow4['date_time'] !== $time ){  
				    $text_2 ="даты нет ";
	                $result2 = mysqli_query($connect,"INSERT INTO {$tabl} (date_time, {$columns}) VALUES ('$time',{$values})"); //заносим в базу данные, если аналогичная дата отсутствует

		         }else{
			      
					 $text_2 ="дата есть ";
		              }
				   echo $myrow4['date_time']."ок11 ".$database." ".$time." ".$text_2;
				   }else{
					 echo mysqli_errno($connect) . ": " . mysqli_error($connect) . "<br>"; 
					   
				   }
					if((microtime(true) - $start) < 58){
						 mysqli_close($connect); 
						echo 'Время выполнения скрипта: ' . (microtime(true) - $start) . ' sec<br>';
						sleep(2);
					    start();	
					      }else{
					     mysqli_close($connect); 
						echo 'Общее Время выполнения скрипта: ' . (microtime(true) - $start) . ' sec<br>';
					     }		
                 }
				 
    function read_data($sdd_db_name, $sdd_db_host, $tabl, $VAR){		// функция чтения данных с удаленной базы
	              
	            if($sdd_db_name == "pomol_dozatory"){ //Исправляем косяки Руслана
				   echo $sdd_db_name;
				   $sdd_db_user='user';      // логин доступ к базе данных
                   $sdd_db_pass='пароль1';      // пароль доступа к базе данных
				}else{
                   $sdd_db_user='base';      // логин доступ к базе данных
                   $sdd_db_pass='пароль2';  // пароль доступа к базе данных
				}
                   $connect = mysqli_connect($sdd_db_host, $sdd_db_user, $sdd_db_pass, $sdd_db_name ); //php7
                   mysqli_query($connect, "SET NAMES 'utf8' ") or die("Ошибка подключения ".mysqli_connect_error());
	               $result4 = mysqli_query($connect, "SELECT * FROM {$tabl} ORDER BY id DESC LIMIT 1");
            	   $myrow4=mysqli_fetch_array($result4);
				  
                   $tm = $myrow4['date_time'];
				    
             	if(!empty($myrow4['id']) and (time() - strtotime($tm)) < 1000 ){
				   $time1 = $tm;
				   for($i=0; $i < count($VAR); $i++){
				         $data_event[] = $myrow4[$VAR[$i]];
		              	}		
						  write_data($sdd_db_name, $VAR, $time1, $tabl, $data_event);                    // Отсылаем все в запись (хост, массив столбцов, время данных, значения столбцов)
						  echo 'Текущее время: ' . time().  ' Время на сервере: '.strtotime($time1);
		               }elseif(!empty($myrow4['id']) and (time() - strtotime($tm)) > 1000 ){
					    
					      $result44 = mysqli_query($connect, "TRUNCATE TABLE {$tabl}");                 // Очищаем удаленную базу, если последняя запись имеет большую разницу по времени с текущим временем (признак поломки удаленной базы)
						  echo 'Чистим базу';
		               }
					   
				   mysqli_close($connect); 
                 }

      
	function start(){	

                 global $baseI, $tabI, $hostI;	
			     $res = get_column($baseI, $tabI);       // берем из базы названия столбцов базы
			     read_data($baseI,$hostI,$tabI,$res); 		
	    } 
?>


Для циклического опроса удаленных баз данных, реализован запуск скрипта модуля с помощью крона, периодичность запуска одна минута.

Веб интерфейс или как улучшить восприятие данных


Как вы уже могли догадаться, системные модули и интерфейс были написаны на чистом PHP, без применения каких-либо фреймворков. Функция регистрации пользователей была реализована с использованием метода подтверждения учетной записи с помощью электронной почты, поэтому для этих целей коллеги из отдела IT предоставили доступ к корпоративному почтовому серверу для возможности отправки писем. Само собой, доступ к веб-интерфейсу системы был реализован исключительно в рамках сети предприятия, а подтверждение email-адреса пользователя требовалось исключительно для проверки правильности ввода, так как адрес электронной почты должен был в дальнейшем использоваться для восстановления доступа и системных сообщений.
На первоначальном этапе аутентификация пользователя выполняется с помощью логина и пароля, а дальнейшая авторизация пользователя в системе выполнялась с помощью сгенерированного токена.

PHP функция генерации токена
<?php
  
function generateToken() {                                                          //Функция генерация токена
    $alphanum   = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; // Набор символов
    $w_key      = 15;                                                               // Длина ключа   
    $tokenValue = '';                                                               
    
    for ($i = 0; $i < $w_key; $i++) {
        $tokenValue .= $alphanum[random_int(0, strlen($alphanum) - 1)];
    }

    return $tokenValue;
}

echo generateToken();

?>


В дальнейшем токен сохраняется на устройстве пользователя, а из системы он удаляется, предварительно сохраняя его хеш-сумму в базе авторизации. Ниже приведены скриншоты интерфейса системы.

Страница входа:


Страница выбора источника данных:


Страница параметров объекта «Обжиг»:


Как вы можете видеть на скриншоте, данные разделены на подгруппы и выбираются с помощью нижнего массива навигационных кнопок, данное решение улучшает восприятие и не «захламляет» интерфейс. А данные, которые отображены в таблице, обновляются в реальном времени.

Страница отображения параметров главного привода:


Страница экспорта архивных данных в Excel файл:


Как вы понимаете, неотъемлемая часть бизнес-процессов — это составление отчетов. Данная функция, что отображена на скриншоте, в десятки раз сокращает время данного процесса. И большим плюсом является то, что данные получены напрямую от системы и не искажены, в отличие от «ручного» получения данных, когда данным процессом занимается технологический персонал, где часто возникает «человеческий фактор» ошибок.

Страница отображения моточасов (моторесурс) редукторов Flender:


Интересной задачей была реализация подсчета моточасов для редукторов Flender, которые используются на мощном технологическом оборудовании (печи обжига, сырьевые и цементные мельницы). Для определения часов использования оборудования используется косвенный признак: ток главного привода. Ниже код реализации модуля подсчета.

PHP код модуля подсчета моточасов оборудования
<?php 
//Модуль подсчета моточасов  для крона
//Косвенный признак ток главного привода
//Автор CYBEREX TECH - 2018
set_time_limit(0);


// массив данных доступа

	$bases_arryy = array( 
                   
					  array("Дробилка СМ 203 - Flender ГП", "drobilka" , "db_203" , "i_gp"),
					  array("Помол ЦМ 201 - Flender ГП", "pomol" , "red_capf_dvig", "i_gp"),
					  array("Помол ЦМ 202 - Flender ГП", "pomol" , "tech_202", "i_gp"),
					  array("Помол ЦМ 203 - Flender ГП", "pomol" , "tech_203", "i_gp"),
					  array("Обжиг ВП 205 - Flender ГП", "kiln_205" , "tok_volt", "i_gp_new"),
					  array("Обжиг ВП 206 - Flender ГП", "kiln_206" , "tok_volt", "i_gp_new")
             	  					  
                ); 

 function write_time($database, $event, $time, $time_work){		// функция записи в базу моточасов по объекту
                 if($time_work == 0){
					 $time_work ="0.0";
				 }
				 
                  include '/usr/local/www/apache24/data/web/datalog/dbconnect1.php';
               
	            $result4 = mysqli_query($connect, "SELECT * FROM flenders_time  WHERE date_time='$time' ORDER BY id DESC LIMIT 1"); 
            	$myrow4  = mysqli_fetch_array($result4);
             	if(empty($myrow4['id'])){  
				    $text_2  = "нет ID";
	                $result2 = mysqli_query($connect,"INSERT INTO flenders_time (date_time, {$event}) VALUES ('$time','$time_work')");  // Если записи нет, то создаем новую
		         }else{
			         $result2 = mysqli_query ($connect,"UPDATE flenders_time SET {$event}='$time_work' WHERE date_time='$time'");       // Если есть, то обновляем
					 $text_2  = "есть ID";
		           }
                mysqli_close($connect);  
			    return "ок ".$database." ".$event." ".$time." ".$time_work." ".$text_2 ;
              }

function return_time($seconds){
	
	$minutes = floor($seconds / 60);      // Считаем минуты
	$hours   = floor($minutes / 60);      // Считаем количество полных часов
	$minutes = $minutes - ($hours * 60);  // Считаем количество оставшихся минут

	return $hours.':'.$minutes;           // Возвращаем время в формате 0:00
	
}

 function days_in_month($month, $year){  // Функция для определения количества дней в месяце
     return $month == 2 ? ($year % 4 ? 28 : ($year % 100 ? 29 : ($year % 400 ? 28 : 29))) : (($month - 1) % 7 % 2 ? 30 : 31);
             }

 function time_moto($database, $tabl, $dates, $first_time, $last_time){
	        $time_moto = 0;
	        $time_temp = 0;
			$count = 0;
            include '/usr/local/www/apache24/data/web/datalog/dbconnect1.php';
            $result=mysqli_query($connect, "SELECT * FROM {$tabl}  WHERE  date_time >='$first_time' AND date_time <='$last_time' ORDER BY `id` DESC;"); // делаем выборку из таблицы
            while($row=mysqli_fetch_array($result)){
 			  if(!empty($row[$dates]) && is_numeric ($row[$dates])){
                 
				    if($row[$dates] > 50){
						if($count == 0){
							$time_temp = strtotime($row['date_time']); // записываем первую переменную времени при значении выше
							$count++;
						}else{
							$time_moto += $time_temp - strtotime($row['date_time']);
							$time_temp  = strtotime($row['date_time']); // Преобразуем время в UNIX формат
							$count++;
						}
						
					 } else{
						    $time_temp = strtotime($row['date_time']);	// Преобразуем время в UNIX формат
					     }
				      }
			       }
             mysqli_close($connect);  
			 return $time_moto; 
          }
	
	  $dateQ  = date("d");
	  $dateYQ = date("Y");
	  $datemQ = date("m"); 

for ($i3 = 0; $i3 < count($bases_arryy); $i3++){
     
     $event_file_name   = $bases_arryy[$i3][1]."_".date("Y_m_d_H_i_s").".csv";
	 $event_file_folder = "/usr/local/www/apache24/data/web/datalog/appl_files/";
     $text_to_file      = $bases_arryy[$i3][0]."; \n";
	 
	 $text_to_file .= "Период;";
	 $text_to_file .= "Моточасы ЧЧ:ММ; \n";
	 
	 $text_to_file = iconv('UTF-8', 'cp1251', $text_to_file);

	 $count_time = 0;
	 $tmp_times  = 0;
	 
     for($i=$dateQ; $i < $dateQ+1; $i++){
	
			$M = $datemQ; 
		
	
	   $f_t = $dateYQ."-".$M."-".$i;
	   if($i == $dateQ){ 
	       $tt = 1;
		   if($M == 12){
	          $M = 1;
		   }else{
	          $M = $M+1;
		    }
	   }else{
		  $tt = $i+1;  
	   }
	   if($M ==1){
		   $dateQQ = $dateYQ +1;
		   $l_t    = $dateQQ."-".$M."-".$tt;  
	   }else{
		   $l_t    = $dateYQ."-".$M."-".$tt; 
	   }
	   

	 $tmp_times = time_moto($bases_arryy[$i3][1],$bases_arryy[$i3][2],$bases_arryy[$i3][3], $f_t, $l_t); 
	 $count_time += $tmp_times;
	 if($i == days_in_month($datemQ, $dateYQ)){ 
	
	    $text_to_file2  = "с ".$f_t." по ".$l_t.";";
	    $text_to_file2 .= return_time($tmp_times)."; \n";
	    $text_to_file2 .= "ИТОГО: ;";
	    $text_to_file2 .= return_time($count_time)."; \n";
	    echo $text_to_file2;
	    $text_to_file2  = iconv('UTF-8', 'cp1251', $text_to_file2);
	
	}else{
		
	    $text_to_file2  = "с ".$f_t." по ".$l_t.";";
	    $text_to_file2 .= return_time($tmp_times)."; \n";
	    echo $text_to_file2;
	    $text_to_file2  = iconv('UTF-8', 'cp1251', $text_to_file2);	
	
	}
	echo write_time("until_time_unit", $bases_arryy[$i3][1]."_".$bases_arryy[$i3][2], $f_t, $tmp_times);
	//file_put_contents($event_file_folder.$event_file_name, $text_to_file2, FILE_APPEND); // Раскомментировать если нужен csv файл для отладки
     
	    }
 echo "<a href=\"http:\/\/".$_SERVER['HTTP_HOST']."/web/datalog/appl_files/".$event_file_name."\"> Скачать отчет. </a>";
				  }

?>


Данный модуль вызывается с помощью крона с периодичностью в один час.

График отображения моточасов:


Страница отображения уровней шлам бассейнов:


График уровня шлам бассейна:


Вот мы и подошли к примеру того, где реализована одна из функций персональной настройки системы, ради которых и задумывалась авторизация пользователей. Ниже пример установки границ отслеживания параметров, которая позволяет визуально отследить на графике выход значений за пределы нормального режима. Также в системе реализована функция слежения за динамикой изменения параметра, но об этом позже.

Пример установки границ параметра для отслеживания:


Удачное сохранение границ параметра:


Так выглядит график после установки границ отслеживания параметра:


Пример конфигурации отображения параметра:


Как можно видеть на скриншоте, панель администратора не отягощена дизайнерскими решениями ;), но тем не менее, хоть я и не дизайнер, но старался делать красиво.

Хочется чего-то большего...


Прошло какое-то время, система обрела более-менее законченный вид. Система популярна, ею пользуются, начиная от технологического персонала и заканчивая генеральным директором. Несмотря на то что система задумывалась как «локальный» инструмент для инженеров АСУ ТП и наладчиков КИП и А, она обрела широкую популярность среди сотрудников компании и даже использовалась в аудите эффективности использования технологического оборудования. Но хотелось чего-то большего.

Мобильное приложение.
Да, данная мысль меня часто посещала, но меня сдерживал вопрос организации защищенного канала для доступа к API. Но идея меня не оставляла в покое, и я написал предварительную версию мобильного приложения для теста в условиях локальной сети. Результат мне понравился, и не только мне — теперь появился запрос и на мобильное приложение, так как оно позволяло расширить количество персонала, которое могло бы иметь доступ к технологическим данным, так как не у всех есть доступ к корпоративному ПК. В итоге удалось решить вопрос с выделенным каналом связи для нужд мобильного приложения с белым IP и другими «плюшками». Для обработки запросов мобильного приложения был выделен отдельный сервер, который был изолирован от корпоративной сети. На сервере был реализован API для работы приложения, бэкенд я реализовал на PHP, ну и для организации защищенного соединения были получены TLS-сертификаты. Ниже вы можете видеть скриншоты мобильного приложения.

Авторизация в приложении:


Скриншоты главного экрана и списка параметров:


Выше на скриншоте вы можете видеть главный список объектов и список параметров объекта, значения параметров, также как и в веб версии, они отображаются в реальном времени.

Выбор объекта и просмотр параметра:


На скриншоте вы можете видеть список данных объекта, график объекта (отображающий тренд параметра), всплывающую панель с отображением параметра в реальном времени и форму ввода «Триггеры событий». Еще, наверное, вы заметили переключатель с надписью «Трансляция данных в AR устройство», но об этой функции я расскажу позже. Также как и в веб-версии, в приложении предусмотрен ввод границ параметра для отслеживания и, в дополнение к этому, значения для отслеживания динамики параметра (реализована для расчета времени наступления аварийного события). Все события по отслеживанию динамики параметра персонализировано сохраняются в базе данных на сервере и могут быть просмотрены в «Журнале триггеров» в мобильном приложении. Ниже вы можете видеть пример работы данной функции.

Журнал событий в мобильном приложении:


В мобильном приложении реализовал встроенную систему обновления, без использования сторонних Play-маркетов. Таким образом, была решена проблема с поддержкой приложения.

Для тех, кто может воспользоваться YouTube, ниже размещено видео демонстрации работы мобильного приложения.

На последних минутах видео, вы можете наблюдать работу функции сортировки параметров в списке. Данная функция задумывалась для удобства персонала, чтобы иметь возможность сформировать персональный список параметров в зависимости от приоритета использования. Данные персональной сортировки списка сохраняются в базе данных на сервере, и сохраненный порядок списка параметров также отображается и в веб-приложении.

И по просьбе некоторых коллег я написал мобильное приложение для владельцев iPhone, ниже вы можете видеть фото из архива.

Мобильное приложение для iOS:


Нам нужно больше данных!


Через некоторое время, после того как все технологические объекты, имеющие SCADA-систему, были подключены к системе сбора данных, начали часто поступать запросы о возможности подключения к системе объектов, которые не имеют системы автоматизации. К таким объектам относились Гидрофол и Шлам-бассейны. Данные объекты даже не имели ближайших точек подключения к сети предприятия, поэтому пришлось подумать об организации сетевой инфраструктуры и о вариантах получения данных. Что касательно сети, у меня имеется достаточно опыта по организации линий связи с применением оптоволокна, поэтому здесь не возникло проблем. Был смонтирован оптоволоконный кабель до объектов, установлено сетевое оборудование. Теперь стоял вопрос, каким образом мы можем получить данные с узлов.

Что касается объекта Гидрофол, там был установлен регистратор данных, который собирал измерения со всех необходимых датчиков. Регистратор умеет отдавать данные по протоколу Modbus TCP/IP, чем мы непременно воспользуемся, а в качестве устройства, которое будет передавать данные на сервер, был выбран дешевый промышленный контроллер от компании SIEMENS Simatic S7-1200, CPU 1214C. Данный контроллер будет опрашивать регистратор и отправлять полученные данные на сервер сбора данных. Ниже фото тыловой (интерфейсной) части регистратора.

Регистратор данных Метран:


В интегрированной среде разработки TIA Portal был создан простейший проект, где был написан «кастомный» функциональный блок. Этот блок включал в себя функцию опроса регистратора по протоколу Modbus TCP, функцию формирования JSON запроса с полученными данными и функцию отправки данных на удаленный сервер.

Проект отправки данных объекта с ПЛК SIEMENS:


Данные отправлялись на сервер по протоколу TCP на порт 5000. Ниже приведен код приема данных с объекта Гидрофол.
PHP код приема данных
<?php
// Скрипт приема данных с объекта Гидрофол
// CYBEREX TECH
error_reporting(E_ALL);

$host = "10.10.10.80"; // IP текущего сервера
$port = 5000;          // Порт для примема TCP пакетов

set_time_limit(60); 
$start = microtime(true); // Получаем время запуска скрипта для цикла опроса
$socket = socket_create(AF_INET, SOCK_STREAM, 0) or die("Невозможно создать сокет\n");
$result = socket_bind($socket, $host, $port) or die("Не могу найти сокет\n");
$result = socket_listen($socket, 3) or die("Could not set up socket listener\n");
$spawn = socket_accept($socket) or die("Could not accept incoming connection\n");

       function write_data($database,$t1,$t2,$t3,$t4,$t5,$t6,$t7,$t8,$PR,$RS ){
		            $sdd_db_host ='127.0.0.1';
                    $sdd_db_user ='base';    // логин доступ к базе данных
                    $sdd_db_pass ='пароль базы';// пароль доступа к базе данных
	                $connect     = mysqli_connect($sdd_db_host, $sdd_db_user, $sdd_db_pass, $database ); //php7
	  if ($connect){
		     $result2 = mysqli_query($connect,"INSERT INTO technical (t1, t2, t3, t4, t5, t6, t7, t8, p1, p2, date_time) VALUES ('$t1','$t2','$t3','$t4','$t5','$t6','$t7','$t8','$PR','$RS', NOW())"); //заносим в базу данные, если аналогичная дата отсутствует
	         if ($result2=='TRUE'){
				   echo "Данные успешно отправлены ";
				   mysqli_close($connect); 
                   }
	          }
        }

while((microtime(true) - $start) <= 58){                                        // Запуск цикла опроса порта на 58 сек
           $input = socket_read($spawn, 1024) or die("Could not read input\n"); // Попытка чтения сокета
           $json = trim(substr($input, 2));                                     // Удалаем первые два мусорных символа, что добавляет контроллер SIEMENS
                 if(strlen($json)> 2){                                          // Проверяем длину строки, и если больше двух символов, то обрабатываем
                             $obj = json_decode($json);
	                         if($obj->ID =="HYDROFALL"){                    // Декодируем JSON
                                      $t1 =  round(floatval($obj->T1),2);   // Температура точки 1
                                      $t2 =  round(floatval($obj->T2),2);   // Температура точки 2
                                      $t3 =  round(floatval($obj->T3),2);   // Температура точки 3
                                      $t4 =  round(floatval($obj->T4),2);   // Температура точки 4
                                      $t5 =  round(floatval($obj->T5),2);   // Температура точки 5 
                                      $t6 =  round(floatval($obj->T6),2);   // Температура точки 6
                                      $t7 =  round(floatval($obj->T7),2);   // Температура точки 7
                                      $t8 =  round(floatval($obj->T8),2);   // Температура точки 8
                                      $PR =  round(floatval($obj->PR),2);   // Давление масла в редукторе
                                      $RS =  round(floatval($obj->RS),2);   // Давление масла в маслостанции
                                      $ID = $obj->ID;                       // Идентификатор сессии 
                                      write_data('hydrofall',$t1,$t2,$t3,$t4,$t5,$t6,$t7,$t8,$PR,$RS); // Сохраняем данные в базу
                                      echo $t1, " "; // 
                                      echo $t2, " "; // 
                                      echo $t3, " "; // 
                                      echo $t4, " "; // 
                                      echo $t5, " "; // 
                                      echo $t6, " "; // 
                                      echo $t7, " "; // 
                                      echo $t8, " "; // 
                                      echo $PR, " "; // 
                                      echo $RS, " "; // 
                                      echo $ID, "<br> "; //
	}
  }
}
// Закрываем соединение
socket_close($spawn);
socket_close($socket);
echo "Опрос закончен";
?>


На объекте «Шлам бассейны» ситуация попроще: за контроль уровней отвечают радарные измерители уровня, которые подключены к регистратору данных Memograph M RSG45 от компании Endress + Hauser. Данный регистратор имеет на борту встроенный web-сервер, который умеет отдавать данные в формате XML. Ниже представлен пример вывода данных.

Вывод данных в формате XML
<?xml version="1.0" encoding="UTF-8"?>
<workers>
  <worker>
    <tag>Bacein1</tag>
    <v1>5181</v1>
    <param>
      <min>0</min>
      <max>5727</max>
    </param>
  </worker>
  <worker>
    <tag>Bacein2</tag>
    <v1>4281</v1>
    <param>
      <min>0</min>
      <max>5727</max>
    </param>
  </worker>
  <worker>
    <tag>Banka1</tag>
    <v1>647</v1>
    <param>
      <min>0</min>
      <max>766</max>
    </param>
  </worker>
  <worker>
    <tag>Banka2</tag>
    <v1>388</v1>
    <param>
      <min>0</min>
      <max>766</max>
    </param>
  </worker>
  <worker>
    <tag>Banka3</tag>
    <v1>417</v1>
    <param>
      <min>0</min>
      <max>766</max>
    </param>
  </worker>
  <worker>
    <tag>Banka4</tag>
    <v1>697</v1>
    <param>
      <min>0</min>
      <max>766</max>
    </param>
  </worker>
  <worker>
    <tag>Banka5</tag>
    <v1>101</v1>
    <param>
      <min>0</min>
      <max>762</max>
    </param>
  </worker>
  <worker>
    <tag>Banka6</tag>
    <v1>402</v1>
    <param>
      <min>0</min>
      <max>766</max>
    </param>
  </worker>
  <worker>
    <tag>Banka7</tag>
    <v1>753</v1>
    <param>
      <min>0</min>
      <max>766</max>
    </param>
  </worker>
  <worker>
    <tag>Banka8</tag>
    <v1>360</v1>
    <param>
      <min>0</min>
      <max>711</max>
    </param>
  </worker>
</workers>



Соответственно, нет ничего проще, чем разобрать данный XML с помощью PHP-скрипта. Ниже приведен код модуля опроса для извлечения данных объекта.

PHP модуль опроса объекта
<?php
// Скрипт опроса уровней шлам бассейнов
// CYBEREX TECH - 2022
set_time_limit(60);
$start = microtime(true); 
start();

 function write_data($database,$Bacein1,$Bacein2,$Banka1,$Banka2,$Banka3,$Banka4,$Banka5,$Banka6,$Banka7,$Banka8 ){
		            $sdd_db_host='127.0.0.1';
                    $sdd_db_user='base';    // логин доступ к базе данных
                    $sdd_db_pass='пароль';// пароль доступа к базе данных
	                $connect = mysqli_connect($sdd_db_host, $sdd_db_user, $sdd_db_pass, $database ); //php7
	  if ($connect){
		  $result2 = mysqli_query($connect,"INSERT INTO level_bank (Bacein1, Bacein2, Banka1, Banka2, Banka3, Banka4, Banka5, Banka6, Banka7, Banka8, date_time) VALUES ('$Bacein1','$Bacein2','$Banka1','$Banka2','$Banka3','$Banka4','$Banka5','$Banka6','$Banka7','$Banka8', NOW())"); //заносим в базу данные, если аналогичная дата отсутствует
	  if ($result2=='TRUE')
               {
				   echo "Данные успешно отправлены ";
				   mysqli_close($connect); 
                   }
	  }
	
      } 
function getdata(){
global $start;
$xml = simplexml_load_file('http://10.10.10.58/xml');  // Забираем XML по ссылке устройства



foreach ($xml as $worker) {
	if($worker->tag !=''){
		$data_b = $worker->tag;
		if($data_b == "Bacein1"){	$Bacein1 =$worker->v1;}
		elseif ($data_b == "Bacein2"){	$Bacein2 =$worker->v1;}
		elseif ($data_b == "Banka1"){	$Banka1 =$worker->v1;}
		elseif ($data_b == "Banka2"){	$Banka2 =$worker->v1;}
		elseif ($data_b == "Banka3"){	$Banka3 =$worker->v1;}
		elseif ($data_b == "Banka4"){	$Banka4 =$worker->v1;}
		elseif ($data_b == "Banka5"){	$Banka5 =$worker->v1;}
		elseif ($data_b == "Banka6"){	$Banka6 =$worker->v1;}
		elseif ($data_b == "Banka7"){	$Banka7 =$worker->v1;}
		elseif ($data_b == "Banka8"){	$Banka8 =$worker->v1;}
	
	echo $data_b; 
	echo ": ";
	echo $worker->v1; 
	echo " min: ";
	echo $worker->param->min; 
	echo " max: ";
	echo $worker->param->max; 
	echo "<br>";
		
	}
	
}
if($data_b !=""){
    write_data('clay_bank',$Bacein1,$Bacein2,$Banka1,$Banka2,$Banka3,$Banka4,$Banka5,$Banka6,$Banka7,$Banka8);
}
if((microtime(true) - $start) < 58){
						echo 'Время выполнения скрипта: ' . (microtime(true) - $start) . ' sec<br>';
						sleep(2);
					    getdata();	
					      }else{
						echo 'Общее Время выполнения скрипта: ' . (microtime(true) - $start) . ' sec<br>';
					     }

}

function start(){	
           getdata(); 		
	    }

?>


Ну и так как появилась возможность сетевого подключения оборудования, для облегчения жизни технологического персонала был установлен сенсорный монитор для отображения уровней бассейнов в реальном времени.

Терминал для отображения уровней шлам бассейнов в реальном времени:


Обновленная страница терминала:


Демонстрация работы терминала:


Дополнительные «штучки»


Надеюсь, вы еще помните о переключателе в приложении с названием "Трансляция данных в AR устройство"? Пришло время рассказать и даже показать, что это такое. Это очередная попытка упростить рабочий процесс наладчиков КИП и А, тем самым оптимизировать рабочее время на выполнение замены и/или настройки датчиков. Для этих целей я разработал и собрал прототип следующего устройства.

Прототип AR монитора для фиксации на защитных очках:


AR монитор имеет в своем составе микроконтроллер ESP32 и OLED дисплей. Устройство подключается с помощью Bluetooth Low Energy к смартфону и, используя мобильное приложение системы сбора данных, отображает данные параметра в реальном времени.

Устройство размещено на очках:


Ниже представлено фото отображения данных в AR мониторе, в реальности это выглядит гораздо эффектнее.

Отображение данных в AR:


Как раньше происходила наладка датчика:
  1. Наладчик звонил (или связывался по рации) в операторскую и просил передать показания датчика;
  2. Оператор отрываясь от ведения процесса, открывал окно параметра и диктовал данные;
  3. При изменении настроек или регулировки положения, операция №2 повторялась.

Иногда, когда оператор был слишком занят, приходилось второму наладчику идти в операторскую и по рации диктовать показания датчика. При данном сценарии слишком много людей было вовлечено в процесс.

Как происходит процесс наладки с данным устройством:

  • Наладчик открывает приложение;
  • Выбирает необходимый параметр и запускает трансляцию данных в AR устройство.

При использовании устройства нет необходимости связи с операторской. Данные напрямую передаются от системы в AR-устройство, при этом наладчик имеет свободные руки для манипуляций с датчиком.

Демонстрация работы устройства:


Дальнейшее развитие системы (которое не случилось)


За пять лет работы системы накопился огромный массив данных, который содержал технологические и технические параметры. Ценность технических данных была в том, что они содержали параметры оборудования до и во время аварийных событий, то есть можно было создать шаблоны, которые бы могли предсказать развитие аварийных сценариев. Данную задачу я планировал реализовать с помощью алгоритмов машинного обучения (ML). Также планировалось упростить ведение технологического процесса, применяя модель ML, которая была бы обучена на технологических данных. Ранее я занимался проектом по интеграции лабораторных аналитических приборов в систему LIMS, куда в автоматическом режиме передавались результаты исследований проб клинкера и других составляющих технологического процесса. В данном проекте я также реализовал локальное хранение данных, что позволяло мне воспользоваться ими для обучения модели и поиска закономерностей, используя архивные данные лабораторных исследований и данных технологического процесса.

Итоги


Несмотря на описанный в статье опыт, разработка программного обеспечения абсолютно не входила в профессиональные обязанности ведущего инженера АСУ ТП (в том числе, учитывая уровень оплаты труда). Но, тем не менее, на тот момент это казалось единственным и меньшим по временным затратам решением проблем в АСУ ТП в плане простоев и диагностики проблем. Да и в какой-то мере это был проект, направленный на повышение имиджа нашего подразделения, и, по моему мнению, с этой задачей он прекрасно справился.

И невзирая на то, что 99% кода системы было написано в личное время, долгими домашними вечерами, а иногда и ночами (так как выполнение профессиональных обязанностей никто не отменял, да и обстановка на работе не располагала к творчеству), я вполне доволен результатом и полученным опытом. Но если сейчас меня спросить, готов ли я пожертвовать таким количеством личного времени на реализацию подобного проекта и сделать такой щедрый подарок компании, то я со стопроцентной уверенностью скажу — НЕТ, но это уже другая история.

Статья получилась довольно объемной, спасибо всем, кто дочитал её до конца. Желаю вам удачных проектов и успехов в профессиональной деятельности!




Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале



? Читайте также:

Комментарии (6)


  1. pechkinkorp
    10.10.2024 08:31

    плюсик за труды.

    добавлю, что прозрачные таблички с цифрами я бы сделал не прозрачными.


    1. CyberexTech Автор
      10.10.2024 08:31

      Спасибо за добрый комментарий! Изначально таблица была не прозрачная, но потом захотелось эффекта Aero, видимо это отражение эмоционального состояния на работе)


  1. Paralon12
    10.10.2024 08:31
    +1

    Добротное описание. Сейчас для заводов DATA-ориентированный подход к управлению техпроцессами становится нормой. Повсеместно трубят о Индустрии 4.0, на предприятиях появляются сети датчиков промышленного интернета вещей, IoT-платформы с их яркими и информативными дашбордами. Контроль уставок у измеряемых параметров в тысячах точках контроля. Вобщем, предположу, что эта статья сработает как отличное приложение к Вашему резюме. Инженера с описанной в статье экспертизой промпредприятия явно заметят.


    1. CyberexTech Автор
      10.10.2024 08:31

      Спасибо за позитивный комментарий! Хочу немного пожаловаться: к сожалению, изначально руководство не видело перспектив в подобной системе, и она была реализована скорее не благодаря, а вопреки.


  1. woodiron
    10.10.2024 08:31
    +1

    Хотелось бы увидеть итог деятельности в виде - меня заметили владельцы предприятия и сделали директором завода, но нет. Недавно был на современном цементном заводе, спросил - чьё ПО, оказалось Сименс. А тут всё самостоятельно сделано, здорово.


    1. CyberexTech Автор
      10.10.2024 08:31
      +1

      Как правило, своих специалистов часто обесценивают. За "забором" специалисты качественнее и умнее. Я предлагал создать своё локальное подразделение для разработки ПО, но моё предложение не восприняли всерьез. Хотя на том же предприятии, будучи в статусе ведущего инженера АСУ ТП, я разработал информационную систему в сфере ОТ и ТБ.