I believe my problems have shifted from the scope of this question. With help from Kuzulis I have implemented Write/Read functions to successfully send and read serial messages consistently. Kuzulis recommended using the Synchronous blocking communication pattern, however it was later decided that the Asynchronous Non-Blocking method would be best fit for my application.
My implementation closely follows the "Master" example provided with the QSerialPort source files.
I use CurrentStatus
to iterate through a QList of Device
objects. For each Light in the Device list, I format an 8 Byte message to query the current status of the device (ON/OFF).
void Device::currentStatus(QList<Device *> * deviceList){
QString devID, updateQry;
int devStatus, updateStatus;
updateStatus=0;
QSqlQuery query;
for(int i=0; i<deviceList->size(); i++){
if(deviceList->at(i)->type == "Light"){
devStatus = deviceList->at(i)->status;
devID = deviceList->at(i)->deviceID;
QByteArray msg;
msg.resize(8);
msg[0] = 0x02;
msg[1] = 0x62;
msg[2] = 0x00;
msg[3] = 0x00;
msg[4] = 0x00;
msg[5] = 0x05;
msg[6] = 0x19;
msg[7] = 0x00;
msg.replace(2, 3, QByteArray::fromHex( devID.toLocal8Bit() ) );
qDebug() << "Has device " << deviceList->at(i)->name << "Changed?";
emit writeRequest(msg);
if(devStatus!=updateStatus){
qDebug() << deviceList->at(i)->name << " is now: " << updateStatus;
updateStatus = !updateStatus;
}
}
}
}
In the Device class constructor, I connect the signals and slots:
Device::Device(){
serialTimer.setSingleShot(true);
QObject::connect(&serial, SIGNAL(readyRead()),
this, SLOT(handleResponse()));
QObject::connect(&serialTimer, SIGNAL(timeout()),
this, SLOT(processTimeout()));
QObject::connect(this, SIGNAL(writeRequest(QByteArray)),
this, SLOT(writeSerial(QByteArray)));
}
After the message to send in currentStatus
has been prepared, emit writeRequest(msg);
is called. This dispatches a signal that is connected to the slot writeRequest
. writeRequest is used to setup and actually write the message to the serial port.
void Device::writeSerial(const QByteArray &msg){
if (serial.portName() != "COM3") {
serial.close();
serial.setPortName("COM3");
if (!serial.open(QIODevice::ReadWrite)) {
processError(tr("Can't open %1, error code %2")
.arg(serial.portName()).arg(serial.error()));
return;
}
if (!serial.setBaudRate(QSerialPort::Baud19200)) {
processError(tr("Can't set rate 19200 baud to port %1, error code %2")
.arg(serial.portName()).arg(serial.error()));
return;
}
if (!serial.setDataBits(QSerialPort::Data8)) {
processError(tr("Can't set 8 data bits to port %1, error code %2")
.arg(serial.portName()).arg(serial.error()));
return;
}
if (!serial.setParity(QSerialPort::NoParity)) {
processError(tr("Can't set no patity to port %1, error code %2")
.arg(serial.portName()).arg(serial.error()));
return;
}
if (!serial.setStopBits(QSerialPort::OneStop)) {
processError(tr("Can't set 1 stop bit to port %1, error code %2")
.arg(serial.portName()).arg(serial.error()));
return;
}
if (!serial.setFlowControl(QSerialPort::NoFlowControl)) {
processError(tr("Can't set no flow control to port %1, error code %2")
.arg(serial.portName()).arg(serial.error()));
return;
}
}
qDebug() << "Message written";
this->msgRequest = msg;
serial.write(msgRequest);
serialTimer.start(400);
}
After setting up the serial port, I save the current message to msgRequest
. This may have to be used to resend the message if there is an error. After serial.write()
is called, I setup a timer for 400ms. Once this timer expires, I check what was read from the serial port.
handleResponse()
is a slot that is called everytime QSerialPort emits the readyRead()
signal. readyRead()
appends any available data to the QByteArray response
.
void Device::handleResponse(){
response.append(serial.readAll());
}
After 400ms, serialTimer (one shot Timer) will emit a timeout()
signal. serialTimer
was started right after writing our requested message to the serial port. processTimeout()
is where we finally check the response received from the PowerLinc Modem after sending our message. When messages are sent to the INSTEON PowerLinc Modem (PLM), the PLM echoes back the message and appends either 0x06 (Positive ACK) or 0x15 (NACK). In processTimeout()
I check to make sure the last byte received is the ACK byte, if not - resend our originally requested message.
void Device::processTimeout(){
qDebug() << "Read: " << response.toHex();
int msgLength = this->msgRequest.length();
if(response.at(msgLength)!=0x06){
qDebug() << "Error, resend.";
emit writeRequest(msgRequest);
}
response.clear();
}
I used the Serial Port Monitor 4.0 (Eltima Software) to verify the write and read transactions on the serial port. Below, you can see the log printout for 1 sample transaction.
20:44:30:666 STATUS_SUCCESS 02 62 1d e9 4b 05 19 00 <--- Send
20:44:30:669 STATUS_SUCCESS 02 62 1d e9 4b 05 19 00 06 <--- Receive
20:44:30:875 STATUS_SUCCESS 02 <--- Receive
20:44:30:881 STATUS_SUCCESS 50 1d e9 4b 1e da f7 21 00 ff <--- Receive
For 20 sends, I received the same response. Thus, I can safely say my issues with inconsistent data arrival have been resolved. Now I am struggling with multiple write requests, but I believe that is a separate question to be investigated. I appreciate everyone's support.
I've solved my problems, so I post here the correct code in case someone needs similar stuff.
Open Port
int USB = open( "/dev/ttyUSB0", O_RDWR| O_NOCTTY );
Set parameters
struct termios tty;
struct termios tty_old;
memset (&tty, 0, sizeof tty);
/* Error Handling */
if ( tcgetattr ( USB, &tty ) != 0 ) {
std::cout << "Error " << errno << " from tcgetattr: " << strerror(errno) << std::endl;
}
/* Save old tty parameters */
tty_old = tty;
/* Set Baud Rate */
cfsetospeed (&tty, (speed_t)B9600);
cfsetispeed (&tty, (speed_t)B9600);
/* Setting other Port Stuff */
tty.c_cflag &= ~PARENB; // Make 8n1
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;
tty.c_cflag &= ~CRTSCTS; // no flow control
tty.c_cc[VMIN] = 1; // read doesn't block
tty.c_cc[VTIME] = 5; // 0.5 seconds read timeout
tty.c_cflag |= CREAD | CLOCAL; // turn on READ & ignore ctrl lines
/* Make raw */
cfmakeraw(&tty);
/* Flush Port, then applies attributes */
tcflush( USB, TCIFLUSH );
if ( tcsetattr ( USB, TCSANOW, &tty ) != 0) {
std::cout << "Error " << errno << " from tcsetattr" << std::endl;
}
Write
unsigned char cmd[] = "INIT \r";
int n_written = 0,
spot = 0;
do {
n_written = write( USB, &cmd[spot], 1 );
spot += n_written;
} while (cmd[spot-1] != '\r' && n_written > 0);
It was definitely not necessary to write byte per byte, also int n_written = write( USB, cmd, sizeof(cmd) -1)
worked fine.
At last, read:
int n = 0,
spot = 0;
char buf = '\0';
/* Whole response*/
char response[1024];
memset(response, '\0', sizeof response);
do {
n = read( USB, &buf, 1 );
sprintf( &response[spot], "%c", buf );
spot += n;
} while( buf != '\r' && n > 0);
if (n < 0) {
std::cout << "Error reading: " << strerror(errno) << std::endl;
}
else if (n == 0) {
std::cout << "Read nothing!" << std::endl;
}
else {
std::cout << "Response: " << response << std::endl;
}
This one worked for me. Thank you all!
Best Answer
The .NET framework even has a proper SerialPort class, which will be just fine for your application.