Search code examples
c++qtmockingautomated-testsuart

How can i mock a serial port UART linux


Preface

So basically im doing a project for a extracurricular activity and it involves having a microcontroller read some data from a CAN bus and then send that data over through a UART serial connection to a bananaPi Zero M2 thats currently running arch linux.

The microcontroller is probably an arduino of some kind(most likely a modified version of it), the problem resides with the constant change of the project, and since i want my code to survive longer than a year a part of that is creating tests, ive been looking for a way to emulate the serial connection that is made from the bananaPi (on file/dev /dev/ttyS0) to the microcontroller so that i dont have to constantly compile the code for the bananaPi set everything up just to check if "hello" is being correctly sent over the serial line. The thing is i havent found a way to sucessfully virtualize a serial port

Attempts

So i've looked a bit on options and i found socat, apparently it can redirect sockets and all kinds of connections and especially baud rates(although personally its not really that relevant for giving credence to the tests to my colleagues is of the most importance) So i spent a evening trying to learn three things at once and after a lot of problems and a lot of learning i came to this

void Tst_serialport::sanityCheck(){

    socat.startDetached("socat -d -d pty,rawer,b115200,link=/tmp/banana,  pty,rawer,b115200,link=/tmp/tango");
    sleep(1);

    _store = new store("/tmp/banana");

    QCOMPARE(_store->dev,"/tmp/banana");
}
void Tst_serialport::checkSendMessage(){
    QSerialPort tango;
    tango.setPortName("/tmp/tango");
    tango.setBaudRate(QSerialPort::Baud115200);
    tango.setDataBits(QSerialPort::Data8);
    tango.setParity(QSerialPort::NoParity);
    tango.setStopBits(QSerialPort::OneStop);
    tango.setFlowControl(QSerialPort::NoFlowControl);
    tango.open(QIODevice::ReadWrite);
    tango.write("Hello");
    tango.waitForBytesWritten();

    tango.close();

    QCOMPARE(_store->lastMessage,"Hello");
    
}


void Tst_serialport::closeHandle(){
    socat.close();
}
QTEST_MAIN(Tst_serialport)

The intent here being that in sanityCheck a fake serial device would be created on /tmp/banana and /tmp/tango that would redirect io between each other so that when _store started listening to banana and i sent a message to tango i would receive that same message inside the store object

The thing is the function that is waiting for messages, etc... isnt triggering even tough ive managed to work with it when i had an arduino plugged directly to my computer

before continuing im sorry that the code is kinda all messed up, im kinda new to both qt and c++, although i have some experience with C which made me use a lot of C stuff when i shouldve sticked with qt. Unfortunately i havent had much time to refactor everything to a more clean version of the code

Here's the other side

int store::setupSerial() {
    
    QSerialPort* serial= new QSerialPort();
    serial->setPortName(this->dev);
    serial->setBaudRate(QSerialPort::Baud115200);
    serial->setDataBits(QSerialPort::Data8);
    serial->setStopBits(QSerialPort::OneStop);
    serial->setParity(QSerialPort::NoParity);
    serial->setFlowControl(QSerialPort::NoFlowControl);
    if (!serial->open(QIODevice::ReadOnly)) {
        qDebug() << "Can't open " << this->dev << ", error code" << serial->error();
        return 1;
    }

    this->port = serial;
    connect(this->port, &QSerialPort::readyRead, this, &store::handleReadyRead);
    connect(this->port, &QSerialPort::errorOccurred, this, &store::handleError);
    

    return 0;   
}

store::store( char * dev, QObject *parent  ): QObject(parent){
    if (dev == nullptr){
        // TODO: fix this(use a better function preferably one handled by QT)
        int len = sizeof(char)*strlen(DEFAULT_DEVICE)+1;
        this->dev = (char*)malloc(len);
        strcpy(this->dev,DEFAULT_DEVICE);
    }
    //copy dev to this->dev
    else{
        int len = sizeof(char)*strlen(dev)+1;
        this->dev = (char*)malloc(len);
        strcpy(this->dev,dev);
    }


    setupSerial();
    
}


void store::handleReadyRead(){
    bufferMessage=port->readAll(); 
    serialLog.append(bufferMessage);

    //can be optimized using pointers or even a variable as a "bookmark" wether a int or pointer 
    lastMessage.append(bufferMessage);
    uint32_t size = (int)lastMessage[0] | (int)lastMessage[1] << 8 | (int)lastMessage[2] << 16 | (int)lastMessage[3] << 24;
    int8_t eof = 0x00;
    
    if((bool)((long unsigned int)lastMessage.size() == size+sizeof(size)+sizeof(eof))&& ((bool) lastMessage[lastMessage.size()-1] == eof)){
        parseJson();
        //clear lastMessage()
        lastMessage.clear();
    }
    
    

}
//... some other code here

If you're wondering whats the output or the result well


11:23:40: Starting /home/micron/sav/Trabalhos/2022-2023/FormulaStudent/VolanteAlphaQT/build-VolanteAlphaQT-Desktop-Testing/bin/VolanteAlphaQT_testes...
********* Start testing of Tst_serialport *********
Config: Using QtTest library 5.15.8, Qt 5.15.8 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 12.2.1 20230201), arch unknown
PASS   : Tst_serialport::initTestCase()
2023/02/15 11:23:40 socat[6248] N PTY is /dev/pts/2
2023/02/15 11:23:40 socat[6248] N PTY is /dev/pts/3
2023/02/15 11:23:40 socat[6248] N starting data transfer loop with FDs [5,5] and [7,7]
PASS   : Tst_serialport::sanityCheck()
FAIL!  : Tst_serialport::checkSendMessage() Compared values are not the same
   Actual   (_store->lastMessage): ""
   Expected ("Hello")            : Hello
   Loc: [../VolanteAlphaQT_1/test/tst_serialport.cpp(35)]
PASS   : Tst_serialport::closeHandle()
PASS   : Tst_serialport::cleanupTestCase()
Totals: 4 passed, 1 failed, 0 skipped, 0 blacklisted, 1005ms
********* Finished testing of Tst_serialport *********
11:23:41: /home/micron/sav/Trabalhos/2022-2023/FormulaStudent/VolanteAlphaQT/build-VolanteAlphaQT-Desktop-Testing/bin/VolanteAlphaQT_testes exited with code 1

As usual all per most of my questions its not very descriptive it basically just never triggers the signal ReadyRead which in turn causes last message to be blank

Conclusion / TL;DR

So what am i doing wrong? why is the ready read signal not being triggered? Is there a better way to simulate/mock a serial connection?


Solution

  • Well, I found the solution.

    Apparently it wasn't a socat problem, the ready signal is way slower than I had in mind and when I slept it actually froze the process. Due to the ready signal taking some time even after the buffer itself being ready, the QCOMPARE came right after the "unfreeze" making the stall useless.

    The actual solution was rather simple I placed a _store->waitForReadyRead(); so I could wait for the signal to be sent without freezing the process.