Реализация протокола SNMP на контроллерах Fastwel
Особенности реализации протокола SNMP на контроллерах Fastwel.
Была поставлена задача по реализации системы на контроллерах Fastwel. Объект был военный, данные контроллеры отечественного производителя (Россия), система создавалась под специфические запросы.
В статье изложен действующий пример того, как в Codesys можно реализовать проблему отсутствия какого-то нативного (вложенного) протокола, который не поддерживается контроллером по умолчанию. Codesys достаточно мощная и гибкая среда разработки. При достаточно скромных возможностях аппаратного обеспечения программист может создать сложный проект, используя встроенные возможности программного обеспечения. Программа основана на использовании стандартных сокетов (портов), к которым обращаются стандартные библиотеки.
Если рассматривать разницу между брендами Fastwel или WAGO, то контроллеры Fastwel - это удачная попытка российских производителей сделать свой контроллер, аналогичный европейскому. У производителей есть собственная производственная площадка. Они сами изготавливают платы, сами собирают модули, хотя, конечно, вся элементарная база чипов - это импорт из Китая. С другой стороны, это почти чистый WAGO, где даже форм-фактор (корпуса) и даже некоторые программные библиотеки подходят и работают. Исключением стал протокол SNMP. На Fastwel он просто незаработали библиотеки от WAGO, и поэтому пришлось писать свой код.
Применяют контроллеры Fastwel в основном для государственного сектора, например, для военных и специальных структур. Оборудование имеет сертификат российского производителя и производитель может дать расширенную гарантию в 11-12 лет. Если что-то ломается, то бесплатно по соответствующей партии делается замена. Учитывая, что размеры партий не очень большие, это не сильно влияет на прибыль. В 2017-2018 годы стоимость WAGO была ниже FASTWEL где-то в полтора раза.
О реализации протокола SNMP
Вообще реализацией протокола это назвать можно с огромной натяжкой, так как реализовано только получение данных. С другой стороны, заменой порта и направлением потока данных можно записывать необходимые данные в устройство. Также данная реализация возможна не только на контроллерах Fastwel, а вообще на всех контроллерах, использующих среду разработки Codesys 2.3. Используется библиотека FastwelSysLibSockets, для других контроллеров можно использовать стандартную SysLibSockets.
Итак, сторона принимающая – контроллер Fastwel CPM713. Сторона, отдающая данные, – неведомый контроллер уникального ИБП. В инструкции прописаны адреса данных.
Сразу следует оговориться, что обмен писался под определенное устройство, без возможности на ходу менять адрес, порт, направление чтения и прочее, но весь этот функционал неплохо реализуется при желании.
Итак, у нас есть функция, которая отвечает за формирование строки запроса (за полным описанием протокола лучше обратиться к документу RFC-1592).
FUNCTION RequestForming : ARRAY[0..50] OF BYTE (*Результат – строка запроса в виде последовательности байт. Сделано для совместимости с функциями отправки данных*)
VAR_INPUT
Address: STRING[50]; (*Адрес переменной в строковом виде, то есть 1.3.6.1.4.1.34498.2.1.1.1.1.0 , например*)
END_VAR
VAR
Ar: ARRAY [0..20] OF INT; (*Массив для преобразования адреса переменной из строкового в числовой вид*)
Title: ARRAY[0..38] OF BYTE := 16#30, 16#2D, 16#02, 16#01, 16#01, 16#04, 16#06, 16#70, 16#75, 16#62, 16#6C, 16#69, 16#63, 16#A0, 16#20, 16#02, 16#02, 16#5A, 16#FB, 16#02, 16#01, 16#00, 16#02, 16#01, 16#00, 16#30, 16#14, 16#30, 16#12, 16#06, 16#0E, 16#2B, 16#06, 16#01, 16#04, 16#01, 16#82, 16#8D, 16#42; (*Заголовок запроса. К сожалению, пришлось тянуть его из сниффера, потому как в описании протокола SNMP есть неточности и самостоятельно его сформировать не получилось*)
I: INT; (*Всяческие временные и служебные переменные*)
J: BYTE;
tmpStr: STRING;
END_VAR
(*Заполняем заголовок запроса. Он всегда один*)
FOR I := 0 TO 38 DO
RequestForming[I] := Title[I];
END_FOR
(*Распознаём строку адреса и формируем битовый адрес для запроса*)
J := 0;
tmpStr := '';
FOR I := 1 TO LEN(Address) DO
IF MID(Address, 1, I) = '.' THEN
Ar[J] := STRING_TO_INT(tmpStr);
J := J+1;
tmpStr := '';
ELSE
tmpStr := CONCAT(tmpStr, MID(Address, 1, I));
END_IF
END_FOR
(*Дописываем адрес запроса нашими значениями*)
FOR I := 7 TO J DO
RequestForming[I-6+38] := INT_TO_BYTE(Ar[I]);
END_FOR
(*Ну и меняем необходимые данные*)
RequestForming[38+J-6+1] := 16#05;(*Значение окончания запроса*)
RequestForming[38+J-6+2] := 16#00;
RequestForming[1] := 37+J-4; (*Длина всего запроса*)
RequestForming[14] := 24+J-4;(*Длина запроса Get*)
RequestForming[26] := 12+J-4;(*Длина Цифрового кода переменной*)
RequestForming[28] := 10+J-4;
RequestForming[30] := 8+J-6;
Сама программа отправки запросов и получения ответов
Для начала в разделе типов определим следующие типы данных:
TYPE TSNMPAnswer : ARRAY[0..ANSWER_SIZE] OF BYTE;
END_TYPE;
TYPE TSNMPRequest: ARRAY[0..REQUEST_SIZE] OF BYTE;
END_TYPE;
Массив IBEPReq расположен в глобальных переменных и заполнен адресами переменных
IBEPReq[0] := '1.3.6.1.4.1.34498.2.1.1.1.1.0';
IBEPReq[1] := '1.3.6.1.4.1.34498.2.1.1.1.2.0';
…
PROGRAM SNMP_READ
VAR
clntSendSocket: DINT := SOCKET_INVALID;
clntRecvSocket: DINT := SOCKET_INVALID;
sockAddr: SOCKADDRESS;
sockAddrRecv: SOCKADDRESS;
SendDataBytes: DINT;
BytesReceived: DINT;
sendBuffer: TSNMPRequest;
recvBuffer: TSNMPAnswer;
ReqNum: INT;
dintOpt: DINT;
blRes: BOOL;
SendingTimer: TON;
i: DINT;
StartTimer: TON;
StartSend: BOOL;
END_VAR
REPEAT (*В бесконечном цикле…*)
(*Формируем запрос*)
sendBuffer := RequestForming(IBEPReq[ReqNum]);
IF clntSendSocket = SOCKET_INVALID THEN
(*Создать сокет клиента*)
clntSendSocket := FwSysSockCreate(SOCKET_AF_INET, SOCKET_DGRAM, SOCKET_IPPROTO_UDP);
dintOpt := 1;
blRes := FwSysSockSetOption(clntSendSocket, SOCKET_SOL, SOCKET_SO_REUSEADDR, ADR(dintOpt), SIZEOF(dintOpt));
IF clntSendSocket = SOCKET_INVALID THEN
EXIT;
END_IF;
sockAddr.sin_family := SOCKET_AF_INET;
sockAddr.sin_addr := FwSysSockInetAddr(clntIpAddr);
sockAddr.sin_port := FwSysSockHtons(srvPort);
blRes := FwSysSockBind(clntSendSocket, ADR(sockAddr), SIZEOF(sockAddr));
END_IF;
(*Если сокет создан…*)
IF clntSendSocket <> SOCKET_INVALID THEN
IF StartSend THEN
(*… пытаемся отправить данные*)
sockAddr.sin_family := SOCKET_AF_INET;
sockAddr.sin_addr := FwSysSockInetAddr(srvIpAddr);
sockAddr.sin_port := FwSysSockHtons(srvPort);
SendDataBytes := FwSysSockSendTo(clntSendSocket, ADR(sendBuffer), SIZEOF(sendBuffer), 0, ADR(sockAddr), SIZEOF(sockAddr));
FOR i := 0 TO ANSWER_SIZE-1 DO
recvBuffer[i] := 0;
END_FOR
(*Запрос сформировали, отправили…*)
StartSend := FALSE;
ELSE
(*…ждем ответа…*)
BytesReceived := FwSysSockRecvFrom(clntSendSocket, ADR(recvBuffer), ANSWER_SIZE, 0, ADR(sockAddrRecv), SIZEOF(sockAddrRecv));
IF BytesReceived > 0 THEN
IBEPConn := TRUE;
IBEPAnsw[ReqNum] := recvBuffer;
StartSend := TRUE;
ReqNum := ReqNum + 1;
IF ReqNum >= REAL_TO_INT(SIZEOF(IBEPReq)/35) THEN
ReqNum := 0;
IBEP_Slave();(*ФБ обработки ответов*)
END_IF
ELSE
(*Рвем соединение, чтобы подключиться заново*)
IBEPConn := FALSE;
END_IF
END_IF
END_IF
(*Реализация таймаута получения ответа*)
IF StartTimer.Q THEN
StartSend := TRUE;
END_IF
StartTimer(IN := NOT StartSend, PT := t#100ms);
UNTIL TRUE
END_REPEAT
Ну и, собственно, разбор полученного ответа в функциональном блоке, так как ИБП может быть несколько
FUNCTION_BLOCK IBEP
VAR_INPUT
Slave_Status : NET_CONNECTION_STATUS;
END_VAR
VAR_OUTPUT
SlaveStatus : NET_CONNECTION_STATUS;
uDC: REAL;
iDC: REAL;
ControllerTemp: REAL;
NumberOfACGroup: INT;
NumberOfAlarms: INT;
ACFlag: BOOL;
DCPower: REAL;
LoadPercent: REAL;
MainAlarm: BOOL;
ACAlarm: BOOL;
RectifierAlarm: BOOL;
InverterAlarm: BOOL;
BattDischargeAlarm: BOOL;
BattLowAlarm: BOOL;
BattDisBalanceAlarm: BOOL;
BattCount: INT;
BattCurrent: REAL;
END_VAR
VAR
AnswerType: BYTE;
AnswerSize: INT;
AnswerPos: INT;
I: INT;
Answer: TSNMPAnswer;
Stt: STRING;
J: INT;
K: INT;
iValue: INT;
rValue: REAL;
END_VAR
VAR_IN_OUT
END_VAR
IF IBEPConn THEN
SlaveStatus := NCS_CONNECTED;
ELSE
SlaveStatus := NCS_NOT_CONNECTED;
END_IF
(*Вытаскиваем данные из ответа*)
FOR K := 0 TO REAL_TO_INT(SIZEOF(IBEPReq)/35)-1 DO
Answer := IBEPAnsw[K];
I := Answer[30]+30;
AnswerPos := I+3;
AnswerSize := Answer[I+2];
AnswerType := Answer[I+1];
IF AnswerType = 2 THEN
iValue := Answer[AnswerPos];
END_IF
IF AnswerType = 4 THEN
Stt := '';
FOR J := 1 TO AnswerSize DO
CASE (Answer[AnswerPos+J-1]) OF
48..57: Stt := CONCAT(Stt, BYTE_TO_STRING(Answer[AnswerPos+J-1]-48));
44, 46: Stt := CONCAT(Stt, '.');
ELSE
;
END_CASE
END_FOR
IF Stt <> '' THEN
rValue := STRING_TO_REAL(Stt);
END_IF
END_IF
(*Сортируем данные в зависимости от того, с какой переменной это всё пришло*)
CASE K OF
1: uDC := rValue;
2: iDC := rValue;
3: ControllerTemp := rValue;
4: NumberOfACGroup := iValue;
5: NumberOfAlarms := iValue;
6: ACFlag := iValue > 0;
7: DCPower := rValue;
8: LoadPercent := rValue;
9: MainAlarm := iValue > 0;
10: ACAlarm := iValue > 0;
11: RectifierAlarm := iValue > 0;
12: InverterAlarm := iValue > 0;
13: BattDischargeAlarm := iValue > 0;
14: BattLowAlarm := iValue > 0;
15: BattDisBalanceAlarm := iValue > 0;
16: BattCount := iValue;
17: BattCurrent := rValue;
ELSE
;
END_CASE;
END_FOR
Автор решения: Михаил Туровец (Красноярск)
#Fastwel, #SNMP
Оставьте первый комментарий