diff --git a/nymea-modbus-cli/main.cpp b/nymea-modbus-cli/main.cpp index dca37ad..43cc7f0 100644 --- a/nymea-modbus-cli/main.cpp +++ b/nymea-modbus-cli/main.cpp @@ -34,31 +34,88 @@ #include #include +#include #include +#include #include +#include + +void sendRequest(quint16 modbusServerAddress, QModbusDataUnit::RegisterType registerType, quint16 registerAddress, quint16 length, const QByteArray &writeData, QModbusClient *client); int main(int argc, char *argv[]) { QCoreApplication application(argc, argv); application.setApplicationName("nymea-modbus-cli"); application.setOrganizationName("nymea"); - application.setApplicationVersion("1.0.0"); + application.setApplicationVersion("1.1.0"); + + QString description = QString("\nTool for testing and reading Modbus TCP or RTU registers.\n\n"); + description.append(QString("Copyright %1 2016 - 2023 nymea GmbH \n\n").arg(QChar(0xA9))); + + + + description.append("TCP\n"); + description.append("-----------------------------------------\n"); + description.append("Example reading 2 holding registers from address 1000:\n"); + description.append("nymea-modbus-cli -a 192.168.0.10 -p 502 -r 1000 -l 2\n\n"); + + + description.append("RTU\n"); + description.append("-----------------------------------------\n\n"); + + description.append("Typical baudrates:\n"); + description.append("- 1200\n"); + description.append("- 2400\n"); + description.append("- 4800\n"); + description.append("- 9600\n"); + description.append("- 19200\n"); + description.append("- 38400\n"); + description.append("- 57600\n"); + description.append("- 115200\n\n"); + + description.append("Example reading 2 holding registers from address 1000:\n"); + description.append("nymea-modbus-cli --serial /dev/ttyUSB0 --baudrate 9600 -r 1000 -l 2\n\n"); - QString description = QString("\nTool for testing and reading Modbus TCP registers.\n\n"); - description.append(QString("Copyright %1 2016 - 2022 nymea GmbH \n").arg(QChar(0xA9))); QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); parser.setApplicationDescription(description); - QCommandLineOption addressOption(QStringList() << "a" << "address", QString("The IP address of the modbus TCP server."), "address"); + // TCP + QCommandLineOption addressOption(QStringList() << "a" << "address", QString("TCP: The IP address of the modbus TCP server."), "address"); parser.addOption(addressOption); - QCommandLineOption portOption(QStringList() << "p" << "port", QString("The port of the modbus TCP server. Default is 502."), "port"); + QCommandLineOption portOption(QStringList() << "p" << "port", QString("TCP: The port of the modbus TCP server. Default is 502."), "port"); portOption.setDefaultValue("502"); parser.addOption(portOption); + // RTU + QCommandLineOption serialPortOption(QStringList() << "serial", QString("RTU: The serial port to use for the RTU communication."), "port"); + parser.addOption(serialPortOption); + + QCommandLineOption baudrateOption(QStringList() << "baudrate", QString("RTU: The baudrate for the RTU communication. Default is 19200."), "baudrate"); + baudrateOption.setDefaultValue("19200"); + parser.addOption(baudrateOption); + + QCommandLineOption parityOption(QStringList() << "parity", QString("RTU: The parity for the RTU communication. Allowed values are [none, even, odd, space, mark]. Default is none."), "parity"); + parityOption.setDefaultValue("none"); + parser.addOption(parityOption); + + QCommandLineOption dataBitsOption(QStringList() << "databits", QString("RTU: The amount of data bits for the RTU communication. Allowed values are [5, 6, 7, 8]. Default is 8."), "databits"); + dataBitsOption.setDefaultValue("8"); + parser.addOption(dataBitsOption); + + QCommandLineOption stopBitsOption(QStringList() << "stopbits", QString("RTU: The amount of stop bits for the RTU communication. Allowed values are [1, 1.5, 2]. Default is 1."), "stopbits"); + stopBitsOption.setDefaultValue("1"); + parser.addOption(stopBitsOption); + + + QCommandLineOption listSerialPortsOption(QStringList() << "list-serials", QString("List the available serial ports on this host.")); + parser.addOption(listSerialPortsOption); + + // General + QCommandLineOption modbusServerAddressOption(QStringList() << "m" << "modbus-address", QString("The modbus server address on the bus (slave ID). Default is 1."), "id"); modbusServerAddressOption.setDefaultValue("1"); parser.addOption(modbusServerAddressOption); @@ -85,6 +142,24 @@ int main(int argc, char *argv[]) bool verbose = parser.isSet(debugOption); if (verbose) qDebug() << "Verbose debug print enabled"; + if (parser.isSet(listSerialPortsOption)) { + foreach (const QSerialPortInfo &serialPortInfo, QSerialPortInfo::availablePorts()) + qInfo().noquote() << serialPortInfo.systemLocation() << "|" << serialPortInfo.description() << "|" << serialPortInfo.serialNumber() << "|" << serialPortInfo.manufacturer(); + + exit(EXIT_SUCCESS); + } + + // Make sure we have either RTU, or TCP, not both or none + if (parser.isSet(addressOption) && parser.isSet(serialPortOption)) { + qCritical() << "Error: invalid paramter combination. Use either TCP connection by defining the \"address\" or RTU by defining the \"serial\" paramter, not both."; + exit(EXIT_FAILURE); + } + + if (!parser.isSet(addressOption) && !parser.isSet(serialPortOption)) { + qCritical() << "Error: unknown protocol. Use either TCP connection by specifying the \"address\" or RTU by specifying the \"serial\" port."; + exit(EXIT_FAILURE); + } + QModbusDataUnit::RegisterType registerType = QModbusDataUnit::RegisterType::Invalid; QString registerTypeString = parser.value(registerTypeOption); if (registerTypeString.toLower() == "input") { @@ -96,31 +171,26 @@ int main(int argc, char *argv[]) } else if (registerTypeString.toLower() == "coils") { registerType = QModbusDataUnit::RegisterType::Coils; } else { - qCritical() << "Invalid register type:" << parser.value(registerTypeOption) << "Please select on of the valid register types: input, holding, discrete, coils"; + qCritical() << "Error: invalid register type:" << parser.value(registerTypeOption) << "Please select on of the valid register types: input, holding, discrete, coils"; exit(EXIT_FAILURE); } bool valueOk = false; quint16 modbusServerAddress = parser.value(modbusServerAddressOption).toUInt(&valueOk); if (modbusServerAddress < 1 || !valueOk) { - qCritical() << "Error: Invalid modbus server address (slave ID):" << parser.value(modbusServerAddressOption); + qCritical() << "Error: invalid modbus server address (slave ID):" << parser.value(modbusServerAddressOption); exit(EXIT_FAILURE); } quint16 registerAddress = parser.value(registerOption).toUInt(&valueOk); if (!valueOk) { - qCritical() << "Error: Invalid register number:" << parser.value(registerOption); + qCritical() << "Error: invalid register number:" << parser.value(registerOption); exit(EXIT_FAILURE); } quint16 length = parser.value(lengthOption).toUInt(&valueOk); if (!valueOk) { - qCritical() << "Error: Invalid register length number:" << parser.value(lengthOption); - exit(EXIT_FAILURE); - } - - if (!parser.isSet(addressOption)) { - qWarning() << "Error: please specify the IP address of the modbus TCP server you want connect to. Modbus RTU is not implemented yet so you need to specify it."; + qCritical() << "Error: invalid register length number:" << parser.value(lengthOption); exit(EXIT_FAILURE); } @@ -130,115 +200,205 @@ int main(int argc, char *argv[]) qDebug() << "Write data:" << writeData; } - // TCP connection - QHostAddress address = QHostAddress(parser.value(addressOption)); - if (address.isNull()) { - qCritical() << "Error: Invalid address:" << parser.value(addressOption); - exit(EXIT_FAILURE); + // TCP + if (parser.isSet(addressOption)) { + // TCP connection + QHostAddress address = QHostAddress(parser.value(addressOption)); + if (address.isNull()) { + qCritical() << "Error: invalid address:" << parser.value(addressOption); + exit(EXIT_FAILURE); + } + + quint16 port = parser.value(portOption).toUInt(); + + qInfo().noquote() << "Connecting to" << QString("%1:%2").arg(address.toString()).arg(port) << "modbus server address:" << modbusServerAddress; + QModbusTcpClient *client = new QModbusTcpClient(nullptr); + client->setConnectionParameter(QModbusDevice::NetworkAddressParameter, address.toString()); + client->setConnectionParameter(QModbusDevice::NetworkPortParameter, port); + client->setTimeout(3000); + client->setNumberOfRetries(3); + + QObject::connect(client, &QModbusTcpClient::stateChanged, &application, [=](QModbusDevice::State state){ + if (verbose) qDebug() << "Connection state changed" << state; + if (state != QModbusDevice::ConnectedState) + return; + + qDebug() << "Connected successfully to" << QString("%1:%2").arg(address.toString()).arg(port); + sendRequest(modbusServerAddress, registerType, registerAddress, length, writeData, client); + }); + + QObject::connect(client, &QModbusTcpClient::errorOccurred, &application, [=](QModbusDevice::Error error){ + qWarning() << "Modbus error occurred:" << error << client->errorString(); + exit(EXIT_FAILURE); + }); + + if (!client->connectDevice()) { + qWarning() << "Error: could not connect to" << QString("%1:%2").arg(address.toString()).arg(port); + exit(EXIT_FAILURE); + } } - quint16 port = parser.value(portOption).toUInt(); + if (parser.isSet(serialPortOption)) { - qInfo() << "Connecting to" << QString("%1:%2").arg(address.toString()).arg(port) << "Modbus server address:" << modbusServerAddress; - QModbusTcpClient *client = new QModbusTcpClient(nullptr); - client->setConnectionParameter(QModbusDevice::NetworkAddressParameter, address.toString()); - client->setConnectionParameter(QModbusDevice::NetworkPortParameter, port); - client->setTimeout(3000); - client->setNumberOfRetries(3); + QString serialPortName = parser.value(serialPortOption); - QObject::connect(client, &QModbusTcpClient::stateChanged, &application, [=](QModbusDevice::State state){ - if (verbose) qDebug() << "Connection state changed" << state; - if (state == QModbusDevice::ConnectedState) { - qDebug() << "Connected successfully to" << QString("%1:%2").arg(address.toString()).arg(port); + quint32 baudrate = parser.value(baudrateOption).toUInt(); - if (writeData.isEmpty()) { - qDebug() << "Reading from modbus server address" << modbusServerAddress << registerTypeString << "register:" << registerAddress << "Length:" << length; - QModbusDataUnit request = QModbusDataUnit(registerType, registerAddress, length); - QModbusReply *reply = client->sendReadRequest(request, modbusServerAddress); - if (!reply) { - qCritical() << "Failed to read register" << client->errorString(); - exit(EXIT_FAILURE); - } - - if (reply->isFinished()) { - reply->deleteLater(); // broadcast replies return immediately - qCritical() << "Reply finished immediatly. Something might have gone wrong:" << reply->errorString(); - exit(EXIT_FAILURE); - } - - QObject::connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - QObject::connect(reply, &QModbusReply::finished, client, [=]() { - if (reply->error() != QModbusDevice::NoError) { - qCritical() << "Reply finished with error:" << reply->errorString(); - exit(EXIT_FAILURE); - } - - const QModbusDataUnit unit = reply->result(); - for (uint i = 0; i < unit.valueCount(); i++) { - quint16 registerValue = unit.values().at(i); - quint16 registerNumber = unit.startAddress() + i; - qInfo() << "-->" << registerNumber << ":" << QString("0x%1").arg(registerValue, 4, 16, QLatin1Char('0')) << registerValue; - } - - exit(EXIT_SUCCESS); - }); - - QObject::connect(reply, &QModbusReply::errorOccurred, client, [=] (QModbusDevice::Error error){ - qCritical() << "Modbus reply error occurred:" << error << reply->errorString(); - }); - } else { - QModbusDataUnit request = QModbusDataUnit(registerType, registerAddress, length); - QDataStream stream(writeData); - qDebug() << "Reading write data" << writeData; - quint16 data = writeData.toUInt(); - request.setValues({data}); - - qDebug() << "Writing" << request.values(); - QModbusReply *reply = client->sendWriteRequest(request, modbusServerAddress); - if (!reply) { - qCritical() << "Failed to read register" << client->errorString(); - exit(EXIT_FAILURE); - } - - if (reply->isFinished()) { - reply->deleteLater(); // broadcast replies return immediately - qCritical() << "Reply finished immediatly. Something might have gone wrong:" << reply->errorString(); - exit(EXIT_FAILURE); - } - - QObject::connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - QObject::connect(reply, &QModbusReply::finished, client, [=]() { - if (reply->error() != QModbusDevice::NoError) { - qCritical() << "Reply finished with error:" << reply->errorString(); - exit(EXIT_FAILURE); - } - - const QModbusDataUnit unit = reply->result(); - for (uint i = 0; i < unit.valueCount(); i++) { - quint16 registerValue = unit.values().at(i); - quint16 registerNumber = unit.startAddress() + i; - qInfo() << "-->" << registerNumber << ":" << QString("0x%1").arg(registerValue, 4, 16, QLatin1Char('0')) << registerValue; - } - - exit(EXIT_SUCCESS); - }); - - QObject::connect(reply, &QModbusReply::errorOccurred, client, [=] (QModbusDevice::Error error){ - qCritical() << "Modbus reply error occurred:" << error << reply->errorString(); - }); - } + QSerialPort::Parity parity = QSerialPort::NoParity; + QString parityString = parser.value(parityOption); + if (parityString.toLower() == "none") { + parity = QSerialPort::NoParity; + } else if (parityString.toLower() == "even") { + parity = QSerialPort::EvenParity; + } else if (parityString.toLower() == "odd") { + parity = QSerialPort::OddParity; + } else if (parityString.toLower() == "space") { + parity = QSerialPort::SpaceParity; + } else if (parityString.toLower() == "mark") { + parity = QSerialPort::MarkParity; + } else { + qCritical() << "Error: invalid parit type:" << parser.value(parityOption) << "Please select on of the valid values: [none, even, odd, space, mark]."; + exit(EXIT_FAILURE); } - }); - QObject::connect(client, &QModbusTcpClient::errorOccurred, &application, [=](QModbusDevice::Error error){ - qWarning() << "Modbus error occurred:" << error << client->errorString(); - exit(EXIT_FAILURE); - }); + QSerialPort::StopBits stopBits = QSerialPort::OneStop; + QString stopBitsString = parser.value(stopBitsOption); + if (stopBitsString == "1") { + stopBits = QSerialPort::OneStop; + } else if (stopBitsString == "1.5") { + stopBits = QSerialPort::OneAndHalfStop; + } else if (stopBitsString == "2") { + stopBits = QSerialPort::TwoStop; + } else { + qCritical() << "Error: invalid stop bits:" << parser.value(stopBitsOption) << "Please select on of the valid values: [1, 1.5, 2]."; + exit(EXIT_FAILURE); + } - if (!client->connectDevice()) { - qWarning() << "Error: could not connect to" << QString("%1:%2").arg(address.toString()).arg(port); - exit(EXIT_FAILURE); + QSerialPort::DataBits dataBits = QSerialPort::Data8; + QString dataBitsString = parser.value(dataBitsOption); + if (dataBitsString == "5") { + dataBits = QSerialPort::Data5; + } else if (dataBitsString == "6") { + dataBits = QSerialPort::Data6; + } else if (dataBitsString == "7") { + dataBits = QSerialPort::Data7; + } else if (dataBitsString == "8") { + dataBits = QSerialPort::Data8; + } else { + qCritical() << "Error: invalid data bits:" << parser.value(dataBitsOption) << "Please select on of the valid values: [5, 6, 7, 8]."; + exit(EXIT_FAILURE); + } + + QModbusRtuSerialMaster *client = new QModbusRtuSerialMaster(nullptr); + client->setConnectionParameter(QModbusDevice::SerialPortNameParameter, serialPortName); + client->setConnectionParameter(QModbusDevice::SerialBaudRateParameter, baudrate); + client->setConnectionParameter(QModbusDevice::SerialDataBitsParameter, dataBits); + client->setConnectionParameter(QModbusDevice::SerialStopBitsParameter, stopBits); + client->setConnectionParameter(QModbusDevice::SerialParityParameter, parity); + client->setNumberOfRetries(3); + client->setTimeout(500); + + QObject::connect(client, &QModbusTcpClient::stateChanged, &application, [=](QModbusDevice::State state){ + qDebug() << "Connection state changed" << state; + if (state != QModbusDevice::ConnectedState) + return; + + qDebug() << "Connected successfully to" << serialPortName << baudrate << dataBits << stopBits << parity << "modbus server address:" << modbusServerAddress; + sendRequest(modbusServerAddress, registerType, registerAddress, length, writeData, client); + }); + + QObject::connect(client, &QModbusRtuSerialMaster::errorOccurred, &application, [=](QModbusDevice::Error error){ + qWarning() << "Error occurred for modbus RTU master" << error << client->errorString(); + if (error != QModbusDevice::NoError) { + exit(EXIT_FAILURE); + } + }); + + if (!client->connectDevice()) { + qWarning() << "Error: failed not connect to" << serialPortName << client->errorString(); + exit(EXIT_FAILURE); + } } return application.exec(); } + +void sendRequest(quint16 modbusServerAddress, QModbusDataUnit::RegisterType registerType, quint16 registerAddress, quint16 length, const QByteArray &writeData, QModbusClient *client) +{ + if (writeData.isEmpty()) { + qDebug() << "Reading from modbus server address" << modbusServerAddress << registerType << "register:" << registerAddress << "Length:" << length; + QModbusDataUnit request = QModbusDataUnit(registerType, registerAddress, length); + QModbusReply *reply = client->sendReadRequest(request, modbusServerAddress); + if (!reply) { + qCritical() << "Failed to read register" << client->errorString(); + exit(EXIT_FAILURE); + } + + if (reply->isFinished()) { + reply->deleteLater(); // broadcast replies return immediately + qCritical() << "Reply finished immediatly. Something might have gone wrong:" << reply->errorString(); + exit(EXIT_FAILURE); + } + + QObject::connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + QObject::connect(reply, &QModbusReply::finished, client, [=]() { + if (reply->error() != QModbusDevice::NoError) { + qCritical() << "Reply finished with error:" << reply->errorString(); + exit(EXIT_FAILURE); + } + + const QModbusDataUnit unit = reply->result(); + for (uint i = 0; i < unit.valueCount(); i++) { + quint16 registerValue = unit.values().at(i); + quint16 registerNumber = unit.startAddress() + i; + qInfo() << "-->" << registerNumber << ":" << QString("0x%1").arg(registerValue, 4, 16, QLatin1Char('0')) << registerValue; + } + + exit(EXIT_SUCCESS); + }); + + QObject::connect(reply, &QModbusReply::errorOccurred, client, [=] (QModbusDevice::Error error){ + qCritical() << "Modbus reply error occurred:" << error << reply->errorString(); + }); + } else { + QModbusDataUnit request = QModbusDataUnit(registerType, registerAddress, length); + QDataStream stream(writeData); + qDebug() << "Reading write data" << writeData; + quint16 data = writeData.toUInt(); + request.setValues({data}); + + qDebug() << "Writing" << request.values(); + QModbusReply *reply = client->sendWriteRequest(request, modbusServerAddress); + if (!reply) { + qCritical() << "Failed to read register" << client->errorString(); + exit(EXIT_FAILURE); + } + + if (reply->isFinished()) { + reply->deleteLater(); // broadcast replies return immediately + qCritical() << "Reply finished immediatly. Something might have gone wrong:" << reply->errorString(); + exit(EXIT_FAILURE); + } + + QObject::connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + QObject::connect(reply, &QModbusReply::finished, client, [=]() { + if (reply->error() != QModbusDevice::NoError) { + qCritical() << "Reply finished with error:" << reply->errorString(); + exit(EXIT_FAILURE); + } + + const QModbusDataUnit unit = reply->result(); + for (uint i = 0; i < unit.valueCount(); i++) { + quint16 registerValue = unit.values().at(i); + quint16 registerNumber = unit.startAddress() + i; + qInfo() << "-->" << registerNumber << ":" << QString("0x%1").arg(registerValue, 4, 16, QLatin1Char('0')) << registerValue; + } + + exit(EXIT_SUCCESS); + }); + + QObject::connect(reply, &QModbusReply::errorOccurred, client, [=] (QModbusDevice::Error error){ + qCritical() << "Modbus reply error occurred:" << error << reply->errorString(); + }); + } +}