KNX Tunneling Features Example

A KNX client for handling KNXnet/IP tunneling features.

"KNX tunneling features example"

The KNX Tunneling Features user interface contains various Qt Widgets, most prominently a QTreeWidget to display detailed information about sent and received KNX tunneling feature messages.

To get started, users select one of the network interfaces on their machine in the Interface field. Once that is done, the application automatically performs a continuous search for available KNXnet/IP devices and displays the results in the Device field.

To connect to a KKXnet/IP device, either the one preselected in the Device can be used or a different one must be chosen from the list of discovered devices.

The application also supports KNXnet/IP secure devices, but to be able to connect to such a device, a KNX ETS keyring file needs to be imported via the File menu.

Once a connection is successfully established, the user has the possibility to send tunneling feature get or set commands and to monitor incoming tunnel feature info messages.

The application consists of two classes:

  • MainWindow is a QMainWindow that renders the general layout of the application.
  • DeviceItem is a QStandardItem that is used to display and store information about discovered KNXnet/IP devices.

Main Window Class Definition and Implementation

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void onConnected();
    void onDisconnected();
    void onDeviceDiscovered(QKnxNetIpServerInfo info);
    void onErrorOccurred(QKnxNetIpEndpointConnection::Error error, QString errorString);

    void on_actionImport_triggered();
    void on_devices_currentIndexChanged(int index);
    void on_serviceTypes_currentIndexChanged(int index);

    void on_sendRead_clicked();
    void on_sendWrite_clicked();
    void on_connection_clicked();

private:
    void setupInterfaces();
    void toggleUi(bool enable);
    void populateServiceTypesComboBox();
    void populateInterfaceFeaturesComboBox();

    void setText(QKnx::InterfaceFeature feature, const QKnxByteArray &value);
    void populateFrame(QKnxNetIp::ServiceType type, QKnx::InterfaceFeature feature,
        const QKnxByteArray &value, int returnCode = -1);
    void onFeatureResponseReceived(QKnx::InterfaceFeature feature, QKnx::ReturnCode code,
        const QKnxByteArray &value);
    void onFeatureInfoReceived(QKnx::InterfaceFeature feature, const QKnxByteArray &value);

private:
    Ui::MainWindow *ui { nullptr };

    DeviceItem *m_device { nullptr };
    QTreeWidgetItem *m_last { nullptr };
    QTreeWidgetItem *m_current { nullptr };

    QKnxNetIpTunnel m_tunnel;
    QKnxNetIpServerDiscoveryAgent m_discoveryAgent;
    QVector<QKnxNetIpSecureConfiguration> m_secureConfigs;
};

The MainWindow class uses a QKnxNetIpServerDiscoveryAgent instance that allows discovering KNXnet/IP servers by sending continuous search requests to the network that the client is connected to. It also saves an instance of the QKnxNetIpTunnel used to establish the connection to the KNX network and a list of imported KNX QKnxNetIpSecureConfiguration secure configurations.

There are signal handlers installed for every signal emitted by the QKnxNetIpTunnel. Here is an example of the setup capturing the signals emitted when an event occurs targeting the KNXnet/IP connection. In this specific example, we will also see how to set up the KNXnet/IP tunnel and connect to the KNXnet/IP device:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ...
    connect(&m_tunnel, &QKnxNetIpTunnel::connected, this, &MainWindow::onConnected);
    connect(&m_tunnel, &QKnxNetIpTunnel::disconnected, this, &MainWindow::onDisconnected);
    connect(&m_tunnel, &QKnxNetIpTunnel::tunnelingFeatureInfoReceived, this,
        &MainWindow::onFeatureInfoReceived);
    connect(&m_tunnel, &QKnxNetIpTunnel::tunnelingFeatureResponseReceived, this,
        &MainWindow::onFeatureResponseReceived);
    connect(&m_tunnel, &QKnxNetIpTunnel::errorOccurred, this, &MainWindow::onErrorOccurred);
    ...
}

void MainWindow::on_connection_clicked()
{
    if (ui->devices->count() <= 0)
        return;

    if (m_tunnel.state() == QKnxNetIpTunnel::State::Connected)
        return m_tunnel.disconnectFromHost();

    const auto list = ui->interfaces->currentData().toStringList();
    m_tunnel.setLocalAddress(QHostAddress(list.first()));
    m_tunnel.setSerialNumber(QKnxByteArray::fromHex(list.last().toLatin1()));

    m_last = new QTreeWidgetItem(ui->communication, m_last);
    m_last->setText(0, tr("Establish connection to: %1 (%2 : %3)")
        .arg(m_device->info().deviceName())
        .arg(m_device->info().controlEndpointAddress().toString())
        .arg(m_device->info().controlEndpointPort()));
    m_last->setFirstColumnSpanned(true);

    if (ui->secureSession->isChecked()) {
        auto secureConfiguration = m_secureConfigs.value(ui->secureConfigs->currentData().toInt());
        secureConfiguration.setKeepSecureSessionAlive(true);
        m_tunnel.setSecureConfiguration(secureConfiguration);
        m_tunnel.connectToHostEncrypted(m_device->info().controlEndpointAddress(),
            m_device->info().controlEndpointPort());
    } else {
        m_tunnel.connectToHost(m_device->info().controlEndpointAddress(),
            m_device->info().controlEndpointPort(), QKnxNetIp::HostProtocol::UDP_IPv4);
    }
}

The QKnxNetIpServerDiscoveryAgent is initialized and started after the user interface has been set up and the necessary tunnel connections have been made. Here is the code snippet doing it:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ...
    m_discoveryAgent.setTimeout(-1);
    m_discoveryAgent.setSearchFrequency(6);
    connect(&m_discoveryAgent, &QKnxNetIpServerDiscoveryAgent::deviceDiscovered, this,
        &MainWindow::onDeviceDiscovered);
    m_discoveryAgent.setDiscoveryMode(QKnxNetIpServerDiscoveryAgent::DiscoveryMode::CoreV2);

    m_discoveryAgent.start();
}

There is a signal handler installed for the device discovered signal emitted by the discovery agent. When the signal QKnxNetIpServerDiscoveryAgent::deviceDiscovered is triggered, the function MainWindow::onDeviceDiscovered() is called. It adds a new device item to the Device if it is not already there.

void MainWindow::onDeviceDiscovered(QKnxNetIpServerInfo info)
{
    if (ui->devices->findText(info.deviceName()) == -1)
        qobject_cast<QStandardItemModel*>(ui->devices->model())->appendRow(new DeviceItem(info));

    if (m_management.state() == QKnxNetIpTunnel::State::Disconnected)
        ui->devices->setEnabled(bool(ui->devices->count()));
}

At this point, users can select one of the available devices to establish a connection, create and send the different types of frames or monitor the KNX tunneling feature info messages. The MainWindow::on_devices_currentIndexChanged method saves the selected KNXnet/IP device in the the MainWindow instance.

In this last example, after the user has triggered the Read button and a valid tunneling feature response was received, the function MainWindow::setText() is called and the frame content gets extracted and visually processed:

void MainWindow::setText(QKnx::InterfaceFeature feature, const QKnxByteArray &data)
{
    const auto hex = data.toByteArray().toHex();
    m_last->setText(5, QStringLiteral("0x") + QLatin1String(hex, hex.size()));

    QString value;
    switch (feature) {
    case QKnx::InterfaceFeature::ActiveEmiType:
    case QKnx::InterfaceFeature::SupportedEmiType: {
        QStringList types;
        const auto emi = QKnx::EmiTypes(hex.toUShort(nullptr, 16));
        const auto metaEnum = QMetaEnum::fromType<QKnx::EmiType>();
        if (emi.testFlag(QKnx::EmiType::EMI1))
            types += QLatin1String(metaEnum.valueToKey(int(QKnx::EmiType::EMI1)));
        if (emi.testFlag(QKnx::EmiType::EMI2))
            types += QLatin1String(metaEnum.valueToKey(int(QKnx::EmiType::EMI2)));
        if (emi.testFlag(QKnx::EmiType::cEMI))
            types += QLatin1String(metaEnum.valueToKey(int(QKnx::EmiType::cEMI)));
        value = types.join(QLatin1Char('|'));
    } break;

    case QKnx::InterfaceFeature::HostDeviceDescriptorType0:
        value = QString::number(hex.toUInt(nullptr, 16));
    break;

    case QKnx::InterfaceFeature::BusConnectionStatus: {
        const auto metaEnum = QMetaEnum::fromType<QKnxState::State>();
        value = metaEnum.valueToKey(int(QKnxState::State(data.value(0))));
    } break;

    case QKnx::InterfaceFeature::MaximumApduLength:
        value = QString::number(hex.toUShort(nullptr, 16));
    break;

    case QKnx::InterfaceFeature::KnxManufacturerCode: {
        const auto id = hex.toUShort(nullptr, 16);
        value = QKnx::Ets::Manufacturers::fromId(id, QString::number(id));
    } break;

    case QKnx::InterfaceFeature::IndividualAddress:
        value = QKnxAddress(QKnxAddress::Type::Individual, data).toString();
    break;

    case QKnx::InterfaceFeature::InterfaceFeatureInfoServiceEnable: {
        const auto metaEnum = QMetaEnum::fromType<QKnxEnable::State>();
        value = metaEnum.valueToKey(int(QKnxEnable::State(data.value(0))));
    } break;

    default:
        break;
    }
    m_last->setText(3, value);
}

The Main Function

The KNX tunneling feature example main() function does not have any special handling. It looks like the main function for any Qt application:

#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

Files:

© 2019 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.