Fastwel + SNMP
Features of the implementation of the SNMP protocol on Fastwel controllers.
The task was set to implement the system on Fastwel controllers. The object was military, these controllers were made by a domestic manufacturer (Russia), the system was created for specific requests.
The article presents a working example of how Codesys can implement the problem of the absence of some native (nested) protocol that is not supported by the controller by default. Codesys is a fairly powerful and flexible development environment. With fairly modest hardware capabilities, a programmer can create a complex project using the built-in capabilities of the software. The program is based on the use of standard sockets (ports) accessed by standard libraries.
If we consider the difference between the Fastwel or WAGO brands, then Fastwel controllers are a successful attempt by Russian manufacturers to make their own controller similar to the European one. Manufacturers have their own production site. They make the boards themselves, assemble the modules themselves, although, of course, the entire elementary base of chips is imported from China. On the other hand, it's almost pure WAGO, where even the form factor (enclosures) and even some software libraries fit and work. The exception was the SNMP protocol. On Fastwel, he simply did not earn the libraries from WAGO, and therefore had to write his own code.
Fastwel controllers are used mainly for the public sector, for example, for military and special structures. The equipment has a certificate of the Russian manufacturer and the manufacturer can give an extended warranty of 11-12 years. If something breaks, then a replacement is made free of charge for the corresponding batch. Given that the batch sizes are not very large, this does not greatly affect the profit. In 2017-2018, the cost of WAGO was about one and a half times lower than FASTWEL.
About the implementation of the SNMP protocol
In general, it can be called the implementation of the protocol with a huge stretch, since only data acquisition is implemented. On the other hand, by replacing the port and directing the data flow, you can write the necessary data to the device. Also, this implementation is possible not only on Fastwel controllers, but in general on all controllers using the Codesys 2.3 development environment. The FastwelSysLibSockets library is used, for other controllers you can use the standard SysLibSockets.
So, the receiving side is the Fastwel CPM713 controller. The party giving the data is an unknown controller of a unique UPS. The instructions contain the addresses of the data.
You should immediately make a reservation that the exchange was written for a specific device, without the ability to change the address, port, reading direction, etc. on the go, but all this functionality is implemented well if desired.
So, we have a function that is responsible for forming a query string (for a full description of the protocol, it is better to refer to the document RFC-1592).
FUNCTION RequestForming : ARRAY[0..50] OF BYTE (*The result is a query string in the form of a sequence of bytes. Made for compatibility with data sending functions *)
VAR_INPUT
Address: STRING[50]; (*The address of the variable in string form, i.e. 1.3.6.1.4.1.34498.2.1.1.1.1.0 , for example *)
END_VAR
VAR
Ar: ARRAY [0..20] OF INT; (*Array for converting the address of a variable from a string to a numeric form *)
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; (*Request header. Unfortunately, I had to pull it from the sniffer, because there are inaccuracies in the description of the SNMP protocol and it was not possible to form it independently *)
I: INT; (*All sorts of temporary and service variables *)
J: BYTE;
tmpStr: STRING;
END_VAR
(*Fill in the request header. He's always alone *)
FOR I := 0 TO 38 DO
RequestForming[I] := Title[I];
END_FOR
(*We recognize the address string and form a bit address for the request *)
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
(*We add the request address with our values *)
FOR I := 7 TO J DO
RequestForming[I-6+38] := INT_TO_BYTE(Ar[I]);
END_FOR
(*Well, we change the necessary data *)
RequestForming[38+J-6+1] := 16#05;(* The value of the end of the request *)
RequestForming[38+J-6+2] := 16#00;
RequestForming[1] := 37+J-4; (*Length of the entire request *)
RequestForming[14] := 24+J-4;(* Length of the Get request *)
RequestForming[26] := 12+J-4;(* The length of the Digital code of the variable *)
RequestForming[28] := 10+J-4;
RequestForming[30] := 8+J-6;
The program itself sends requests and receives responses
To begin with, in the types section, we define the following data types:
TYPE TSNMPAnswer : ARRAY[0..ANSWER_SIZE] OF BYTE;
END_TYPE;
TYPE TSNMPRequest: ARRAY[0..REQUEST_SIZE] OF BYTE;
END_TYPE;
The IBEPReq array is located in global variables and is filled with variable addresses
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 (*In an infinite loop…*)
(*Forming a request *)
sendBuffer := RequestForming(IBEPReq[ReqNum]);
IF clntSendSocket = SOCKET_INVALID THEN
(*Create a client socket *)
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 the socket is created…*)
IF clntSendSocket <> SOCKET_INVALID THEN
IF StartSend THEN
(*... trying to send data *)
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
(*The request was formed, sent…*)
StartSend := FALSE;
ELSE
(*...waiting for an answer…*)
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();(*FB response processing *)
END_IF
ELSE
(*Breaking the connection to reconnect *)
IBEPConn := FALSE;
END_IF
END_IF
END_IF
(*Implementing a response timeout *)
IF StartTimer.Q THEN
StartSend := TRUE;
END_IF
StartTimer(IN := NOT StartSend, PT := t#100ms);
UNTIL TRUE
END_REPEAT
And, in fact, the analysis of the received response in the functional block, since there may be several UPS
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
(*Pulling the data out of the response *)
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
(*We sort the data depending on which variable it all came from *)
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
The author of the decision: Mikhail Turovets (Krasnoyarsk)
#Fastwel, #SNMP
Be the first to comment