0
votes

I'm developing a multithreaded application, I need to instantiate n devices by modbus. So I created a controller (ServiceSM) that instantiates N threads (ServiceSlots).

The devices are varied so I had to create "drivers" for each type of device, one of the drivers uses the QModbusClient class, so I created a controller to manage the device type.

schema

To test the operation of the state machine and connection to the device, I made an example code to run in a graphical interface.

I deleted some snippets of code that do not matter to make it easier to understand


In the MD4040driver class When my code runs this section, the following messages appear. If I instantiate the DeviceDriver class in the graphical interface, it works perfectly, the problem occurs when I instantiate it inside a thread.

when calls

modbusDevice->connectDevice()

MD4040drive::sm_conn() - try connect - this my message Error:

QObject::connect: Cannot queue arguments of type 'QModbusDevice::State' (Make sure 'QModbusDevice::State' is registered using qRegisterMetaType().)

QObject: Cannot create children for a parent that is in a different thread. (Parent is QTcpSocket(0x24a6ce8), parent's thread is ServiceSlots(0xea66488), current thread is QThread(0x2418a78)

QObject: Cannot create children for a parent that is in a different thread. (Parent is QTcpSocket(0x24a6ce8), parent's thread is ServiceSlots(0xea66488), current thread is QThread(0x2418a78)

void MD4040drive::sm_conn()
{

    if (modbusDevice->state() != QModbusDevice::ConnectedState) {
        modbusDevice->setConnectionParameter(QModbusDevice::NetworkPortParameter, this->cfg.modbus.porta );
        modbusDevice->setConnectionParameter(QModbusDevice::NetworkAddressParameter, this->cfg.modbus.ip);
        modbusDevice->setTimeout( this->cfg.modbus.timeout );
        modbusDevice->setNumberOfRetries(this->cfg.modbus.retries);
        qDebug() << "MD4040drive::sm_conn() - try connect";
        if (!modbusDevice->connectDevice()) {
            qDebug()  << "Erro: " << modbusDevice->errorString();
        } else {
            qDebug()  << "Aguardando conexão...";
        }
    }
    else{
        //already connected...
        this->getDados_NO_TH();
    }
}

rest my code(parts)


devicedriverviewgui.h devicedriverviewgui.cpp

class DeviceDriverViewGUI : public QDialog
{
    Q_OBJECT
public:
    explicit DeviceDriverViewGUI(QWidget *parent = 0);
    ~DeviceDriverViewGUI();
private slots:
    void on_pbTry_clicked();

private:
    Ui::DeviceDriverViewGUI *ui;
    ServiceSlots *serviceSlot;

};

void DeviceDriverViewGUI::on_pbTry_clicked()
{
    Equip equip_try = Equip();

    serviceSlot = new ServiceSlots();        
    serviceSlot->setEquipamento(equip_try);
    serviceSlot->start();
}

serviceslots.h serviceslots.cpp

class ServiceSlots : public QThread
{
     Q_OBJECT
public:
    ServiceSlots();
    void run();
private:
    QTimer *timer;
    DeviceDriver *device;

private slots:
    void sm_getData();
    void device_response(bool boEnd);
};

void ServiceSlots::run()
{
    int e;
    eventLoop = new QEventLoop();
    timer = new QTimer();
    connect(timer, SIGNAL(timeout()),this, SLOT(sm_controler()));
    timer->start(TICK_SM_SLOT);
    this->device = new DeviceDriver();
    e = eventLoop->exec();
    qDebug() << "Exit loop"<< e;
}

void ServiceSlots::sm_controler()
{
    if(this->idleState){;}
    else{
        this->sm_getData();
        this->idleState = true;
    }
}

void ServiceSlots::sm_getData()
{
    connect(device,SIGNAL(end(bool)),this,SLOT(device_response(bool)));
    device->requestDeviceDriver(&this->equipamento,&this->next_date);
}

devicedriver.h devicedriver.cpp

class DeviceDriver : public QObject
{
    Q_OBJECT
public:
    DeviceDriver();
    void requestDeviceDriver(Equip *equip,QDateTime *date);
private:
    //Drivers de dispositivos..
    MD4040drive *md4040;
private slots:
    //Request data to driver...
    void request();
signals:
    void end(bool boEnd);
};

void DeviceDriver::request()
{
    connect(md4040,SIGNAL(end(bool)),this,SLOT(md4040_end(bool)));
    this->md4040->requestMD4040drive(&this->equip,&this->date);
}

DeviceDriver::DeviceDriver(){
    ----
    md4040 = new MD4040drive();
    ---
}

void DeviceDriver::requestDeviceDriver(Equip *equip, QDateTime *date){
    this->equip = *equip;
    this->date = *date;
    this->request();
}

md4040drive.h md4040drive.cpp

class MD4040drive : public QObject // QObject//public QObject  QRunnable  QThread
{
    Q_OBJECT
public:
    explicit MD4040drive(QObject *parent = 0);
    ~MD4040drive();
    void requestMD4040drive(Equip *equip,QDateTime *date);
private:
    void run();
    QModbusClient *modbusDevice;

private slots:
    void m_conn();
signals:
    void end(bool boRun);

};

MD4040drive::MD4040drive(QObject *parent): QObject(parent)
{
    modbusDevice = new QModbusTcpClient();
    connect(modbusDevice, &QModbusClient::stateChanged,this, &MD4040drive::onStateChanged);
}

void MD4040drive::requestMD4040drive(Equip *equip, QDateTime *date)
{
    this->equip = *equip;
    this->date = *date;
    this->run();
}


void MD4040drive::run()
{
    this->sm_conn();
}

void MD4040drive::sm_conn()
{

    if (modbusDevice->state() != QModbusDevice::ConnectedState) {
        modbusDevice->setConnectionParameter(QModbusDevice::NetworkPortParameter, this->cfg.modbus.porta );
        modbusDevice->setConnectionParameter(QModbusDevice::NetworkAddressParameter, this->cfg.modbus.ip);
        modbusDevice->setTimeout( this->cfg.modbus.timeout );
        modbusDevice->setNumberOfRetries(this->cfg.modbus.retries);
        qDebug() << "MD4040drive::sm_conn() - try connect";
        if (!modbusDevice->connectDevice()) {
            qDebug()  << "Erro: " << modbusDevice->errorString();
        } else {
            qDebug()  << "Aguardando conexão...";
        }
    }
    else{
        //already connected...
        this->getDados_NO_TH();
    }
}
2

2 Answers

2
votes

There are a few problems:

  1. You need to call qRegisterMetaType<QModbusDevice::State>() in main().

  2. You need to maintain parent-child relationships between all objects that are potentially moved to other threads as a group.

  3. The ServiceSlots and DeviceDriver classes seem to be unnecessary.

  4. The ubiquitous this-> is non-idiomatic C++. Don't write this-> unless you need to disambiguate a member from a local variable.

  5. Prefer to hold objects by value if they have the same lifetime as the parent object. Let the compiler generate memory management code for you!

  6. Leverage C++11.

Fist of all, let's have a helper SafeThread class that provides us with a thread that is safely destructible at any time:

class SafeThread : public QThread {
   Q_OBJECT
   using QThread::run;
public:
   using QThread::QThread;
   ~SafeThread() { quit(); wait(); }
};

The DeviceDriverViewGUI class can hold the drive and its thread by value:

class DeviceDriverViewGUI : public QDialog
{
   Q_OBJECT
public:
   explicit DeviceDriverViewGUI(QWidget *parent = nullptr);

private:
   Ui::DeviceDriverViewGUI ui;
   MD4040drive drive;
   SafeThread driveThread{this};
   Equip equipamento;
   QDateTime nextDate;

   Q_SLOT void on_pbTry_clicked();
};

Then, the pushbutton can be connected directly to the drive's thread context, and run the requestMD4040drive in the proper thread:

DeviceDriverViewGUI::DeviceDriverViewGUI(QWidget *parent) : QDialog(parent)
{
   ui.setupUi(this);
   //                                       vvvvvv -- gives the thread context
   connect(ui.pbTry, &QPushButton::clicked, &drive, [this]{
      Q_ASSERT(QThread::currentThread() == drive.thread()); // ensured by the thread context
      drive.requestMD4040drive(&equipamento, nextDate);
   });
   connect(&drive, &MD4040drive::end, this, [this](bool end){
      //...
   });
   drive.moveToThread(&driveThread);
   driveThread.start();
}

When done this way, you don't need any extraneous helper objects nor timers to queue requests. Qt handles all of it.

When passing Qt value classes to functions, pass them by const reference, not by pointer. The MD4040drive should look roughly as follows:

class MD4040drive : public QObject
{
   Q_OBJECT
public:
   explicit MD4040drive(QObject *parent = nullptr);
   void requestMD4040drive(Equip *equip, const QDateTime &date);
      Q_SIGNAL void end(bool boRun);
private:
   Equip *equip = nullptr;
   QDateTime date;
   QModbusTcpClient modbusDevice{this};
   Cfg cfg;

   Q_SLOT void onStateChanged();
   Q_SLOT void m_conn();
   void sm_conn();
   void getDados_NO_TH() {}
};

The implementation:

MD4040drive::MD4040drive(QObject *parent): QObject(parent)
{
   connect(&modbusDevice, &QModbusClient::stateChanged,this, &MD4040drive::onStateChanged);
}

void MD4040drive::requestMD4040drive(Equip *equip, const QDateTime &date)
{
   this->equip = equip;
   this->date = date;
   sm_conn();
}

void MD4040drive::sm_conn()
{

   if (modbusDevice.state() != QModbusDevice::ConnectedState) {
      modbusDevice.setConnectionParameter(QModbusDevice::NetworkPortParameter, cfg.modbus.porta );
      modbusDevice.setConnectionParameter(QModbusDevice::NetworkAddressParameter, cfg.modbus.ip);
      modbusDevice.setTimeout( this->cfg.modbus.timeout );
      modbusDevice.setNumberOfRetries(this->cfg.modbus.retries);
      qDebug() << "MD4040drive::sm_conn() - try connect";
      if (!modbusDevice.connectDevice()) {
         qDebug()  << "Erro: " << modbusDevice.errorString();
      } else {
         qDebug()  << "Aguardando conexão...";
      }
   }
   else{
      //already connected...
      getDados_NO_TH();
   }
}

The configuration class might look as follows - notice that the compiler will generate the necessary constructors and destructors for you:

struct Cfg {
   struct Modbus {
      int porta = 0;
      QString ip = QStringLiteral("127.0.0.1");
      int timeout = 1000;
      int retries = 2;
   } modbus;
};
0
votes

Make sure 'QModbusDevice::State' is registered using qRegisterMetaType()

Means you need to call qRegisterMetaType<QModbusDevice::State>(); before connecting signal/slot that would pass this type of parameter between threads.

And/or add Q_DECLARE_METATYPE(QModbusDevice::State) macro at global scope (never understood clearly which one is actually needed, putting both works for sure...).

See this post for more details: Emitting signals with custom types does not work