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 "tunneling.h"
 #include "ui_tunneling.h"

 #include <QKnxLinkLayerFrameBuilder>
 #include <QKnxNetIpSecureConfiguration>
 #include <QMetaEnum>
 #include <QMetaType>
 #include <QStandardItemModel>
 #include <QTreeView>
 #include <QTreeWidget>

 // -- KnxAddressValidator

 KnxAddressValidator::KnxAddressValidator(QLatin1Char delimiter, QObject *parent)
     : QValidator(parent)
     , m_delimiter(delimiter)
     , m_expr(QString::fromLatin1("[^0-9\\%1]").arg(delimiter))
 {}

 QValidator::State KnxAddressValidator::validate(QString & input, int &) const
 {
     auto result = m_expr.match(input);
     if (result.hasMatch())
         return State::Invalid;

     auto n = input.split(m_delimiter);
     if (n.value(0).toInt() > 15 || n.value(1).toInt() > 15 || n.value(2).toInt() > 255)
         return State::Invalid;
     return State::Intermediate;
 }

 // -- Tunneling

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

     setupApciTpciComboBox();
     setupMessageCodeComboBox();
     updateAdditionalInfoTypesComboBox();

     m_frame.setMediumType(QKnx::MediumType::NetIP);

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

     connect(&m_tunnel, &QKnxNetIpTunnel::connected, this, [&] {
         ui->connectTunneling->setEnabled(false);
         ui->disconnectTunneling->setEnabled(true);
         ui->tunnelingSendRequest->setEnabled(true);
         ui->sourceAddress->setText(m_tunnel.individualAddress().toString());
         ui->textOuputTunneling->append(tr("Successfully connected to: %1 on port: %2").arg(m_server
             .controlEndpointAddress().toString()).arg(m_server.controlEndpointPort()));
     });

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

     connect(&m_tunnel, &QKnxNetIpTunnel::disconnected, this, [&] {
         if (!ui)
             return;
         ui->connectTunneling->setEnabled(true);
         ui->disconnectTunneling->setEnabled(false);
         ui->tunnelingSendRequest->setEnabled(false);
         ui->textOuputTunneling->append(tr("Successfully disconnected from: %1 on port: %2\n")
             .arg(m_server.controlEndpointAddress().toString()).arg(m_server.controlEndpointPort()));
     });

     connect(ui->tunnelingSendRequest, &QPushButton::clicked, this, [&]() {
         ui->textOuputTunneling->append(tr("Send tunnel frame with cEMI payload: ")
             + ui->cemiFrame->text());
         auto data = QKnxByteArray::fromHex(ui->cemiFrame->text().toLatin1());
         auto frame = QKnxLinkLayerFrame::builder()
                 .setData(data)
                 .setMedium(QKnx::MediumType::NetIP)
                 .createFrame();
         m_tunnel.sendFrame(frame);
     });

     connect(&m_tunnel, &QKnxNetIpTunnel::frameReceived, this,
         [&](QKnxLinkLayerFrame frame) {
         ui->textOuputTunneling->append(tr("Source address: %1").arg(frame.sourceAddress()
             .toString()));
         ui->textOuputTunneling->append(tr("Destination address: %1").arg(frame.destinationAddress()
             .toString()));
         ui->textOuputTunneling->append(tr("Received tunnel frame with cEMI payload: "
             + frame.bytes().toHex().toByteArray()));
     });

     connect(&m_tunnel, &QKnxNetIpTunnel::errorOccurred, this,
         [&] (QKnxNetIpEndpointConnection::Error, QString errorString) {
             ui->textOuputTunneling->append(errorString);
     });

     updateControlField();
     updateExtendedControlField();

     connect(ui->frameFormat, &QComboBox::currentTextChanged, this, [&](const QString &text) {
         if (text == "Extended") {
             for (int i = 0; i < ui->additionalInfosList->count(); ++i) {
                 auto b = ui->additionalInfosList->item(i)->text().toLatin1();
                 m_frame.addAdditionalInfo(QKnxAdditionalInfo::fromBytes(QKnxByteArray::fromHex(b), 0));
             }
             ui->additionalInfo->setEnabled(true);
         } else {
             m_frame.clearAdditionalInfos();
             ui->additionalInfo->setEnabled(false);
         }
         updateControlField();
     });
     connect(ui->repeat, &QComboBox::currentTextChanged, this, &Tunneling::updateControlField);
     connect(ui->broadcast, &QComboBox::currentTextChanged, this, &Tunneling::updateControlField);
     connect(ui->priority, &QComboBox::currentTextChanged, this, &Tunneling::updateControlField);
     connect(ui->acknowledge, &QComboBox::currentTextChanged, this, &Tunneling::updateControlField);
     connect(ui->confirm, &QComboBox::currentTextChanged, this, &Tunneling::updateControlField);

     connect(ui->destAddrType, (&QComboBox::currentTextChanged), this, [&](const QString &text) {
         QLatin1Char delimiter(text == "Group" ? '/' : '.');
         ui->destAddress->setValidator(new KnxAddressValidator(delimiter));
         ui->destAddress->setText(ui->destAddress->text().replace(QRegularExpression("\\/|\\."),
             QString(delimiter)));
         updateExtendedControlField();
     });
     using overload = void (QSpinBox::*)(int);
     connect(ui->hopCount, static_cast<overload>(&QSpinBox::valueChanged), this,
         &Tunneling::updateExtendedControlField);
     connect(ui->extFrameFormat, &QComboBox::currentTextChanged, this,
         &Tunneling::updateExtendedControlField);

     connect(ui->additionalInfoTypes, &QComboBox::currentTextChanged, this, [&]() {
         bool fixed = true;
         auto type = QKnxAdditionalInfo::Type(ui->additionalInfoTypes->currentData().toUInt());
         int size = QKnxAdditionalInfo::expectedDataSize(type, &fixed);
         ui->additionallnfoData->setMaxLength((fixed ? size : 252) * 2);
     });

     connect(ui->addAdditionalInfo, &QPushButton::clicked, this, [&]() {
         auto type = QKnxAdditionalInfo::Type(ui->additionalInfoTypes->currentData().toUInt());
         auto info = QKnxAdditionalInfo(type, QKnxByteArray::fromHex(ui->additionallnfoData->text()
             .toLatin1()));
         if (info.isValid()) {
             m_frame.addAdditionalInfo(info);
             ui->cemiFrame->setText(m_frame.bytes().toHex().toByteArray());
             ui->additionalInfosList->addItem(new QListWidgetItem(info.bytes().toByteArray()));
         }
     });

     connect(ui->removeAdditionalInfo, &QPushButton::clicked, this, [&]() {
         auto index = ui->additionalInfosList->currentIndex();
         if (index.isValid()) {
             auto currentItem = ui->additionalInfosList->item(index.row());
             auto b = currentItem->text().toLatin1();
             m_frame.removeAdditionalInfo(QKnxAdditionalInfo::fromBytes(QKnxByteArray::fromHex(b),
                 0));
             ui->cemiFrame->setText(m_frame.bytes().toHex().toByteArray());
             delete ui->additionalInfosList->takeItem(index.row());
         }
     });

     connect(ui->mc, &QComboBox::currentTextChanged, this, &Tunneling::updateFrame);
     connect(ui->sourceAddress, &QLineEdit::textChanged, this, &Tunneling::updateFrame);
     connect(ui->destAddress, &QLineEdit::textChanged, this, &Tunneling::updateFrame);
     connect(ui->apci, &QComboBox::currentTextChanged, this, &Tunneling::updateFrame);
     connect(ui->tpci, &QComboBox::currentTextChanged, this, &Tunneling::updateFrame);
     connect(ui->data, &QLineEdit::textChanged, this, &Tunneling::updateFrame);

     connect(ui->manualInput, &QCheckBox::clicked, this, [&](bool checked) {
         bool disable = checked;
         if (!checked)
             disable = ui->frameFormat->currentText() == "Standard";
         ui->additionalInfo->setDisabled(disable);

         if (!checked) {
             disable = ui->tpci->itemData(ui->tpci->currentIndex())
                 .value<QKnxTpdu::TransportControlField>() >= QKnxTpdu::TransportControlField::Connect;
         }
         ui->apci->setDisabled(disable);
         ui->data->setDisabled(disable);
     });

     ui->destAddress->setValidator(new KnxAddressValidator(QLatin1Char('/')));
     ui->sourceAddress->setValidator(new KnxAddressValidator(QLatin1Char('.')));
     ui->data->setValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]+")));
     ui->additionallnfoData->setValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]+")));
 }

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

 void Tunneling::setNatAware(bool isNatAware)
 {
     m_tunnel.setNatAware(isNatAware);
 }

 void Tunneling::setLocalAddress(const QHostAddress &address)
 {
     m_tunnel.disconnectFromHost();
     m_tunnel.setLocalAddress(address);
 }

 void Tunneling::setKnxNetIpServer(const QKnxNetIpServerInfo &server)
 {
     m_tunnel.disconnectFromHost();
     m_server = server;

     if (m_tunnel.state() == QKnxNetIpEndpointConnection::State::Disconnected) {
         ui->connectTunneling->setEnabled(true);
         ui->disconnectTunneling->setEnabled(false);
     }

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

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

 void Tunneling::clearLogging()
 {
     ui->textOuputTunneling->clear();
 }

 void Tunneling::updateFrame()
 {
     m_frame.setMessageCode(QKnxLinkLayerFrame::MessageCode(ui->mc->currentData().toUInt()));
     m_frame.setControlField(m_ctrl);
     m_frame.setExtendedControlField(m_extCtrl);
     m_frame.setSourceAddress({ QKnxAddress::Type::Individual, ui->sourceAddress->text() });
     m_frame.setDestinationAddress({ m_extCtrl.destinationAddressType(), ui->destAddress->text() });

     auto tpdu = QKnxTpdu { ui->tpci->itemData(ui->tpci->currentIndex())
         .value<QKnxTpdu::TransportControlField>() };

     if (tpdu.transportControlField() < QKnxTpdu::TransportControlField::Connect) {
         ui->apci->setEnabled(true);
         ui->data->setEnabled(true);
         tpdu.setApplicationControlField(ui->apci->itemData(ui->apci->currentIndex())
             .value<QKnxTpdu::ApplicationControlField>());
         tpdu.setData(QKnxByteArray::fromHex(ui->data->text().toLatin1()));
     } else {
         ui->apci->setEnabled(false);
         ui->data->setEnabled(false);
     }
     m_frame.setTpdu(tpdu);
     ui->cemiFrame->setText(m_frame.bytes().toHex().toByteArray());

     ui->tunnelingSendRequest->setEnabled(m_frame.isValid());
 }

 void Tunneling::updateControlField()
 {
     m_ctrl.setFrameFormat(QKnxControlField::FrameFormat(ui->frameFormat->currentIndex()));
     m_ctrl.setRepeat(QKnxControlField::Repeat(ui->repeat->currentIndex()));
     m_ctrl.setBroadcast(QKnxControlField::Broadcast(ui->broadcast->currentIndex()));
     m_ctrl.setPriority(QKnxControlField::Priority(ui->priority->currentIndex()));
     m_ctrl.setAcknowledge(QKnxControlField::Acknowledge(ui->acknowledge->currentIndex()));
     m_ctrl.setConfirm(QKnxControlField::Confirm(ui->confirm->currentIndex()));

     updateFrame();
 }

 void Tunneling::updateExtendedControlField()
 {
     m_extCtrl.setDestinationAddressType(QKnxAddress::Type(ui->destAddrType->currentText() == "Group"));
     m_extCtrl.setHopCount(ui->hopCount->value());
     m_extCtrl.setFormat(QKnxExtendedControlField::ExtendedFrameFormat(ui->extFrameFormat->currentIndex()));

     updateFrame();
 }

 void Tunneling::on_manualInput_clicked(bool checked)
 {
     ui->cemiFrame->setReadOnly(!checked);
     ui->cemiFrame->setMaxLength(SHRT_MAX);
     ui->cemiFrame->setFocus();
 }

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

 void Tunneling::setupApciTpciComboBox()
 {
     int index = QKnxTpdu::staticMetaObject.indexOfEnumerator("ApplicationControlField");
     auto typeEnum = QKnxTpdu::staticMetaObject.enumerator(index);
     if (!typeEnum.isValid())
         return;

     QRegularExpression regexp("[A-Z]");
     for (auto i = 0; i < typeEnum.keyCount() - 1; ++i) {
         auto string = QString::fromLatin1(typeEnum.key(i));
         auto index = string.lastIndexOf(regexp);
         if (index > 0) string.insert(index, QLatin1Char('_'));
         ui->apci->addItem(QStringLiteral("A_") + string, typeEnum.value(i));
     }

     index = QKnxTpdu::staticMetaObject.indexOfEnumerator("TransportControlField");
     typeEnum = QKnxTpdu::staticMetaObject.enumerator(index);
     if (!typeEnum.isValid())
         return;

     regexp = QRegularExpression("(?=[A-Z])");
     for (auto i = 0; i < typeEnum.keyCount() - 1; ++i) {
         auto string = QString::fromLatin1(typeEnum.key(i)).split(regexp, QString::SkipEmptyParts);
         ui->tpci->addItem("T_" + string.join(QLatin1Char('_')), typeEnum.value(i));
     }
 }

 void Tunneling::setupMessageCodeComboBox()
 {
     ui->mc->addItem("L_Data.req",
         quint8(QKnxLinkLayerFrame::MessageCode::DataRequest));
     ui->mc->addItem("L_Raw.req",
         quint8(QKnxLinkLayerFrame::MessageCode::RawRequest));
     ui->mc->addItem("M_Reset.req",
         quint8(QKnxLinkLayerFrame::MessageCode::ResetRequest));
 }

 void Tunneling::updateAdditionalInfoTypesComboBox()
 {
     int index = QKnxAdditionalInfo::staticMetaObject.indexOfEnumerator("Type");
     auto typeEnum = QKnxAdditionalInfo::staticMetaObject.enumerator(index);
     if (!typeEnum.isValid())
         return;

     auto model = static_cast<QStandardItemModel*> (ui->additionalInfoTypes->model());
     for (int row = 0; row < model->rowCount(); ++row) {
         auto data = model->itemData(model->index(row, 0));
         data.insert(Qt::UserRole, typeEnum.value(row));
         model->setItemData(model->index(row, 0), data);
     }
     model->item(0)->setEnabled(false);
     model->item(model->rowCount() - 1)->setEnabled(false);
 }

 void Tunneling::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;

         const auto ia = config.individualAddress();
         ui->secureSessionCb->addItem(tr("User ID: %1 (Individual Address: %2)")
             .arg(config.userId())
             .arg(ia.isValid() ? ia.toString() : tr("No specific address")), i);
     }
 }