KNX Editor Example

 /****************************************************************************
 **
 ** Copyright (C) 2017 The Qt Company Ltd.
 ** Contact: https://www.qt.io/licensing/
 **
 ** This file is part of the QtKnx module.
 **
 ** $QT_BEGIN_LICENSE:BSD$
 ** Commercial License Usage
 ** Licensees holding valid commercial Qt licenses may use this file in
 ** accordance with the commercial license agreement provided with the
 ** Software or, alternatively, in accordance with the terms contained in
 ** a written agreement between you and The Qt Company. For licensing terms
 ** and conditions see https://www.qt.io/terms-conditions. For further
 ** information use the contact form at https://www.qt.io/contact-us.
 **
 ** BSD License Usage
 ** Alternatively, you may use this file under the terms of the BSD license
 ** as follows:
 **
 ** "Redistribution and use in source and binary forms, with or without
 ** modification, are permitted provided that the following conditions are
 ** met:
 **   * Redistributions of source code must retain the above copyright
 **     notice, this list of conditions and the following disclaimer.
 **   * Redistributions in binary form must reproduce the above copyright
 **     notice, this list of conditions and the following disclaimer in
 **     the documentation and/or other materials provided with the
 **     distribution.
 **   * Neither the name of The Qt Company Ltd nor the names of its
 **     contributors may be used to endorse or promote products derived
 **     from this software without specific prior written permission.
 **
 **
 ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
 **
 ** $QT_END_LICENSE$
 **
 ****************************************************************************/

 #include "localdevicemanagement.h"
 #include "ui_localdevicemanagement.h"

 #include <QKnxByteArray>
 #include <QKnxDeviceManagementFrameBuilder>
 #include <QKnxInterfaceObjectPropertyDataType>
 #include <QKnxNetIpSecureConfiguration>
 #include <QMetaEnum>
 #include <QMetaType>
 #include <QTreeWidget>

 LocalDeviceManagement::LocalDeviceManagement(QWidget* parent)
     : QWidget(parent)
     , ui(new Ui::LocalDeviceManagement)
 {
     ui->setupUi(this);

     setupMessageCodeComboBox();

     const auto setTreeWidget = [](QComboBox *comboBox) {
         auto treeWidget = new QTreeWidget;
         treeWidget->setHeaderHidden(true);
         treeWidget->setUniformRowHeights(true);

         comboBox->setModel(treeWidget->model());
         comboBox->setView(treeWidget);
     };
     setTreeWidget(ui->property);
     setTreeWidget(ui->objectType);

     setupComboBox(ui->property, QKnxInterfaceObjectProperty::staticMetaObject);
     setupComboBox(ui->objectType, QKnxInterfaceObjectType::staticMetaObject);

     connect(ui->connectRequestDeviceManagement, &QPushButton::clicked, this, [&]() {
         m_management.setLocalPort(0);
         if (ui->secureSessionCheckBox->isChecked()) {
             auto config = m_configs.value(ui->secureSessionCb->currentIndex());
             config.setKeepSecureSessionAlive(true);
             m_management.setSecureConfiguration(config);
             m_management.connectToHostEncrypted(m_server.controlEndpointAddress(),
                 m_server.controlEndpointPort());
         } else {
             m_management.connectToHost(m_server.controlEndpointAddress(),
                 m_server.controlEndpointPort(), m_proto);
         }
     });

     connect(&m_management, &QKnxNetIpDeviceManagement::connected, this, [&] {
         ui->deviceManagementSendRequest->setEnabled(true);
         ui->connectRequestDeviceManagement->setEnabled(false);
         ui->disconnectRequestDeviceManagement->setEnabled(true);
         ui->textOuputDeviceManagement->append(tr("Successfully connected to: %1 on port: %2")
             .arg(m_server.controlEndpointAddress().toString()).arg(m_server.controlEndpointPort()));

         m_management.sendFrame(QKnxDeviceManagementFrame::propertyReadBuilder()
             .setObjectType(QKnxInterfaceObjectType::System::Device)
             .setObjectInstance(1)
             .setProperty(QKnxInterfaceObjectProperty::Device::IoList)
             .setNumberOfElements(1)
             .setStartIndex(0).createRequest());
     });

     connect(ui->disconnectRequestDeviceManagement, &QPushButton::clicked, this, [&]() {
         m_management.disconnectFromHost();
     });

     connect(&m_management, &QKnxNetIpDeviceManagement::disconnected, this, [&] {
         if (!ui)
             return;
         m_awaitIoListResponse = true;
         ui->deviceManagementSendRequest->setEnabled(false);
         ui->connectRequestDeviceManagement->setEnabled(true);
         ui->disconnectRequestDeviceManagement->setEnabled(false);
         setupComboBox(ui->objectType, QKnxInterfaceObjectType::staticMetaObject);
         ui->textOuputDeviceManagement->append(tr("Successfully disconnected from: %1 on port: %2\n")
             .arg(m_server.controlEndpointAddress().toString()).arg(m_server.controlEndpointPort()));
     });

     connect(ui->deviceManagementSendRequest, &QPushButton::clicked, this, [&]() {
         ui->textOuputDeviceManagement->append(tr("Send device management frame with cEMI payload: ")
             + ui->cemiFrame->text());
         auto data = QKnxByteArray::fromHex(ui->cemiFrame->text().toUtf8());
         if (ui->cemiData->isEnabled())
             data.append(QKnxByteArray::fromHex(ui->cemiData->text().toUtf8()));
         m_management.sendFrame(QKnxDeviceManagementFrame::fromBytes(data, 0,
             data.size()));
     });

     connect(&m_management, &QKnxNetIpDeviceManagement::frameReceived,
         this, [&](QKnxDeviceManagementFrame frame) {
         ui->textOuputDeviceManagement->append(tr("Received device management frame with cEMI "
             "payload: " + frame.bytes().toHex().toByteArray()));

         if (m_awaitIoListResponse)
             handleIoListResponse(frame);
     });

     connect(&m_management, &QKnxNetIpDeviceManagement::errorOccurred, this,
         [&] (QKnxNetIpEndpointConnection::Error, QString errorString) {
             ui->textOuputDeviceManagement->append(errorString);
     });

     ui->cemiData->setValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]+")));
     ui->cemiFrame->setValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]+")));
 }

 LocalDeviceManagement::~LocalDeviceManagement()
 {
     delete ui;
     ui = nullptr;
 }

 void LocalDeviceManagement::setNatAware(bool isNatAware)
 {
     m_management.setNatAware(isNatAware);
 }

 void LocalDeviceManagement::setLocalAddress(const QHostAddress &address)
 {
     m_management.disconnectFromHost();
     m_management.setLocalAddress(address);
 }

 void LocalDeviceManagement::setKnxNetIpServer(const QKnxNetIpServerInfo &server)
 {
     m_management.disconnectFromHost();
     m_server = server;

     if (m_management.state() == QKnxNetIpEndpointConnection::State::Disconnected) {
         ui->connectRequestDeviceManagement->setEnabled(true);
         ui->disconnectRequestDeviceManagement->setEnabled(false);
     }

     updateSecureConfigCombo();
     ui->deviceManagementSendRequest->setEnabled(false);
 }

 void LocalDeviceManagement::setTcpEnable(bool value)
 {
     m_proto = (value ? QKnxNetIp::HostProtocol::TCP_IPv4 : QKnxNetIp::HostProtocol::UDP_IPv4);
 }

 void LocalDeviceManagement::clearLogging()
 {
     ui->textOuputDeviceManagement->clear();
 }

 void LocalDeviceManagement::on_mc_currentIndexChanged(int index)
 {
     if (ui->cemiFrame->text().size() > 2)
         m_fullCemiFrame = ui->cemiFrame->text();

     int maxLength = 10;
     bool dataEnabled = true;
     auto cemiFrame = m_fullCemiFrame;

     quint8 data = ui->mc->itemData(index).toUInt();
     switch (QKnxDeviceManagementFrame::MessageCode(data)) {
     case QKnxDeviceManagementFrame::MessageCode::PropertyReadRequest:
         dataEnabled = false;
     case QKnxDeviceManagementFrame::MessageCode::PropertyWriteRequest:
         maxLength = 14;
     case QKnxDeviceManagementFrame::MessageCode::FunctionPropertyCommandRequest:
     case QKnxDeviceManagementFrame::MessageCode::FunctionPropertyStateReadRequest:
         if (cemiFrame.size() < maxLength) {
             cemiFrame.append(QStringLiteral("%1").arg(ui->noe->value(), 1, 16, QLatin1Char('0')));
             cemiFrame.append(QStringLiteral("%1").arg(ui->startIndex->value(), 3, 16, QLatin1Char('0')));
         }
         cemiFrame = QStringLiteral("%1").arg(data, 2, 16, QLatin1Char('0')) + cemiFrame.mid(2);
         break;
     case QKnxDeviceManagementFrame::MessageCode::ResetRequest:
         maxLength = 2;
         dataEnabled = false;
         cemiFrame = QStringLiteral("%1").arg(data, 2, 16, QLatin1Char('0'));
         break;
     default:
         break;
     }
     ui->cemiFrame->setMaxLength(maxLength);
     ui->cemiFrame->setText(cemiFrame);
     ui->cemiData->setEnabled(dataEnabled);
 }

 void LocalDeviceManagement::on_objectType_currentTextChanged(const QString &type)
 {
     bool keyExists = false;
     quint16 value = keyToValue(QKnxInterfaceObjectType::staticMetaObject, type, &keyExists);
     if (keyExists) {
         auto text = ui->cemiFrame->text();
         ui->cemiFrame->setText(text.left(2) + QStringLiteral("%1").arg(value, 4, 16,
             QLatin1Char('0')) + text.mid(6));
         updatePropertyTypeCombobox(type);
     }
 }

 void LocalDeviceManagement::on_property_currentTextChanged(const QString &property)
 {
     bool keyExists = false;
     quint8 value = keyToValue(QKnxInterfaceObjectProperty::staticMetaObject, property, &keyExists);
     if (keyExists) {
         auto text = ui->cemiFrame->text();
         ui->cemiFrame->setText(text.left(8) + QStringLiteral("%1").arg(value, 2, 16,
             QLatin1Char('0')) + text.mid(10));
     }
 }

 void LocalDeviceManagement::on_noe_valueChanged(int value)
 {
     auto text = ui->cemiFrame->text();
     ui->cemiFrame->setText(text.left(10) + QStringLiteral("%1").arg(value, 1,
         16, QLatin1Char('0')) + text.mid(11));
 }

 void LocalDeviceManagement::on_startIndex_valueChanged(int value)
 {
     auto text = ui->cemiFrame->text();
     ui->cemiFrame->setText(text.left(11) + QStringLiteral("%1").arg(value, 3, 16,
         QLatin1Char('0')));
 }

 void LocalDeviceManagement::on_objectInstance_valueChanged(int value)
 {
     auto text = ui->cemiFrame->text();
     ui->cemiFrame->setText(text.left(6) + QStringLiteral("%1").arg(value, 2, 16,
         QLatin1Char('0')) + text.mid(8));
 }

 void LocalDeviceManagement::on_manualInput_clicked(bool checked)
 {
     ui->cemiData->setEnabled(checked);
     ui->cemiFrame->setReadOnly(!checked);
     ui->cemiFrame->setMaxLength(SHRT_MAX);
     ui->cemiFrame->setFocus();

     if (checked) {
         if (ui->mc->currentIndex() != ui->mc->count() -1)
             m_fullCemiFrame = ui->cemiFrame->text();
     } else {
         ui->cemiFrame->setText(m_fullCemiFrame);
         on_mc_currentIndexChanged(ui->mc->currentIndex());
     }
 }

 void LocalDeviceManagement::onKeyringChanged(const QVector<QKnxNetIpSecureConfiguration> &configs)
 {
     m_configs = configs;
     updateSecureConfigCombo();
 }

 void LocalDeviceManagement::setupMessageCodeComboBox()
 {
     ui->mc->addItem("M_PropRead.req",
         quint8(QKnxDeviceManagementFrame::MessageCode::PropertyReadRequest));
     ui->mc->addItem("M_PropWrite.req",
         quint8(QKnxDeviceManagementFrame::MessageCode::PropertyWriteRequest));
     ui->mc->addItem("M_FuncPropCommand.req",
         quint8(QKnxDeviceManagementFrame::MessageCode::FunctionPropertyCommandRequest));
     ui->mc->addItem("M_FuncPropStateRead.req",
         quint8(QKnxDeviceManagementFrame::MessageCode::FunctionPropertyStateReadRequest));
     ui->mc->addItem("M_Reset.req",
         quint8(QKnxDeviceManagementFrame::MessageCode::ResetRequest));
 }

 void LocalDeviceManagement::updatePropertyTypeCombobox(const QString &type)
 {
     int index = QKnxInterfaceObjectProperty::staticMetaObject.indexOfEnumerator("General");
     if (index >= 0) {
         auto treeView = qobject_cast<QTreeWidget *>(ui->property->view());
         if (!treeView)
             return;

         auto topLevelItem = treeView->takeTopLevelItem(0);
         ui->property->clear();
         treeView->addTopLevelItem(topLevelItem);

         auto index = QKnxInterfaceObjectProperty::staticMetaObject.indexOfEnumerator(type.toLatin1());
         if (index >= 0) {
             auto typeEnum = QKnxInterfaceObjectProperty::staticMetaObject.enumerator(index);
             topLevelItem = new QTreeWidgetItem(treeView, { typeEnum.name() });
             for (auto a = 0; a < typeEnum.keyCount(); ++a)
                 topLevelItem->addChild(new QTreeWidgetItem({ typeEnum.key(a) }));
             topLevelItem->setFlags(topLevelItem->flags() &~Qt::ItemIsSelectable);
         }
         treeView->expandItem(topLevelItem);
         selectFirstSubitem(treeView, topLevelItem, ui->property);
     }
 }

 void LocalDeviceManagement::handleIoListResponse(const QKnxDeviceManagementFrame &frame)
 {
     if (frame.objectType() != QKnxInterfaceObjectType::System::Device
         || frame.property() != QKnxInterfaceObjectProperty::Device::IoList) {
             return;
     }

     if (frame.isNegativeConfirmation()) {
         auto metaEnum = QMetaEnum::fromType<QKnxNetIp::CemiServer::Error>();
         ui->textOuputDeviceManagement->append(tr("Received negative confirmation. Error code: %1")
             .arg(QString::fromLatin1(metaEnum.valueToKey(int(frame.error())))));
         m_awaitIoListResponse = false;
         return;
     }

     auto dataTypes = QKnxInterfaceObjectPropertyDataType::fromProperty(QKnxInterfaceObjectProperty
         ::Device::IoList);
     if (!dataTypes.value(0).isValid())
         return;

     auto data = frame.data().toByteArray();
     quint8 expectedDataSize = dataTypes[0].size();
     if (frame.startIndex() == 0) {
         if (data.size() == expectedDataSize) {
             m_management.sendFrame(QKnxDeviceManagementFrame::propertyReadBuilder()
                 .setObjectType(QKnxInterfaceObjectType::System::Device)
                 .setObjectInstance(1)
                 .setProperty(QKnxInterfaceObjectProperty::Device::IoList)
                 .setNumberOfElements(data.toHex().toUShort(nullptr, 16))
                 .setStartIndex(1)
                 .createRequest()
             );
         }
     } else {
         if ((data.size() % expectedDataSize) == 0) {
             QSet<int> values;
             for (int i = 0; i < data.size(); i += expectedDataSize)
                 values.insert(data.mid(i, expectedDataSize).toHex().toUShort(nullptr, 16));

             m_awaitIoListResponse = false;
             setupComboBox(ui->objectType, QKnxInterfaceObjectType::staticMetaObject, values);
         }
     }
 }

 int LocalDeviceManagement::keyToValue(const QMetaObject &object, const QString &key, bool *ok)
 {
     auto enumCount = object.enumeratorCount();
     for (auto i = 0; i < enumCount; ++i) {
         int value = object.enumerator(i).keyToValue(key.toLatin1(), ok);
         if (value != -1 && *ok)
             return value;
     }
     return -1;
 }

 void LocalDeviceManagement::setupComboBox(QComboBox *comboBox, const QMetaObject &object,
     const QSet<int> &values)
 {
     comboBox->clear();

     auto treeWidget = qobject_cast<QTreeWidget *> (comboBox->view());
     if (!treeWidget)
         return;

     QTreeWidgetItem *rootItem = nullptr;
     auto enumCount = object.enumeratorCount();
     for (auto i = 0; i < enumCount; ++i) {
         auto typeEnum = object.enumerator(i);
         auto topLevelItem = new QTreeWidgetItem(treeWidget, { typeEnum.name() });
         if (!rootItem)
             rootItem = topLevelItem;
         for (auto a = 0; a < typeEnum.keyCount(); ++a) {
             auto subItem = new QTreeWidgetItem(topLevelItem, { typeEnum.key(a) });
             subItem->setDisabled(values.isEmpty() ? false : !values.contains(typeEnum.value(a)));
         }
         topLevelItem->setFlags(topLevelItem->flags() &~ Qt::ItemIsSelectable);
     }
     treeWidget->expandItem(rootItem);
     selectFirstSubitem(treeWidget, rootItem, comboBox);
 }

 void LocalDeviceManagement::selectFirstSubitem(QTreeWidget *treeWidget, QTreeWidgetItem *rootItem,
     QComboBox *comboBox)
 {
     treeWidget->setCurrentItem(rootItem, 0);
     comboBox->setRootModelIndex(treeWidget->currentIndex());
     comboBox->setCurrentIndex(0);
     treeWidget->setCurrentItem(treeWidget->invisibleRootItem(), 0);
     comboBox->setRootModelIndex(treeWidget->currentIndex());
 }

 void LocalDeviceManagement::updateSecureConfigCombo()
 {
     ui->secureSessionCb->clear();

     ui->secureSessionCheckBox->setEnabled(!m_configs.isEmpty());
     ui->secureSessionCheckBox->setChecked(m_proto == QKnxNetIp::HostProtocol::TCP_IPv4);

     for (int i = 0; i < m_configs.size(); ++i) {
         const auto &config = m_configs[i];
         if (m_server.individualAddress() != config.host())
             continue;

         ui->secureSessionCb->addItem(tr("User ID: %1 (Individual Address: %2)").arg(config.userId())
             .arg(config.individualAddress().toString()), i);
     }
 }