Sur cette page

Commande personnalisée Modbus

Cet exemple montre comment gérer les codes de fonction Modbus personnalisés.

Cet exemple joue à la fois le rôle de client et de serveur Modbus au sein d'une même application. La connexion entre les deux est établie via Modbus TCP. Il sert à envoyer et à recevoir des requêtes Modbus personnalisées et ajuste ses états internes en fonction de la requête et de la réponse.

L'objectif principal de cet exemple est de fournir un code de démonstration expliquant comment implémenter un client ou un serveur Modbus gérant des codes de fonction Modbus personnalisés.

Fenêtre « Commandes Modbus personnalisées » avec les tables de registres serveur et client, codes de fonction personnalisés 0x41 (lecture) et 0x42 (écriture)

Codes de fonction Modbus définis par l’utilisateur

Le protocole Modbus prend en charge les codes de fonction compris dans les plages 1 - 127 (0x01 - 0x7F HEX). La plupart des codes de fonction sont clairement définis et documentés publiquement. Cependant, deux plages peuvent être utilisées pour des fonctions définies par l'utilisateur : 65 - 72 (0x41 - 48 HEX) et 100 - 110 (0x64 - 0x6E HEX). L'utilisateur peut sélectionner des codes de fonction dans ces plages et les traiter de manière personnalisée.

Cette application utilise le code de fonction 65 (0x41 HEX) pour implémenter la commande CustomRead et le code de fonction 66 (0x42 HEX) pour implémenter la commande CustomWrite. Dans cet exemple, les commandes personnalisées sont utilisées simplement pour lire et écrire dans le fichier Holding Registers.

Envoi de commandes Modbus personnalisées

Les commandes Modbus personnalisées sont envoyées à l'aide de la méthode QModbusClient::sendRawRequest(). Cette méthode nécessite de créer un objet QModbusRequest avec le code de fonction souhaité et une liste d'arguments qui seront encodés dans un QByteArray:

    QModbusDataUnit unit {
        QModbusDataUnit::HoldingRegisters,
        ui->startAddress->value(),
        qMin(ui->numberOfRegisters->currentText().toUShort(),
            quint16(10 - ui->startAddress->value())) // ne pas dépasser 10 entrées
    };

   for (qsizetype i = 0, total = unit.valueCount(); i < total;++i)
        unit.setValue(i, m_model->m_registers[i + unit.startAddress()]);

    const quint8 byteCount = quint8(unit.valueCount() * 2);
    QModbusRequest writeRequest {
        QModbusPdu::FunctionCode(ModbusClient::CustomWrite),
        quint16(unit.startAddress()),
        quint16(unit.valueCount()), byteCount, unit.values()
    };

La méthode QModbusClient::sendRawRequest() renvoie un objet QModbusReply qui peut être utilisé pour vérifier la présence d'erreurs comme d'habitude :

    if (auto *reply = m_client.sendRawRequest(writeRequest, ui->serverAddress->value())) {
        if (!reply->isFinished()) {
            connect(reply, &QModbusReply::finished, this, [this, reply]() {
                if (reply->error() == QModbusDevice::ProtocolError) {
                    statusBar()->showMessage(tr("Write response error: %1 (Modbus exception: 0x%2)")
                        .arg(reply->errorString()).arg(reply->rawResult().exceptionCode(), -1, 16),
                        5000);
                } else if (reply->error() != QModbusDevice::NoError) {
                    statusBar()->showMessage(tr("Write response error: %1 (code: 0x%2)").
                        arg(reply->errorString()).arg(reply->error(), -1, 16), 5000);
                }

                reply->deleteLater();
                }
            );
        }
    } else {
        statusBar()->showMessage(tr("Write error: ") + m_client.errorString(), 5000);
    }

Serveur Modbus personnalisé

Le serveur personnalisé est dérivé de la classe ` QModbusTcpServer `. Il redéfinit la méthode ` QModbusServer::processPrivateRequest()`.

class ModbusServer : public QModbusTcpServer
{
    Q_OBJECT
    Q_DISABLE_COPY_MOVE(ModbusServer)

public:
    ModbusServer(QObject *parent = nullptr);

private:
    QModbusResponse processPrivateRequest(const QModbusPdu &request) override;
};

La classe de serveur de base appelle la méthode processPrivateRequest() lorsqu'une commande comportant un code de fonction personnalisé est reçue.

L'implémentation personnalisée gère la commande ` CustomRead ` en générant un ` QModbusResponse ` contenant les valeurs des registres demandés :

    if (ModbusClient::CustomRead == request.functionCode()) {
        quint16 startAddress, count;
        request.decodeData(&startAddress, &count);

        QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, startAddress, count);
        if (!data(&unit)) {
            return QModbusExceptionResponse(request.functionCode(),
                QModbusExceptionResponse::IllegalDataAddress);
        }
        return QModbusResponse(request.functionCode(), startAddress, quint8(count * 2), unit.values());
    }

Le traitement de la commande « CustomWrite » consiste à extraire les nouvelles valeurs de l’ QModbusPdu reçu, à effectuer la mise à jour effective des valeurs, puis à renvoyer un QModbusResponse contenant les registres qui ont effectivement été mis à jour :

    if (ModbusClient::CustomWrite == request.functionCode()) {
        quint8 byteCount;
        quint16 startAddress, numberOfRegisters;
        request.decodeData(&startAddress, &numberOfRegisters, &byteCount);

        if (byteCount % 2 != 0) {
            return QModbusExceptionResponse(request.functionCode(),
                QModbusExceptionResponse::IllegalDataValue);
        }

        const QByteArray pduData = request.data().remove(0, WriteHeaderSize);
        QDataStream stream(pduData);

        QList<quint16> values;
        for (int i = 0; i < numberOfRegisters; i++) {
            quint16 tmp;
            stream >> tmp;
            values.append(tmp);
        }

        if (!writeData({QModbusDataUnit::HoldingRegisters, startAddress, values})) {
            return QModbusExceptionResponse(request.functionCode(),
                QModbusExceptionResponse::ServerDeviceFailure);
        }

        return QModbusResponse(request.functionCode(), startAddress, numberOfRegisters);
    }

Client Modbus personnalisé

Le client personnalisé est dérivé de la classe ` QModbusTcpClient `. Il redéfinit la méthode ` QModbusClient::processPrivateResponse()`.

class ModbusClient : public QModbusTcpClient
{
    Q_OBJECT
    Q_DISABLE_COPY_MOVE(ModbusClient)

public:
    ModbusClient(QObject *parent = nullptr);

    static constexpr QModbusPdu::FunctionCode CustomRead {QModbusPdu::FunctionCode(0x41)};
    static constexpr QModbusPdu::FunctionCode CustomWrite {QModbusPdu::FunctionCode(0x42)};

private:
    bool processPrivateResponse(const QModbusResponse &response, QModbusDataUnit *data) override;
};

La classe cliente de base appelle la méthode processPrivateResponse() pour traiter les réponses du serveur à l'aide de codes de fonction personnalisés.

L'implémentation personnalisée gère les réponses avec les codes de fonction CustomRead et CustomWrite:

bool ModbusClient::processPrivateResponse(const QModbusResponse &response, QModbusDataUnit *data)
{
    if (!response.isValid())
        return QModbusClient::processPrivateResponse(response, data);

    if (CustomRead == response.functionCode())
        return collateBytes(response, data);

    if (CustomWrite == response.functionCode())
        return collateMultipleValues(response, data);
    return QModbusClient::processPrivateResponse(response, data);
}

La réponse « CustomRead » est traitée en décodant le « QModbusPdu » fourni et en extrayant les valeurs des registres demandés :

static bool collateBytes(const QModbusPdu &response, QModbusDataUnit *data)
{
    if (response.dataSize() < MinimumReadResponseSize)
        return false;

    quint16 address; quint8 byteCount;
    response.decodeData(&address, &byteCount);

    if (byteCount % 2 != 0)
        return false;

    if (data) {
        QDataStream stream(response.data().remove(0, 3));

        QList<quint16> values;
        const quint8 itemCount = byteCount / 2;
        for (int i = 0; i < itemCount; i++) {
            quint16 tmp;
            stream >> tmp;
            values.append(tmp);
        }
        *data = {QModbusDataUnit::HoldingRegisters, address, values};
    }
    return true;
}

La réponse « CustomWrite » est traitée en validant simplement les paramètres de la réponse :

static bool collateMultipleValues(const QModbusPdu &response, QModbusDataUnit *data)
{
    if (response.dataSize() != WriteResponseSize)
        return false;

    quint16 address, count;
    response.decodeData(&address, &count);

    if (count < 1 || count > 10)
        return false;

    if (data)
        *data = {QModbusDataUnit::HoldingRegisters, address, count};
    return true;
}

Exécution de l’exemple

Vous pouvez exécuter l'exemple à partir de :

Cet exemple ne peut pas être utilisé conjointement avec d’autres applications. Une fois lancé, il ne peut échanger que des commandes Modbus personnalisées au sein de l’application elle-même. Toutes les interactions entre le client et le serveur utilisent le protocole Modbus TCP.

Projet d’exemple sur code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.