Moscow, Azovskaya 14
+7 (495) 310-97-15
Mo-Fr: 9.00 - 18.00 (Moscow time)
Order a call
Callback
Your name *
Enter phone *
Your Email *
Call me back
Sample source code for Fastwel controller

Fastwel + SNMP

Features of the implementation of the SNMP protocol on Fastwel controllers.


Fastwel

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

You comment add