Modbus Data Logger


Thread Starter


Hello Everyone,

I am currently working on a project to log data from two serial ports. The serial ports are connected to two PLCs (Scada devices). Serial port-1 is connected to the Master PLC and Serial Port-2 is connected to the Slave PLC. I have a USB to two port serial converter device connected to my laptop.

I want to create a "C" program in Linux that can take data from Port1 (Master) log it in the proper format(MODBUS ASCII/RTU mode) into a file on my laptop, send it to Port2 (Slave). This process should happen the other way around also. I was successful in creating a program to act as a null modem. But I am clueless as to how I can get the data and store it in the right format in a file.

Can you guys provide me with suggestions and guide me in the right direction on how to do this? I did read the documents on the modbus website and I know how to detect the starting and end of characters in ASCII and RTU mode. But I am still confused.
I think you would need to do something like this:

1) Detect the start of the message and reset the message capture buffer.

2) Accumulate characters until the end of the message is detected.

3) Perform any required checksum calculations.

4) Extract the function code.

5) Use the function code to decide how to extract the rest of the message.

6) Write the message to disk. Depending on what you are doing, you might want to either just write it to a text file, or save it in a database (MySQL, PostgreSQL, SQLite, DBM, etc.).

While you are doing all this, you also keep passing data through the serial ports. That means your program is going to have to operate asynchronously, and not "stall" while waiting for something to happen.

I think you will also have to use command line parameters to tell your program whether to expect RTU or ASCII. The data format is different in each case, but extracting the data is pretty straightforward. There are lots of Modbus C libraries on Sourceforge if you want some examples.

It is hard to be more specific than this because your question is so general. If you have a more focused question, I will try to answer it.
Thank you for answering Griffin.

I plan to make my program work in only one mode at a time. So separate programs for ASCII and RTU modes.

I will be storing the captured data from Port1(Master) in one text file and data from Port 2(Slave) in a separate text file.

I did happen to check out some examples like libmodbus and freemodbus. But they seem a bit advanced and I am new to this stuff. I haven't seen anybody try to log data. I would like to see such implementations and design my program.

The challenge that I have right now is to manage the tasks to happen asynchronously. My code philosophy is to detect a ":" and the end of the packet "CR/LF"(RTU mode). I am not thinking about Function codes and CRC checks at this point.

Sorry I might not be very specific. I am confused that's all. But glad that you are helping.
To make your program asynchronous, you just have to make sure that nothing in your program halts in the middle of something for an extended period of time. That is, you need to run in a loop that does whatever work needs to be done, and then returns to the point where it is waiting for more input. You would then either "sleep", or use an interrupt. It is similar to writing a GUI event loop. For a really simple example:

1) Check if any characters are waiting at either of the serial port inputs. If none are waiting, go back to sleep (for e.g. another 10 milliseconds).

2) Take any new characters and add them to a processing buffer (you'll probably need a separate processing buffer for each serial port).

3) Check the processing buffers to see if you have a complete message in either of them.

4) If a complete message is available, process it (including logging it to disk, and forwarding it out the other serial port).

5) Return to step 1.

There are more sophisticated ways of doing this using things like threads, but if you were prepared to use those methods you wouldn't be asking these sorts of questions.

For Modbus ASCII, there is an explicit end of message character sequence. Modbus/RTU doesn't have one and instead relies on timing. However, what you can do is attempt to decode the message and see if it is all there. Since the message includes the number of data bytes expected, you will know how long it is supposed to be and can tell from that when you have all of it.

By the way, this sort of thing is really easy to do in Python, if you happen to know that language.
I am gonna take your suggestions and implement this.

I don't know python at all only C and C++. This code will eventually have to work on an embedded system so I am sticking with "C".

Griffin do u happen to know any examples that are out there that should help? I know you mentioned about it in your first port.

I did something like this once, but with a twist that may make your job easier.

In my case, I had a master and slave PLC device connected via RS-232 serial cable.

But I used my two serial ports as "snoopers" only. I cut open the RS-232 cable and used alligator clips to "snoop" the TxD lines from both PLC's plus the GND line. I did not break the connection, though.

On my snooper computer COM1, I connected my RxD and GND to the Master PLC's TxD and GND.... and on my snooper computer COM2, I connected RxD to the Slave PLC's TxD.

COM1 would listen to (and log) data from the Master; and COM2 would listen to (and log) data from the Slave.

This eliminated the need to have your program receive and re-transmit data. You will only need to listen and log data from each of your COM ports.

The reason that this works is because a single RS-232 TxD line is capable of driving two RS-232 RxD lines.

Hope this helps!
For examples of Modbus communications, you might want to look at the following:

This is a C library on Sourceforge.

This is a C++ library which is part of PVBrowser. It is C++, but it isn't doing anything too obscure, so it ought to be easy to use as a reference for a C program.

Both should also show you how to use the serial port (with Unix/Linux, a serial port is just a file).

I'm assuming this is what you were looking for. The rest of the program will be fairly simple C. Look up "sleep" to see the parameters for that.

Just take the steps that I suggested in the previous message and start adding more detail. Then start replacing the description with actual C code. It will become more obvious once you get started.
Funny, I actually thought of doing that but never got to making the cables. Now that I know it worked for you then I will give it a try.

In which language did you implement this? If it is "C" then I would love to take a look at your code.

Apart from data acquisition I want to store the data as proper MODBUS packets(ASCII and RTU modes). I would like to create a routine in my code that says "get Modbus Packet". If a person had to call this routine they should be able to get the packet and do anything with it.

Thank you for answering my question.
A variation of this is to "diode auctioneer" the two RS232 TxDs into one Com Port and then separate the two by a 3 byte space timing in software. I have created a Win32 Console "snooper" application that runs on a Laptop with only one serial port. It saves to file in HEX or ASCII which is specified at start up.
Thank you for the link.

I implemented this process and it is working fine. But now I am trying to figure out how to do routines for "getModbus packet". This is for a programmer to take a Modbus packet by calling this routine and manipulate it.

Also thank you Griffin for the links to libmodbus and pvbrowser. You guys are all really helpful. It is strange that no body has taken the approach that I am taking.
I justed wanted to post the code that I have so far and get some feedback. Go easy on me, I am not a pro-programmer.</b>

#include <stdlib.h>
#include <stdio.h> /* Standard input/output definitions */
#include <string.h> /* String function definitions */
#include <unistd.h> /* UNIX standard function definitions */
#include <fcntl.h> /* File control definitions */
#include <errno.h> /* Error number definitions */
#include <termios.h> /* POSIX terminal control definitions */
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>

#define P1 "/dev/ttyS0"
#define P2 "/dev/ttyS1"

#define P1log "/home/vpt/Dropbox/DataLogger/LinuxSerial/Serial/SP1.dat"
#define P2log "/home/vpt/Dropbox/DataLogger/LinuxSerial/Serial/SP2.dat"

#define deadtime

#define MB_ASCII_STARTBIT ':' /*!< Default : character for Modbus ASCII. */
#define MB_ASCII_ENDBIT_CR '\r' /*!< Default CR character for Modbus ASCII. */

#define MB_ASCII_ENDBIT_LF '\n' /*!< Default LF character for Modbus ASCII. */

#define MB_ASCII_SIZE_MIN 3 /*!< Minimum size of a Modbus ASCII frame. */

#define MB_ASCII_SIZE_MAX 256 /*!< Maximum size of a Modbus ASCII frame. */
#define MB_RTU_SIZE_MIN 4 /*!< Minimum size of a Modbus RTU frame. */

#define MB_RTU_SIZE_MAX 256 /*!< Maximum size of a Modbus RTU frame. */

#define BAUDRATE B9600
#define _POSIX_SOURCE 1 /* POSIX compliant source */
#define FALSE 0
#define TRUE 1

/*int get_PKT();//Prototype Function for Getting a MODBUS packet
int send_PKT();//Prototype Function for Sending a MODBUS packet
int mb_PKT();//Prototype Function for MODBUS packet

int getmodbus_ASCII();
int getmodbus_RTU();

char P1_buffer[255], P2_buffer[255];

fpos_t pos;

FILE *P1out_file, *P2out_file;

int SP1, SP2, SP1_log, SP2_log, c, res, SP1_res, SP2_res;
struct termios oldtio1,oldtio2,newtio1, newtio2;
char SP1_buffer[255], *SP1_bufptr;
char SP2_buffer[255], *SP2_bufptr;
int stop=FALSE;
char hello[6] = "Hello";

FILE *SP1out_file = NULL, *SP2out_file = NULL;

SP1 = open (P1, O_RDWR | O_NOCTTY | O_NDELAY);
SP2 = open (P2, O_RDWR | O_NOCTTY | O_NDELAY);

SP1_log = open (P1log, O_RDWR | O_NOCTTY | O_NDELAY);
SP2_log = open (P2log, O_RDWR | O_NOCTTY | O_NDELAY);

if ((SP1 <0)&&(SP2<0))
perror("Unable to Open & Access ports!");

tcgetattr(SP1,&oldtio1); /* save current port settings */

/*Assign Baudrate and other settings*/
bzero(&newtio1, sizeof(newtio1));
//newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
newtio1.c_cflag = BAUDRATE | CS8 | CLOCAL | CREAD;
newtio1.c_iflag = IGNPAR;
newtio1.c_oflag = 0;

/* set input mode (non-canonical, no echo,...) */
newtio1.c_lflag = 0;

newtio1.c_cc[VTIME] = 0; /* inter-character timer unused */
newtio1.c_cc[VMIN] = 1; /* blocking read until 1 charis received */

/*Assign Baudrate and other settings*/
bzero(&newtio2, sizeof(newtio2));
//newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
newtio2.c_cflag = BAUDRATE | CS8 | CLOCAL | CREAD;
newtio2.c_iflag = IGNPAR;
newtio2.c_oflag = 0;

/* set input mode (non-canonical, no echo,...) */
newtio2.c_lflag = 0;

newtio2.c_cc[VTIME] = 0; /* inter-character timer unused */
newtio2.c_cc[VMIN] = 1; /* blocking read until 1 charis received */

tcflush(SP1, TCIFLUSH); /*Flush the port of old data*/

tcflush(SP2, TCIFLUSH);

while (!stop) { /* loop for input */
/* Port 1 ------> Port 2 */
SP1_res = read(SP1,SP1_buffer,1); /* returns after 1 char has been recieved */
if (SP1_res > 0) {
SP1_bufptr = malloc(SP1_res + 1);
memcpy(SP1_bufptr,SP1_buffer, SP1_res);
SP1_bufptr[SP1_res] = '\0';

if(SP1_bufptr[0] == MB_ASCII_ENDBIT_CR || SP1_bufptr[0] == MB_ASCII_ENDBIT_LF)//Check of End packet CR and LF
write(SP1_log, "\n", SP1_res);
write(SP1_log, SP1_buffer, SP1_res);
write(SP1_log, SP1_buffer, SP1_res);

write(SP2, SP1_buffer, SP1_res);
printf("Buffer 1 Ptr:%x Read 1:%d\n", SP1_bufptr, SP1_res); /*Print the Buffer and characters read*/

} // end if (SP1_res > 0)

/* Port 2 ------> Port 1 */
SP2_res = read(SP2,SP2_buffer,1); /* returns after 1 char has been recieved */
if (SP2_res > 0) {
SP2_bufptr = malloc(SP2_res + 1);
memcpy(SP2_bufptr,SP2_buffer, SP2_res);
SP2_bufptr[SP2_res] = '\0';

if(SP2_buffer[0] == MB_ASCII_ENDBIT_CR || SP2_buffer[0] == MB_ASCII_ENDBIT_LF)
write(SP2_log, "\n", SP2_res);
write(SP2_log, SP2_buffer, SP2_res);
write(SP2_log, SP2_buffer, SP2_res);

write(SP1, SP2_buffer, SP2_res);
printf("Buffer 2 Ptr:%x Read 2:%d\n", SP2_bufptr, SP2_res); /*Print the Buffer and characters read*/

} // end if (SP2_res > 0)

} // end while(1)


int getmodbus_ASCII()

if( P1out_file == NULL)
if( (P1out_file = fopen("SP1.txt","rb")) != NULL) //Open file for writing SP1 data
fread( P1_buffer, sizeof(char), 256, P1out_file );
if( fgetpos( P1out_file, &pos))
perror("error in getting the file position");
if (fsetpos(P1out_file,&pos)!=0)
perror("Error in File position");
fread( P1_buffer, sizeof(char), 256, P1out_file );


int getmodbus_RTU(){


These are the routines that are a mystery to me.

int get_PKT();//Prototype Function for Getting a MODBUS packet
int send_PKT();//Prototype Function for Sending a MODBUS packet
int mb_PKT();//Prototype Function for MODBUS packet
As for manipulating the Modbus packets, that is what the links to libmodbus and pvbrowser were for. There is code in there for extracting and creating Modbus packets. The pvbrowser code looks easier to follow, so you might want to read that one in detail first.

Of course you could just *use* those those drivers instead of writing your own, but that would mean that your program would have to structured to suit the way they work.

You said you preferred to do this in C rather than in Python, because C is what you know. I can understand that, but it means that you have to do the extra work involved in using C.

My suggestion would be to print out the pvbrowser Modbus code and start marking up the print out with a pencil as you figure out what each piece does. You might have a different work flow than that, but that is how I would approach the problem.

Alternatively, you can just write your own code. The protocol itself is very simple once you understand it. You will want to have the Modbus specs beside you as a reference to understand what is going on. They are very well written and have examples, but you need to take a bit of time to understand them. Basically, you are just extracting binary data from a string or array and interpreting it. I will give you an example with Modbus/TCP because I am more familiar with that, but you can adapt that to RTU and ASCII.

For a Modbus/TCP client request message,

A) The first two bytes are the transaction ID.

B) The next two are always 0 (zero).

C) The next two are the length.

D) The next byte is the unit ID.

E) The next byte is the function code.

What follows next depends on what the function code was.

F) If it was a request for 1, 2, 3, or 4, the next two bytes have to be address, and the following two have to be quantity.

G) If it was a request for 5 or 6, next two bytes have to be address, and the following two have to be message data.

H) etc.

There are addition rules for other function codes. You also have to check the data to see if it is consistent.

That deals with a request. You also have to do something similar for responses. It is a good idea to draw out a summary of each of the cases you have to deal with (there are only a few valid combinations).

Something you have to take into account in all this is byte order. That doesn't matter for pure byte values, but 2 byte integers have to deal with "endian" issues. The spec says: "MODBUS uses a 'big-Endian' representation for addresses and data items. This means that when a numerical quantity larger than a single byte is transmitted, the most significant byte is sent first."

The reason this matters is that some microprocessors are big-endian, while others are little-endian. Intel happens to be little-endian, which is the opposite of what Modbus happens to use (big-endian is the standard format for most network protocols - which is why it is also sometimes called "network order"). That means that anything that needs to be interpreted as a 2 byte integer needs to have the bytes swapped around before you attempt to use it as a actual number.

The above is for the TCP and RTU message format (RTU also adds a CRC check). ASCII is similar, except the data is encoded in hex ASCII as pairs of characters instead of pure binary bytes (one pair of hex characters is equivalent to one binary byte). That adds a bit of additional complication because you need to check if each character is valid hex before attempting to convert it.

I think this should get you started. I would suggest starting with just implementing 1 simple function code (e.g. function 6). Once you have that working you can add more function codes.