pablofsmelo
Hello
I have an RFID reader that communicates Ethernet TCP-IP. I need to implement a TCP IP client in OpenPLC in order to communicate with the reader.
Based on internet examples, I tried to port a simple client to the main file, but apparently it is not running. When I test the code out of OpenPLC the expected behavior is reached successfully.
Does anyone have any suggestions on how to implement this within the OpenPLC code?

The following is an example that was used to test a simple TCP IP communication:
Note: The "tcp_run" function was called after config_init ();

#define PORT "50007" // the port client will be connecting to
#define MAXDATASIZE 100 // max number of bytes we can get at once

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int tcp_run(void)
{
sprintf(log_msg, "Entrei na funcao de teste...\n");

    int sockfd, numbytes;
    char buf[MAXDATASIZE];
    struct addrinfo hints, *servinfo, *p;
    int rv;
    char s[INET6_ADDRSTRLEN];
    const char *ip = "127.0.0.1";   

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((rv = getaddrinfo(ip, PORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // loop through all the results and connect to the first we can
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("client: socket");
            continue;
        }

        if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
            perror("client: connect");
            close(sockfd);
            continue;
        }

        break;
    }

    if (p == NULL) {
        fprintf(stderr, "client: failed to connect\n");
        return 2;
    }

    inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr),
            s, sizeof s);
    printf("client: connecting to %s\n", s);

    freeaddrinfo(servinfo); // all done with this structure

    if ((numbytes = recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) {
     perror("recv");
     exit(1);
    }

    buf[numbytes] = '\0';

    printf("client: received '%s'\n",buf);
    printf("IP: %s\n", ip);

    close(sockfd);
return 0;
}
Quote 0 0
thiagoralves
You need to call your tcp_run() function somewhere for it to be able to run. Maybe a better approach would be to just add this code on the hardware layer code box. However, this would be far from optimized. OpenPLC already has a lot of network functionality built in. The best approach would be to use its own network functions. What protocol does this RFID reader uses to talk? Is it plain TCP/IP strings?
Quote 0 0
pablofsmelo
I figured OpenPLC would have something implemented, but I could not identify. I have checked that the closest thing to what I need is the interactive_server.cpp file, however I need to implement the code that a TCP-IP client performs.
Yes, Thiago. The RFID reader communicates with another device via TCP IP implementing simple strings for configuration and reading.
Quote 0 0
thiagoralves
OpenPLC has built-in functions to start a server, but not to connect to someone else, and it seems that this is what you need right? If the reader would talk Modbus, then you could use libmodbus functions to talk to it, but since it is just TCP strings, then you will have to implement the functions yourself. As I said before, I believe that the Hardware Layer Code Box is the best place for you to add your functions.
Quote 0 0
pablofsmelo
Hi Thiago,

I've implemented the code in the "Hardware Layer Code Box" and am performing tests in order to investigate whether the new implementation can not affect system performance.

I put the initialization of the RFID reader in the function "void initCustomLayer" and this excerpt is ok.

Updating the variables related to the rfid tags was added in the "void updateCustonIn ()" function.

To perform the update of these variables, I need to execute the function "recv (sockfd_evt, buff_tag, 100, 0);". Every time this function is called, the system seems to be stuck waiting for a TCP update for buff_tag. When I comment on this line, I see that the "void updateCustonIn () function is often called in the main.cpp file (as expected).

Could you tell me some other alternative within OpenPLC that corrects this problem, since the recv function call is mandatory for my application?
Quote 0 0
thiagoralves
You will have to make these requests asynchronously. Requests that go to the network can take any amount of time to be fulfilled, and this is not good for real time systems. Therefore, the best way to make those network requests (read and write) is to make them asynchronously. Just create a thread on the initialization function, and let your parallel thread take care of all the communication and store the answers in a buffer. Then, inside the update buffers functions, all you do is just copy the contents of the buffer from the thread to the right place.
Quote 0 0
pablofsmelo
Is there an example within Openplc that would facilitate this development?
Quote 0 0
thiagoralves
The entire slave devices code is asynchronous. Check /webserver/core/modbus_master.cpp for an example. The initializeMB() function creates the thread at the very end (querySlaveDevices). The thread runs in an infinite loop while OpenPLC is running. It then keeps doing its thing and stores all values read on internal buffers. The functions updateBuffersIn_MB() and updateBuffersOut_MB() update OpenPLC buffers with the values stored on the thread buffer
Quote 0 0
pablofsmelo
I made the adaptations as recommended and works using OpenPLC on an I5 Machine (Linux Ubuntu). When I try to run the same code, however on a Raspberry, the openplc does not run and i get the following message:
 
"OpenPLC Runtime is not running Error; [Errno 111] Connection Refused"
 
Would you know why this might be happening ?
Quote 0 0
thiagoralves
This error means that you’re not being able to connect to the server you set up. Are you sure you changed that char *ip constant to the right one now that you’re running on the pi?
Quote 0 0
cshbicos
Hi guys,

I faced a very similar problem the last couple of days in that I have a few different other components that need to communicate into my PLC via TCP/IP strings - both reading and writing back.

So I tried to build a "native" support for TCP servers into the current OpenPLC runtime. It sits against my github fork cshbicos/OpenPLC_v3 .

I was hoping Thiago could have a look and give me some feedback?

At the moment it supports setting up to 3 TCP servers. Each server listens in a dedicated thread and uses provider/consumer patterns to fill reading/writing buffers, one line at a time.
 
I somehow stumbled across the fact that if I define a global string %MB1000 against the resource, I can directly read/write strings into the PLC code (after extending the glue_variables).... I'm not sure if this is correct or a bug in matiec? Either way, it makes my concepts work nicely 🙂


So each TCP server will use the following (Instead of 1000, replace with 1000 + ({ServerNumber} * 2)):

%MB1000 to provide new strings into the PLC environment
%MX1000.0 for the PLC to inform the TCP server that a new string can be read
%MX1000.1 for the TCP server to tell the PLC environment that a new string is ready to be read

%MB1001 to provide new strings to the TCP server
%MX1000.2 for the PLC to inform the TCP server that a new string should be written to all clients

I wrote a very small tcp echo program as a ST file and it works quite nicely

PROGRAM program0
VAR
READ_READY AT %MX1000.0 : BOOL;
READ_RESULT_READY AT %MX1000.1 : BOOL;
WRITE_DATA_SET AT %MX1000.2 : BOOL;
END_VAR
VAR_EXTERNAL
TCP_READ_DATA : STRING;
TCP_WRITE_DATA : STRING;
END_VAR
VAR
MOVE8_ENO : BOOL;
MOVE8_OUT : STRING;
END_VAR

IF NOT(WRITE_DATA_SET) AND NOT(READ_READY) THEN
READ_READY := TRUE; (*set*)
END_IF;
MOVE8_OUT := MOVE(EN := NOT(WRITE_DATA_SET) AND READ_RESULT_READY, IN := TCP_READ_DATA, ENO => MOVE8_ENO);
IF MOVE8_ENO THEN
WRITE_DATA_SET := TRUE; (*set*)
TCP_WRITE_DATA := MOVE8_OUT;
END_IF;
END_PROGRAM


CONFIGURATION Config0

RESOURCE Res0 ON PLC
VAR_GLOBAL
TCP_WRITE_DATA AT %MB1001 : STRING;
TCP_READ_DATA AT %MB1000 : STRING;
END_VAR
TASK task0(INTERVAL := T#20ms,PRIORITY := 0);
PROGRAM instance0 WITH task0 : program0;
END_RESOURCE
END_CONFIGURATION


It probably needs a lot more thorough testing, but I thought I just share - maybe it helps pablofsmelo's problem 🙂

Cheers,
Chris


Quote 0 0
thiagoralves
This is an interesting approach. If you don't mind, I have some comments about it, to perhaps improve it in the future. The %MB and %MX variables don't have to be global on the program. As long as they are located at the address expected by gluevars, they will be passed along to your TCP server. That's what gluevars do, it "glues" user program variables located at specific addresses to OpenPLC buffers. Once the data is in the buffer, it can be read/written anywhere in the C code.

The second thing is to reuse the code in server.cpp to create your server, instead of creating it from scratch. This helps OpenPLC code to be concise and avoids spaghetti code (i.e. different implementations of the same thing on the same project). If there is a problem on the server, you know where to look and what to fix. I'm pushing today a new commit that adds initial EtherNet/IP support to OpenPLC. The EtherNet/IP server uses the server.cpp code to create its own server. You can take a look at how it was done and use the same approach on your implementation.

The last thing is, if you reuse the server.cpp code, you will be able to determine during runtime how many servers you want to create. This will remove the hardcoded restriction of only 3 servers. You can add this as a parameter on the user program, like %MB1000 is the number of servers. One more thing that I noticed just now is that your gluevars link %MB1000 to string_memory[1000] instead of string_memory[0].
Quote 0 0
cshbicos
Hi Thiago,

thanks for your feedback - really appreciate it!

Fiddling this into the server.cpp is certainly a good idea. I'll see if I can give that a try one of these days. However, I'm not quite clear on what you mean by not having to have a hardcoded restriction of servers. If I want to run three different TCP servers, I need to somehow maintain the variables for that (e.g. your run_dnp3, run_modbus...). At the moment, you always just spin up one server each, which simplifies that problem and you get away with one variable each. Or are you suggesting to completely get rid of the run_dnp3 equivalent and just use the %MB1000 variables as such a global "server is on" switch?

One of my concerns with using server.cpp is that in my design I expect every server instance to be able to handle multiple clients (e.g 2 clients on TCP server 1, 2 clients on TCP server 2). Thus I need to be able to write in a sequenced fashion to the same %MB register, and I need to somehow know all clients so if the PLC wants to send data out, all clients get that information. I'm not sure if that is easily possible in the single-client-thread design at the moment. I'll see if I can figure out a way 🙂

Finally, yes, I agree, I should be able to put %MB1000 into the local programs. But I get an error from matiec that B isn't a string type ("Bit size of data type is incompatible with bit size of location."). And I couldn't find a variable "type" that allows me to bind to a string for a local program. %MB, %MW, %MX all don't seem to work and I couldn't find any other letters that did work...  I'm not sure if the "global" variant is actually a bug on the matiec compiler to allow this or if this is intended, but it works. I couldn't find the IEC spec that this is based on anywhere, and reverse engineering that Bison/Flex that they use for parsing isn't as straightforward as I hoped. So if you know the correct data types to make this work, I'd be more then happy to get your guidance on this!

Anyway, I'll get back to you once I had some time to implement your proposed changes.

Cheers,
Chris


Quote 0 0
pablofsmelo
[QUOTE username=thiagoralves userid=4520672 postid=1308741154]This error means that you’re not being able to connect to the server you set up. Are you sure you changed that char *ip constant to the right one now that you’re running on the pi?[/QUOTE]

Thiago,

Aparently I can keep the connection to the server, because when I submit a tag on the system, I can see through the logs the values of that RFID tag.

When performing some tests, I noticed that the problem may be related to the update of the variable "int_input", being updated in "updateCustomIn()". When commenting the contents of the function below, openPLC returns the respective log:

Function:
[CODE]void updateCustomIn()
{
/*pthread_mutex_lock(&tagLock);

for (int x=2, i=0; i < NUM_MAX_TAG; x=x+2, i++) *int_input[x] = (uint16_t)tags_rfid.tag_id[i];
//if (int_input[x] != NULL) *int_input[x] = (uint16_t)tags_rfid.tag_id[i];

for (int x=3, i=0; i < NUM_MAX_TAG; x=x+2, i++) *int_input[x] = (uint16_t)tags_rfid.antenna_id[i];
//if (int_input[x] != NULL) *int_input[x] = (uint16_t)tags_rfid.antenna_id[i];

pthread_mutex_unlock(&tagLock);*/
}[/CODE]

Log:
From server😮k
Send:reader.events.register(21, event.tag.depart)

From server😮k
sirit_cfg = SETUP_DIO_ALL
Send:reader.events.register(21, event.dio.all)

From server😮k
Send😃io.in.all

From server😮k
Reader configured !
Passei por initcustomlayer no main
Setting main thread priority to RT
Locking main thread memory
Getting current time
Interactive Server: Client accepted! Creating thread for the new client ID: 9...
Interactive Server: waiting for new client...
Interactive Server: Thread created for client ID: 9
Interactive Server: client ID: 9 has closed the connection
Terminating interactive server connections
Interactive Server: Client accepted! Creating thread for the new client ID: 9...
Interactive Server: waiting for new client...
Interactive Server: Thread created for client ID: 9
Issued runtime_logs() command
Interactive Server: client ID: 9 has closed the connection
Terminating interactive server connections
127.0.0.1 - - [13/Oct/2019 10:44:22] "GET /runtime_logs HTTP/1.1" 200 -
Openning database
Disabling Modbus
Interactive Server: Client accepted! Creating thread for the new client ID: 9...
Interactive Server: waiting for new client...
Interactive Server: Thread created for client ID: 9
Interactive Server: client ID: 9 has closed the connection
Terminating interactive server connections
Interactive Server: Client accepted! Creating thread for the new client ID: 9...
Interactive Server: waiting for new client...
Interactive Server: Thread created for client ID: 9
Issued stop_modbus() command
Interactive Server: client ID: 9 has closed the connection
Terminating interactive server connections
Enabling DNP3 on port 20000

From the log you can see that it returns a "Disabling Modbus" and consequently I can no longer export modbus content to potential clients.

In a second test scenario, I kept the entire reader configuration implementation and forced a default value, with *int_input[3] = 53; according the function below:

[CODE]void updateCustomIn() {
*int_input[3] = 53;
}[/CODE]

In this case, it is not possible to run openPLC and the system returns the following log:

From server😮k
Reader configured !
Passei por initcustomlayer no main
Error connecting to OpenPLC runtime
127.0.0.1 - - [13/Oct/2019 10:56:22] "GET /runtime_logs HTTP/1.1" 200 -
Openning database
Disabling Modbus
OpenPLC Runtime is not running. Error: [Errno 111] Connection refused
Enabling DNP3 on port 20000
127.0.0.1 - - [13/Oct/2019 10:56:23] "GET /start_plc HTTP/1.1" 302 -
127.0.0.1 - - [13/Oct/2019 10:56:23] "GET /dashboard HTTP/1.1" 200 -
127.0.0.1 - - [13/Oct/2019 10:56:23] "GET /runtime_logs HTTP/1.1" 200 -
127.0.0.1 - - [13/Oct/2019 10:56:24] "GET /runtime_logs HTTP/1.1" 200 -
127.0.0.1 - - [13/Oct/2019 10:56:25] "GET /runtime_logs HTTP/1.1" 200 -
127.0.0.1 - - [13/Oct/2019 10:56:26] "GET /runtime_logs HTTP/1.1" 200 -
127.0.0.1 - - [13/Oct/2019 10:56:27] "GET /runtime_logs HTTP/1.1" 200 -

I confess I'm a little lost, because the same code developed from custom_layer.cpp works on a core I5 machine and only moving the *int_input variable, inside "UpdateCustomIn()", the system no longer responds as expected.

Attached is the source code for the player configuration.




 
Quote 0 0
thiagoralves
A couple notes:
1. Go to Settings on the OpenPLC webserver and make sure that the Modbus server is enabled. I bet it is disabled.
2. You shouldn't lock the updateCustomIn() with mutexes as it is already a protected segment.
3. You should not write or read from a pointer without checking first if it is a valid location. "*int_input[3] = 53" will get you in trouble, where "if (int_input[3] != NULL) *int_input[3] = 53" will not.
4. Before manipulating regions on the OpenPLC buffers (int_input, int_output, etc) make sure you have disabled them on the ignored vectors (ignored_int_inputs, ignored_int_outputs, etc), otherwise you will get conflicts with the device's hardware layer.
Quote 0 0