Пришла задача придумать “что нибудь” для просмотра и контроля за температурами на производстве. Был уже установлен контроллер ПЛК 160 и подключены датчики температур по интерфейсу RS-485 (Википедия).
Контроллер и датчики были установлены до меня.
Была примерная схема подключения:
Использовали CoDeSys (Википедия) для просмотра.
Не было никакой истории по температурам и неизвестно когда была авария.
Начало
Идея пришла такая — создать WEB-сайт в связке с базой MySQL и хранить там информацию по температурам и авариям.
Первоначальные задачи:
- Просмотр данных с любого компьютера на территории предприятия
- Просмотр текущих аварий и произошедших
- Онлайн просмотр текущих значений
- Изменять максимальные и минимальные значения для регистрации аварий
В дальнейшем выяснилось следующее:
Минимум и максимум недостаточны для контроля аварий.
Были добавлены критический максимум и критический минимум, а также время, во время которого температура может прийти в норму.
- Если температура вышла за пределы минимума или максимума, но вернулась в норму за время T, то это незначительная авария (но данная авария регистрировалась как незначительная).
- Если температура вышла за пределы критического минимума или критического максимума, то это сразу критическая авария.
Потребовалось разграничить доступ:
- Администратор — только для меня )))
- Технологи — для каждого датчика менять 5 параметров
Пришлось добавить изменения параметров аварии по времени. Это для того чтобы, например, с 00:00 до 09:00 не регистрировались аварии.
- Инженеры — калибровка
По правильному необходимо с помощью ноутбука с COM портом цепляться к модулю для калибровки. Решил реализовать так же через WEB, т.е. человек занимающийся калибровкой подходит к датчику со своим термометром и выставляет фактическое значение на сайте.
- Все остальные — просмотр
Программная часть
Была создана виртуальная машина со связкой с ПЛК 160 по локальной сети.
Установлен CoDeSys.
Настроены IP адреса, чтобы компьютер видел контроллер.
Проект находится по пути c:\project\pro\ и называется my_work.pro.
Запуск самого проекта производится через файл run.cmd
"C:\Program Files\3S Software\CoDeSys V2.3\Codesys.exe" "C:\project\pro\my_work.pro" /userlevel 0 /password 157999 /online
Приложение запускает файл run.cmd
WinExec(Pchar(“c:\run.cmd”), SW_HIDE);
Для получения значений температуры использовал DDE (Википедия)
config.ini
[CoDeSys]
service=CoDeSys
topic=C:\project\pro\my_work.pro
item=C:\Program Files\3S Software\CoDeSys V2.3\
cmd=C:\run.cmd
[db]
host=127.0.0.1
port=3306
user=root
key=keypassword
db=workdb
Старт программы:
- Загружаем параметры конфигурации CoDeSys из “config.ini”
Загружаем параметры конфигурации MySQL из “config.ini”
По Таймеру ( Определились что достаточно будет считывать данные раз в минуту ):
- Получаем количество датчиков с MySQL
- Для каждого датчика создаем компонент DDE.DDEConv:
DDE.DDEConv[…]:= TDdeClientConv.Create(Self) DDE.DDEConv[…].ServiceApplication:=”patchcodesys” DDE.DDEConv[…].SetLink(“name”,”patchdde”)
Создаем компонент DDE.DDEItem и связываем с компонентом DDE.DDEConv:
DDE.DDEItem[…]:=TDdeClientItem.Create(Self) DDE.DDEItem[…].DdeConv:=DDE.DDEConv[…]
Передаем имя датчика с MySQL:
DDE.DDEItem[…].DdeItem:=MySQL.GetSensorName(…)
Как результат – получаем значение температуры:
DDE.DDEItem[…].Text
Сохраняем текущее значение температуры и их параметры для каждого датчика.
MySQL.InsertTemp(MySQL.GetSensorName(...),”Температура”,INSQL(UMin[...]),INSQL(UMax[...]),INSQL(CRMin[...]),INSQL(CRMax[...]))
- Получаем из MySQL на текущую дату и время:
Минимум
UMin[I…]:=OUTSQL(MySQL.GetMin(MySQL.GetSensorName(…)))
Максимум
UMax[…]:=OUTSQL(MySQL.GetMax(MySQL.GetSensorName(...)))
Критический минимум
CRMin[…]:=OUTSQL(MySQL.GetCriticalMin(MySQL.GetSensorName(…)))
Критический максимум
CRMax[…]:=OUTSQL(MySQL.GetCriticalMax(MySQL.GetSensorName(…)))
Время
CRTime[…]:=MySQL.GetCriticalTime(MySQL.GetSensorName(…))
Примечание: «Защита отдурака» – если минимум больше максимума или наоборот – то меняем эти значения местами.
if (UMin[…]>=UMax[…]) then begin UM[…]:=UMin[…]; UMin[…]:=UMax[…]; UMax[…]:=UM[…]; end;
- Авария:
Если не было аварии создаем запись
MySQL.InsertCrash(FormatDateTime('yyyy-mm-dd hh:nn:ss', dt),FormatDateTime('yyyy-mm-dd hh:nn:ss', dt),MySQL.GetSensorName(...),…)
Если была авария обновляем
MySQL.UpdateCrash(MySQL.GetCrashID(MySQL.GetSensorName(...)),FormatDateTime('yyyy-mm-dd hh:nn:ss', dt),…)
Авария закончилась устанавливаем Флаг завершения
WEB-сайт
Написал страницы на PHP.
Главная страница (кусок кода, сильно не пинайте):
<?php require 'config.php'; session_start(); $page = isset( $_GET['page'] ) ? $_GET['page'] : ""; switch ( $page ) { case 'login': login(); break; case 'logout': logout(); break; case 'list': listpage(); break; ………………….. ?>
Остальные страницы примерно такого же типа. Каждая страница обрабатывает свои данные.
Что сделано:
- Список датчиков. Наименования, Имя датчика для программы, Тип датчика.
- Датчики были Сгруппированы по назначению.
- Добавлены “статусы аварий”: В процессе аварии, Авария завершена, Критическая авария.
- Реализовано добавление пользователей и их роли.
- Логирование кто что делал.
- Архив всех аварий.
- Графики.
Костыли
- При запуске программы CoDeSys выходит окошко:
Программно его закрываем.
W_WND_Button_Run: HWND: W_WND_RUN: HWND; C_Button_Message='Button'; C_CoDeSys_Message='CoDeSys'; W_WND_RUN := FindWindow(nil,C_CoDeSys_Message); if W_WND_RUN<>0 then begin W_WND_Button_Run:=FindWindowEx(W_WND_RUN, 0,C_Button_Message, nil); if W_WND_Button_Run<>0 then begin SendMessage(W_WND_Button_Run, WM_LBUTTONDOWN, 10, 10); SendMessage(W_WND_Button_Run, WM_LBUTTONUP, 10, 10); SendMessage(W_WND_Button_Run, WM_LBUTTONDOWN, 10, 10); SendMessage(W_WND_Button_Run, WM_LBUTTONUP, 10, 10); end; end;
- Вдруг контроллер отключили.
W_WND_Error:=FindWindow(nil,'Ошибка'); if W_WND_Error<>0 then begin W_WND_Button_Error:=FindWindowEx(W_WND_Error,0,'Button', nil); if W_WND_Button_Error<>0 then begin SendMessage(W_WND_Button_Error, WM_LBUTTONDOWN, 10, 10); SendMessage(W_WND_Button_Error, WM_LBUTTONUP, 10, 10); SendMessage(W_WND_Button_Error, WM_LBUTTONDOWN, 10, 10); SendMessage(W_WND_Button_Error, WM_LBUTTONUP, 10, 10); PostMessage(FindWindow(PChar(C_CoDeSys),nil), WM_QUIT, 0, 0); end; end;
- Непонятное зависание.
Перезапускаем приложение.
C_CLOSE_DEBUG='CoDeSys for Automation Alliance (debug)'; W_WND_CLOSE:=FindWindow(nil,C_CLOSE_DEBUG); if W_WND_CLOSE<>0 then begin KillProcess('Codesys.exe'); KillProcess('WerFault.exe'); PostMessage(FindWindow(PChar(C_Close_DEBUG),nil), WM_QUIT, 0, 0); PostMessage(FindWindow(PChar(C_CoDeSys),nil), WM_QUIT, 0, 0); MySQL.InsertLog('Error debug.. Kill process - codesys.exe and WerFault.exe'); MySQL.InsertLog('Restart programm'); RestartThisApp; end; //Убиваем процесс function KillProcess(ExeName: string): LongBool; var B: BOOL; ProcList: THandle; PE: TProcessEntry32; begin Result := False; ProcList := CreateToolHelp32Snapshot(TH32CS_SNAPPROCESS, 0); PE.dwSize := SizeOf(PE); B := Process32First(ProcList, PE); while B do begin if (UpperCase(PE.szExeFile) = UpperCase(ExtractFileName(ExeName))) then Result := TerminateProcess(OpenProcess($0001, False, PE.th32ProcessID), 0); B:= Process32Next(ProcList, PE); end; CloseHandle(ProcList); end; //Рестарт приложения procedure TForm1.RestartThisApp; begin ShellExecute(Handle, nil, PChar(Application.ExeName), nil, nil, SW_SHOWNORMAL); Application.Terminate; // or, if this is the main form, simply Close; end;
ZABBIX
Создал узел сети с адресом 127.0.0.1.
В нем правило обнаружения с наименованием “Датчики”.
Прототипы элементов данных.
Прототипы триггеров.
Добавляем в zabbix_agentd.conf
UserParameter=sensors[*],/usr/lib/zabbix/alertscripts/sensors.sh UserParameter=crash[*],/usr/lib/zabbix/alertscripts/crash.sh $1
Сами скрипты:
sensors.sh
#!/bin/sh unset id unset res id=(`echo "select id FROM sensor WHERE type='1'" | mysql -uroot -pПароль -D workdb -h 0.0.0.0 --default-character-set=utf8 2>/dev/null`) echo '{ "data": [' for (( count=1; count<${#id[@]}; count++ )) do res=(`echo "select name FROM sensor WHERE (type='1' and id='${id[$count]}') " | mysql -uroot -pПароль -D workdb -h 0.0.0.0 --default-character-set=utf8 2>/dev/null `) r={${res[@]} l=${#r} res1=(`echo "select param FROM sensor WHERE (type='1' and id='${id[$count]}') " | mysql -uroot -pПароль -D workdb -h 0.0.0.0 --default-character-set=utf8 2>/dev/null `) r1={${res1[@]} l1=${#r1} res2=(`echo "select ddename FROM sensor WHERE (type='1' and id='${id[$count]}') " | mysql -uroot -pПароль -D workdb -h 0.0.0.0 --default-character-set=utf8 2>/dev/null `) r2={${res2[@]} l2=${#r2} res3=(`echo "select min FROM temp_${r2:17:l2} ORDER BY id DESC LIMIT 1 " | mysql -uroot -pпарольs -D workdb -h 0.0.0.0 --default-character-set=utf8 2>/dev/null`) r3={${res3[@]} l3=${#r3} res4=(`echo "select max FROM temp_${r2:17:l2} ORDER BY id DESC LIMIT 1 " | mysql -uroot -pПароль -D workdb -h 0.0.0.0 --default-character-set=utf8 2>/dev/null`) r4={${res4[@]} l4=${#r4} res5=(`echo "select cmin FROM temp_${r2:17:l2} ORDER BY id DESC LIMIT 1 " | mysql -uroot -pПароль -D workdb -h 0.0.0.0 --default-character-set=utf8 2>/dev/null`) r5={${res5[@]} l5=${#r5}2>/dev/null res6=(`echo "select cmax FROM temp_${r2:17:l2} ORDER BY id DESC LIMIT 1 " | mysql -uroot -pПароль -D workdb -h 0.0.0.0 --default-character-set=utf8 2>/dev/null`) r6={${res6[@]} l6=${#r6} res7=(`echo "select param FROM temp_${r2:17:l2} ORDER BY id DESC LIMIT 1 " | mysql -uroot -pПароль -D workdb -h 0.0.0.0 --default-character-set=utf8 2>/dev/null`) r7={${res7[@]} l7=${#r7} s=$s'{ "{#SID}": "'${id[$count]}'", "{#SNAME}": "'${r:5:l}'", "{#SDDENAME}": "'${r2:17:l2}'" , "{#SPARAM}": "'${r7:7:l7}'", "{#SMIN}": "'${r3:5:l3}'", "{#SMAX}": "'${r4:5:l4}'", "{#SCMIN}": "'${r5:6:l5}'", "{#SCMAX}": "'${r6:6:l6}'" },' done a=${#s} b=${s: 0: $a-1} c=${#b} d=$b echo $d']}'
crash.sh
#!/bin/sh a=$1 unset res res=(`echo "select flag, id_status FROM crash WHERE id_sensor='$a' ORDER BY id DESC LIMIT 1 " | mysql -uroot -pПароль -D workdb -h 0.0.0.0 --default-character-set=utf8 2>/dev/null `) for (( count=2; count<${#res[@]}; count++ )) do s=$s' '${res[$count]} done b=${s:0:2} c=${s:3:4} if [ $b = 0 -a $c = 1 ] then echo 0 else echo 1 fi
А дальше через zabbix можно и отправлять на почту и смс и многое другое.
Результат
Получилась система мониторинга температур на предприятии с просмотром текущих и произошедших аварий.
Подробнее об аварии.
На данный момент добавлены датчики открытия/закрытия дверей.
Плюсы:
- Минимальные затраты (относительно).
- Плюс к карме (?).
- Мониторинг работает уже 3 года.
Минусы:
- Много точек отказа: контроллер, сеть, программа CoDeSys, виртуальная машина, MySQL, IIS.
P.S.
Не пинайте сильно. Это первая моя статья.