Как мы получали данные от компрессоров

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

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

681c603edfff3ce32d43a77f032303b1.PNG

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

1307919273c268a12dab57b71d34b981.png

Для проверки своей теории я решил запросить через VBS скрипт только то, что нужно мне,

Dim objHTTP
strToSend = "package_discharge_pressure&sump_pressure"
Set objHTTP = CreateObject("Microsoft.XMLHTTP")
Call objHTTP.Open("POST", "http://10.0.163.51/getVar.cgi", false)
objHTTP.setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
objHTTP.Send strToSend
MsgBox(objHTTP.ResponseText)

и как и ожидалось получил то что хотел:

Поскольку данные оказалось так просто получить, то решено было написать программу на C#, которая бы запрашивала каждую секунду нужные данные от компрессора, а затем пересылала бы его ответ на наш сервер. Самый простой вариант это использовать WebClient, но почему-то он не заработал с этим компрессором, хотя на запросы от VBS и powershell отвечал нормально. Но зато заработал WinHttpRequest. В принятом ответе происходит замена & на  разделение на массив значений, который потом преобразуется в массив байт, который передается на наш сервер по протоколу UDP. Весь этот код нагуглил в интернете:

Hidden text

using System;
using System.Globalization;
using WinHttp;
using System.Net;
using System.Net.Sockets;
using System.Threading;

public class getweb
{
    private static IPAddress remoteIPAddress = IPAddress.Parse("127.0.0.1");
    private static int remotePort = 5061;
    [STAThread]
    public static void Main()
    {
        string rl="1";
        do
        {
            if (rl == "1")
            {

                WinHttpRequest req = new WinHttpRequest();

                try
                {
                    req.Open("POST", "http://10.0.163.51/getVar.cgi", true);
                    req.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded");
                    req.Send("online_pressure&offline_pressure&package_discharge_pressure&sump_pressure&airend_discharge_temperature&injected_coolant_temperature&aftercooler_discharge_pressure&separator_pressure_drop&coolant_filter_pressure_drop&inlet_vacuum_pressure&remote_pressure&aftercooler_discharge_temperature&interstage_pressure&machine_state_number&comm_control&status_flags");
                    req.WaitForResponse();
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error : " + ex.Message.Trim());
                    System.IO.File.WriteAllText("C:\\logs\\err_ingers_" + DateTime.Now.ToString("HHmmss") + ".txt", "INGERS WEB: " + ex.ToString() + "\n " + ex.Message);
                }
                string answer = req.ResponseText;
                answer = answer.Replace("&", ";");
                
                float[] strarr = new float[35];
                for (int i = 0; i< 35; i++)
                {
                    int position = answer.IndexOf(";");
                    string str = answer.Substring(0,position);
                    strarr [i] = float.Parse(str, CultureInfo.InvariantCulture.NumberFormat);
                    answer = answer.Substring(position+1);
                                    
                }
                var byteArray = new byte[strarr.Length * 4];
                Buffer.BlockCopy(strarr, 0, byteArray, 0, byteArray.Length);
                Send(byteArray);
                Thread.Sleep(500);
            }
        }
        while (rl=="1");          

    }
    private static void Send(byte[] datagram)
    {
        // Создаем UdpClient
        UdpClient sender = new UdpClient();

        // Создаем endPoint по информации об удаленном хосте
        IPEndPoint endPoint = new IPEndPoint(remoteIPAddress, remotePort);

        try
        {
            sender.Send(datagram, datagram.Length, endPoint);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Возникло исключение: " + ex.ToString() + "\n  " + ex.Message);
            System.IO.File.WriteAllText("C:\\logs\\err_ingers_udp_" + DateTime.Now.ToString("HHmmss") + ".txt", "INGERS udp: " + ex.ToString() + "\n " + ex.Message);
        }
        finally
        {
            // Закрыть соединение
            sender.Close();
        }
    }
}

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

Аналогичным образом устроен веб-интерфейс второго компрессора Atlas Copco: так же каждую секунду запрос, и каждую секунду ответ, но проблема только в том, что запрос и ответ — это массив шестнадцатиричных значений, который вообще непонятно как расшифровать. Например запрос выглядит как QUESTION=30020130020230020330020430020530030130030230030330030430030a30070130070330070430070530070630070730070830070930070a30070b30070c30070d30210130210530210a300501300504300505300506300507300508300509300e02300e03300e04300e18300e2a31130131130331130431130531130731130831130931130a31130b31130c31130d31130e31130f31131031131131131231131331131431131531131631131731131831131931131a31131b31131c31131d31131e31131f31132031132131132231132331132431132531132631132731132831132931132a31132b31132c31140131140231140331140431140531140631140731140831140931140a31140b31140c31140d31140e31140f311410311411311412300901300906300911300907300912300909300108

А ответ приходит такой: 1B1B00807FFF00D002D40080000C008000C900800001008000010080000000800001008000000080149A264B00000C3B000003180AA72A13020C2D7F023B535A01B432FF03F74860000000C80000A0630000230D07381393060006100000002C00B6022000010080000100800001008000010080000100800001008000010080230100002302000023030000231700002329000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000XX00000000000000000000000000000064XXXXXXXXXXXXXX00000003006489B700B2A4A4014043B700B2A4A7014043B70000001C

43f6da663844bcecbcbaf09f315ad712.png

Я сейчас не особо помню как понял как нужно расшифровывать запрос и ответ, видимо обратил внимание на повторяющиеся цифры в запросе, и углядел некоторую структуру., но вообще получилось довольно просто расшифровать строку, сделав скрин в определенный момент, и сохранив ответ от компрессора. Сопоставил в Экселе отображаемые данные на мониторе, с теми что были в 16 ричном формате и получилась такая довольно простая табличка, больше методом подгона:

f08d3a88b44bd3a5e717367334f34296.PNG

Опять же на сервере все это дело требует расшифровки и преобразований в нужные единицы измерения.

Третий компрессор был без веб-интерфейса, хотя и имел какой-то Ethernet порт и в теории мог бы что-то по нему передавать, но протокол передачи был неизвестен, а потому решили накинуть на каждую фазу мотора компрессора трансформаторы тока, просто чтобы знать ток, и передавать его через контроллер на наш сервер. Однако, немного подумав, поняли, что для такого простого дела юзать целый контроллер не совсем продуктивно, и нужно придумать что-то попроще. Трансформаторы тока подключались через отечественные измерители, у которых на борту имелся Modbus и тогда было решено заюзать преобразователь из модбаса в TCP/IP. К тому же софт для сбора данных, как бы имел возможность принимать данные по протоколу Модбас. По прошествии времени выяснилось, что данный софт на сервере при использовании функции сбора данных по модбасу работал плохо и программа часто зависала, и потому решено было идти по старой схеме: опять городить костыли с UDP передачей.

Преобразователь MOXA я настроил в режиме TCP сервера, т.е. он ожидает подключение клиента на 4002 порту и на запросы клиента выдает ответы. Сами запросы посылаются в порт в формате Modbus RTU — то есть запрашивается номер устройства, функциональный код регистра, адрес регистра, количество запрашиваемых регистров, и контрольная сумма

c1aaaf05e6cb850ffdab72823ed9c270.PNG

В самой программе для каждого измерителя тока уже предусмотрен массив байт для запроса, например для первого устройства:

private static byte[] device1 = {0x01,0x03,0x00,0x14,0x00,0x02,0x84,0x0F}; 

01 — адрес измерителя, 03 — запрос Analog Output Holding Registers, 0014 — адрес регистра, 0002 — два регистра запрашиваются (из документации к прибору), 840F — контрольная сумма CRC. Далее создается TCP клиент, и сразу же отправляется запрос для первого устройства.

TcpClient client = new TcpClient("10.0.163.52", 4002);
NetworkStream stream = client.GetStream();
if (deviceNumb==1) {
	stream.Write(device1, 0, device1.Length);
}

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

Таким образом получилось сделать 3 программы, которые крутятся на еще одном сервере, и пересылают данные на другой. Вот потому и нестандартное программирование, кто-то скажет даже всратое, и наверное будет прав. И еще кое-что. Для удобства, чтобы не было видно запущенных окон, все три проги были преобразованы в сервисы Windows при помощи утилиты nssm.exe — даже если сервак перезапустят, службы запустятся в автомате. И да, такая фигня крутится уже больше года и за все время сервисы приходилось перезапускать не больше 3 раз, что как мне кажется небольшая победа, учитывая, костыль на костыле, и что программировалось все при помощи гугла. И да чаще останавливается именно сервис мохи, поскольку подключение происходит при первом запуске программы и дальше возможные разрывы соединения не учитываются, поэтому помогает перезапуск.

© Habrahabr.ru