PDA

View Full Version : QtSerialPort--how to recognize end of message



davethomaspilot
27th November 2012, 19:17
I've been trying out the SerialPort class. It seems easy to use, but I'm struggling with how to deal with a protocol I'm stuck with, without using some kind of time-out mechanism.

The protocol consists of a variable number of fixed length (sixteen byte) records. Each record has an stx (0x02) in the first byte and a etx (0x03) in the last byte. The last record in the message will have an ascii "X" in byte 7.

My Qt C++ code needs to receive these messages and update widgets based on the message content. Messages will be sent to my code asynchronously and infrequently, typically, a set of about ten, sixteen byte records every couple of minutes.

I've implemented code in Tcl that works well, but uses a blocking read with a time-out mechanism. The time out is set to several seconds so that the data that's returned contains all the records for the message. A check for the "X" in byte 7 of the last record is done, but no recovery is attempted if it's something else. It seems to always be correct.

There's no GUI in the Tcl implementation, so I don't have to worry about it becoming non-responsive during the blocking read.

It seems that doing a blocking read is highly undesirable in Qt, so I'm wondering if there is a better approach? I could keep checking SerialPort::bytesAvailable() and do a read, append byte data, and start a timer each time data is read. When the timer expires, the message is assumed to be complete and a check could be done for the "X" character like I do in Tcl.

Does this seem like a reasonable approach? Do I need to do something like "update" in the loop where I'm checking bytesAvailable to keep the GUI responsive?

Thanks,

Dave Thomas

Lesiok
28th November 2012, 07:02
This is classical multi-layer transport. Serial port and QtSerialPort library is a physical layer with stream of bytes. You need to create a second layer that is turning the stream of bytes to the list of packages received. Each package can generate a ready signal to the presentation layer. The presentation layer uses the finished packages. In a case like yours (one-way transmission) the first two layers can be placed in a separate thread.

kuzulis
28th November 2012, 08:20
There is a no bad description of the principle of the development of protocols:

http://www.eventhelix.com/realtimemantra/patterncatalog/protocol_layer.htm

but it is not tied to the signals / slots.

As others have said Lesiok good practice to use ISO / OSI model, for example
can be implement this (pseudo code):

Custom protocol:



packet[<header><body><trailer>]

where:

header[<start><length>]

- start is 1 byte = 0x02
- length is 1 byte from 0 to 255

body[<data>]

- data is data array from 0 to 255 bytes

trailer[<end>]

- end is 1 byte = 0x03


Code:


class Layer : public QIODevice
{
Q_OBJECT
public:
Layer() : lower(0), upper(0) {}
protected:
Layer *lower;
Layer *upper;
};

PresentationLayer : public Layer
{
Q_OBJECT
signals:
void timeout();

public:
PresentationLayer() : process(false) {
lower = new SerialPort();
connect(lower, SIGNAL(readyRead()), this, SLOT(onFromLower());
timer = new QTimer(100); // 100 msec waiting
timer->setSingleShot(true);
connect(timer, SIGNAL(timeout()), this, SLOT(onWaitPacketTimeOut());
}
protected:
virtual quint64 readData() {
return data.size();
}
private slots:
void onFromLower() {
if (!process)
timer->start();

queue.append(lower->readAll());

while (queue.size() > 0) {
char start = queue.at(0);
if (start != START_CONSTANT) {
queue.removeFirst();
continue;
}

char length = queue.at(1);
if (queue.size() < (1 + 1 + length + 1)) // start + length + data + end
// not the whole package was received.
break;

char end = queue.at(1 + length);
if (end != END_CONSTANT) {
queue.removeFirst();
continue;
}

// ok, all checks success, packed received,
// need take data and transfer to upper Level
timer->stop();
queue.removeFirst(); // remove start field
queue.removeFirst(); // remove length field
queue.removeLast(); // remove end field
// done
data = queue.toQByteArray();
queue.clear();
emit readyRead();
}
}

void onWaitPacketTimeOut() {
process = false;
queue.clear();
emit timeout();
}

QQueue<char> queue;
QTimer *timer;
bool process;
QByteArray data;
}


Or something else to come up with ...

davethomaspilot
3rd December 2012, 13:31
Thanks for the replies! I didn't mean to ignore them for so long. I didn't get email notification I expected, so I thought no one had bothered to respnd.

I don't think this a classical multi-layer transport, but a little more background is needed, I think.

I left out the "stickler" in the protocol. Each record includes a "block check character", which is a checksum of the preceeding fields in the record. Since this "bcc" (block check character) field is binary and could contain either the STX or ETX character (and probably WILL about every 256 records), I don't think I can use either the STX or ETX field to figure out record boundaries. So, I think I have to do "strange and unnatural acts" to process the message.

I think the only way I can know the entire message has been sent is to reset a timer after each character is received. When the timer expires, I'll process all the bytes received since the last valid message and check if the new bytes appear be a valid message.

The check for valid message would consist of

1) integral number of 15 byte records in the message (message bye length modulo 15 is zero)
2) first byte of every 15 byte record is STX
3) last byte of every 15 byte record is ETX
4) bcc (14th byte of each record) is checksum


I expect to seem some junk data due to cable plug/unplug, but otherwise there probably will be no checksum errors.

So, back to my question. I still think I need something like a blocking read that times out after a second or two to trigger processing of the bytes received. So, I'd think I need to reset the single shot timer in "onFromLower" handler.

I think the it be more straight forward to do something like this in the readyRead handler:

time_out= QTime::currentTime().addSecs(1);
while( QTime::currentTime() < time_out )
{

msg.append(serial->readAll);
// process events (forgot the Qt call)
}

Or, is there a reason using a slot for the single shot timer is better?

Thanks!

Dave Thomas

kuzulis
3rd December 2012, 17:38
Or, is there a reason using a slot for the single shot timer is better?
I do not know, try it.

There is another way: use blocking I / O in a separate thread, see new examples BlockingMaster/BlockingSlave.