From 879540023c534d23558ba440bd1a87e41db1968f Mon Sep 17 00:00:00 2001 From: Andrew O'Doherty Date: Mon, 4 Jun 2018 17:04:17 +0200 Subject: Add QKnxNetIpRoutingInterface class and auto-test The newly introduced class can be used for transmitting KNX frames between KNXnet/IP routers. Change-Id: Ief8a8731463e251367768f34894b04861f8b5835 Reviewed-by: Karsten Heimrich --- src/knx/netip/netip.pri | 11 +- src/knx/netip/qknxnetiproutinginterface.cpp | 526 +++++++++++++++++++++ src/knx/netip/qknxnetiproutinginterface.h | 137 ++++++ src/knx/netip/qknxnetiproutinginterface_p.cpp | 411 ++++++++++++++++ src/knx/netip/qknxnetiproutinginterface_p.h | 121 +++++ src/knx/netip/qknxtestingrouter_p.cpp | 67 +++ src/knx/netip/qknxtestingrouter_p.h | 66 +++ tests/auto/auto.pro | 3 +- .../qknxnetiproutinginterface.pro | 7 + .../tst_qknxnetiproutinginterface.cpp | 515 ++++++++++++++++++++ 10 files changed, 1860 insertions(+), 4 deletions(-) create mode 100644 src/knx/netip/qknxnetiproutinginterface.cpp create mode 100644 src/knx/netip/qknxnetiproutinginterface.h create mode 100644 src/knx/netip/qknxnetiproutinginterface_p.cpp create mode 100644 src/knx/netip/qknxnetiproutinginterface_p.h create mode 100644 src/knx/netip/qknxtestingrouter_p.cpp create mode 100644 src/knx/netip/qknxtestingrouter_p.h create mode 100644 tests/auto/qknxnetiproutinginterface/qknxnetiproutinginterface.pro create mode 100644 tests/auto/qknxnetiproutinginterface/tst_qknxnetiproutinginterface.cpp diff --git a/src/knx/netip/netip.pri b/src/knx/netip/netip.pri index aee78d9..6f566e6 100644 --- a/src/knx/netip/netip.pri +++ b/src/knx/netip/netip.pri @@ -52,7 +52,8 @@ PUBLIC_HEADERS += $$PWD/qknxnetip.h \ $$PWD/qknxnetipsessionauthenticate.h \ $$PWD/qknxnetipsessionstatus.h \ $$PWD/qknxnetiptimernotify.h \ - $$PWD/qknxnetipsecurewrapper.h + $$PWD/qknxnetipsecurewrapper.h \ + $$PWD/qknxnetiproutinginterface.h PRIVATE_HEADERS += \ $$PWD/qknxbuilderdata_p.h \ @@ -60,7 +61,8 @@ PRIVATE_HEADERS += \ $$PWD/qknxnetipserverdescriptionagent_p.h \ $$PWD/qknxnetipserverdiscoveryagent_p.h \ $$PWD/qknxnetipserverinfo_p.h \ - $$PWD/qknxbuilderdata_p.h + $$PWD/qknxnetiproutinginterface_p.h \ + $$PWD/qknxtestingrouter_p.h SOURCES += $$PWD/qknxnetip.cpp \ $$PWD/qknxnetipconfigdib.cpp \ @@ -114,4 +116,7 @@ SOURCES += $$PWD/qknxnetip.cpp \ $$PWD/qknxnetipsessionauthenticate.cpp \ $$PWD/qknxnetipsessionstatus.cpp \ $$PWD/qknxnetiptimernotify.cpp \ - $$PWD/qknxnetipsecurewrapper.cpp + $$PWD/qknxnetipsecurewrapper.cpp \ + $$PWD/qknxnetiproutinginterface_p.cpp \ + $$PWD/qknxnetiproutinginterface.cpp \ + $$PWD/qknxtestingrouter_p.cpp diff --git a/src/knx/netip/qknxnetiproutinginterface.cpp b/src/knx/netip/qknxnetiproutinginterface.cpp new file mode 100644 index 0000000..e140080 --- /dev/null +++ b/src/knx/netip/qknxnetiproutinginterface.cpp @@ -0,0 +1,526 @@ +/****************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** 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. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "qknxnetiproutinginterface.h" +#include "qknxnetiproutinginterface_p.h" +#include "qknxnetip.h" +#include "qknxnetipframe.h" +#include "qknxnetiproutingbusy.h" +#include "qknxnetiproutingindication.h" +#include "qknxnetiproutinglostmessage.h" + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +/*! + \class QKnxNetIpRoutingInterface + + \inmodule QtKnx + \ingroup qtknx-routing + \ingroup qtknx-netip + + \brief The QKnxNetIpRoutingInterface class enables sending and receiving + routing KNXnet/IP packets to and from other KNXnet/IP routers. + + KNXnet/IP routing is defined as a set of KNXnet/IP routers communicating + over a one-to-many communication relationship (multicast), in which KNX + data shall be transferred from one device to one or more other devices + simultaneously over an IP network. A set of KNXnet/IP routers can replace + KNX line and backbone couplers and connected main lines, allowing usage + of existing cabling (such as Ethernet) and faster transmission times (and + simultaneousness) between KNX subnets. The IP network acts as a fast + backbone that connects KNX subnets and is a high-speed replacement for + the KNX backbone. + + The QKnxNetIpRoutingInterface is bound to a physical network interface + which is used for transmitting and receiving the routing frames. It also + requires the multicast address used by the KNX installation. + + The following code sample illustrates how to use the + QKnxNetIpRoutingInterface to send and receive KNXnet/IP frames: + + \code + QKnxNetIpRoutingInterface router; + router.setInterfaceAffinity(QNetworkInterface::interfaceFromName("eth0")); + router.setMulticastAddress(QHostAddress("224.0.23.32")); + router.start(); + + auto busyWaitTime = ... + auto busyControlField = ... + + // Sending a routing indication to another router on the same IP network + auto routingBusyFrame = QKnxNetIpRoutingBusyProxy::builder() + .setDeviceState(QKnxNetIp::DeviceState::IpFault) + .setRoutingBusyWaitTime(busyWaitTime) + .setRoutingBusyControl(busyControlField) + .create(); + router.sendRoutingBusy(routingBusyFrame); + + // Processing routing indications received + QObject::connect(&router, + &QKnxNetIpRoutingInterface::routingIndicationReceived, + [](QKnxNetIpFrame frame) { + QKnxNetIpRoutingIndicationProxy indication(frame); + qInfo().noquote() << "Received routing indication:" + << indication.isValid(); + }); + + \endcode + + \sa QKnxLinkLayerFrame +*/ + +/*! + \typedef QKnxNetIpRoutingInterface::FilterTable + + A synonym for QKnxNetIpRoutingInterface::QSet which is the + type used to store the filter table of the routing interface. +*/ + +/*! + \enum QKnxNetIpRoutingInterface::State + + This enum holds the state of the QKnxNetIpRoutingInterface. + + \value NotInit + Router not started yet. + \value Routing + Router is listening for incoming messages and accepting messages + for sending. + \value NeighborBusy + A KNX router in the same network sent a busy message or the + QKnxNetIpRoutingInterface itself detected a busy situation. + \value Stop + Router has been explicitly stopped by the user. + \value Failure + Any error that occurs in the router shall set this state. +*/ + +/*! + \enum QKnxNetIpRoutingInterface::Error + + This enum holds the state of the QKnxNetIpRoutingInterface. + + \value None + No errors. + \value KnxRouting + Error found at the KNX routing protocol level. + \value Network + UDP/IP network error. +*/ + +/*! + \enum QKnxNetIpRoutingInterface::FilterAction + + This enum holds the possible courses of action of a router in response to a + received layer service data unit (LSDU). An LSDU is a type of frame used by + the L_Data service belonging to the data link layer. + + \value RouteUnmodified + The LSDU shall be routed from one subnetwork (on the primary side or the + secondary side) to the second subnetwork side (on the secondary side or + the primary side) without modification of the hop count value. The LSDU + shall only be routed to the second subnetwork after it has been + positively acknowledged on the first subnetwork. + \value RouteDecremented + The LSDU shall be routed from one subnetwork (on the primary + side or the secondary side) to the second subnetwork side (on the + secondary side or the primary side) after the hop count value is + decremented. The LSDU shall only be routed to the second subnetwork + after it has been positively acknowledged on the first subnetwork. + \value RouteLast + The LSDU shall be routed from the first subnetwork to the second + subnetwork and the hop count value shall be set to zero. The + telegram shall be acknowledged according to the Data Link Layer + specifications of the medium of the first subnetwork and then + the LSDU shall be routed to the second subnetwork. + \value ForwardLocally + The LSDU shall be processed to an NSDU and given to the local + network layer user after it has been acknowledged positively on + the subnetwork from which it has arrived. + \value IgnoreTotally + The LSDU shall be ignored; no acknowledgment shall be sent back + to the originator of the LSDU. + \value IgnoreAcked + The LSDU shall be ignored, nonetheless it shall be acknowledged + on the subnetwork from which it has arrived. +*/ + +/*! + \enum QKnxNetIpRoutingInterface::RoutingMode + + This enum holds the possible parameterization actions that a router shall + apply on telegrams of a point-to-multipoint connectionless communication + mode with standard group addresses. + + \value BlockRouting + No telegrams are allowed to be forwarded on the interface. + \value RouteAll + All telegrams are routed and the filter table is ignored. + \value FilterTableRouting + All telegrams with destination addresses not in the filter table are + blocked, the rest of telegrams are forwarded. +*/ + +/*! + \fn void QKnxNetIpRoutingInterface::routingIndicationReceived(QKnxNetIpFrame frame, QKnxNetIpRoutingInterface::FilterAction routingAction) + + This signal is emitted when the KNXnet/IP router receives a routing indication + \a frame and specifies the action \a routingAction to be applied by the router. +*/ + +/*! + \fn void QKnxNetIpRoutingInterface::routingBusyReceived(QKnxNetIpFrame frame) + + This signal is emitted when the KNXnet/IP router receives a routing busy + \a frame. +*/ + +/*! + \fn void QKnxNetIpRoutingInterface::routingLostCountReceived(QKnxNetIpFrame frame) + + This signal is emitted when the KNXnet/IP router receives a routing lost + count message held in \a frame. +*/ + +/*! + \fn void QKnxNetIpRoutingInterface::routingIndicationSent(QKnxNetIpFrame frame) + + This signal is emitted when the KNXnet/IP router has finished sending the + routing indication \a frame. +*/ + +/*! + \fn void QKnxNetIpRoutingInterface::routingBusySent(QKnxNetIpFrame frame) + + This signal is emitted when the KNXnet/IP router has finished sending the + routing busy \a frame. +*/ + +/*! + \fn void QKnxNetIpRoutingInterface::routingLostCountSent(QKnxNetIpFrame frame) + + This signal is emitted when the KNXnet/IP router has finished sending the + routing lost count \a frame. +*/ + +/*! + \fn void QKnxNetIpRoutingInterface::stateChanged(QKnxNetIpRoutingInterface::State state) + + This signal is emitted when the KNXnet/IP router transitions to a different + \a state. +*/ + +/*! + \fn void QKnxNetIpRoutingInterface::errorOccurred(QKnxNetIpRoutingInterface::Error error, QString errorString) + + This signal is emitted when the KNXnet/IP router encounters the error \a + error that is described by \a errorString. +*/ + +/*! + Returns the current state of the KNX routing interface. +*/ +QKnxNetIpRoutingInterface::State QKnxNetIpRoutingInterface::state() const +{ + Q_D(const QKnxNetIpRoutingInterface); + return d->m_state; +} + +/*! + Returns a QString describing the latest known error that occurred in the + KNX routing interface. +*/ +QString QKnxNetIpRoutingInterface::errorString() const +{ + Q_D(const QKnxNetIpRoutingInterface); + return d->m_errorMessage; +} + +/*! + Returns the filter table used by the routing algorithm. +*/ +QKnxNetIpRoutingInterface::FilterTable QKnxNetIpRoutingInterface::filterTable() const +{ + Q_D(const QKnxNetIpRoutingInterface); + return d->m_filterTable; +} + +/*! + Sets the filter table used by the routing algorithm to \a table. + */ +void QKnxNetIpRoutingInterface::setFilterTable(const QKnxNetIpRoutingInterface::FilterTable &table) +{ + Q_D(QKnxNetIpRoutingInterface); + d->m_filterTable = table; + d->m_routingMode = RoutingMode::FilterTableRouting; +} + +/*! + Returns the routing mode. +*/ +QKnxNetIpRoutingInterface::RoutingMode QKnxNetIpRoutingInterface::routingMode() const +{ + Q_D(const QKnxNetIpRoutingInterface); + return d->m_routingMode; +} + +/*! + Sets the interface routing mode to \a mode. + */ +void QKnxNetIpRoutingInterface::setRoutingMode(QKnxNetIpRoutingInterface::RoutingMode mode) +{ + Q_D(QKnxNetIpRoutingInterface); + d->m_routingMode = mode; +} + +/*! + Returns an error code that describes the last error that occurred in the + KNX routing interface. +*/ +QKnxNetIpRoutingInterface::Error QKnxNetIpRoutingInterface::error() const +{ + Q_D(const QKnxNetIpRoutingInterface); + return d->m_error; +} + +/*! + Constructs a KNXnet/IP routing interface with the parent \a parent. +*/ +QKnxNetIpRoutingInterface::QKnxNetIpRoutingInterface(QObject *parent) + : QObject(*new QKnxNetIpRoutingInterfacePrivate, parent) +{} + +/*! + Returns the current network interface used by this KNX routing instance. + */ +QNetworkInterface QKnxNetIpRoutingInterface::interfaceAffinity() const +{ + Q_D(const QKnxNetIpRoutingInterface); + return d->m_iface; +} + +/*! + Searches for the network interface that is bound to \a address that the + QKnxNetIpRoutingInterface instance shall use. + */ +void QKnxNetIpRoutingInterface::setInterfaceAffinity(const QHostAddress &address) +{ + Q_D(QKnxNetIpRoutingInterface); + + const auto interfaces = QNetworkInterface::allInterfaces(); + for (const auto &iface : interfaces) { + if (iface.addressEntries().isEmpty()) + continue; + + auto ifaceAddress = iface.addressEntries().first().ip(); + if (address != ifaceAddress) + continue; + + if (!iface.flags().testFlag(QNetworkInterface::IsRunning) + || !iface.flags().testFlag(QNetworkInterface::CanMulticast)) { + break; + } + + d->m_iface = iface; + return; + } + + d->errorOccurred(Error::Network, tr("Could not set an affinity to any interface")); +} + +/*! + Sets the network interface \a iface that the QNetworkInterface instance + shall use. + */ +void QKnxNetIpRoutingInterface::setInterfaceAffinity(const QNetworkInterface &iface) +{ + Q_D(QKnxNetIpRoutingInterface); + if (iface.flags().testFlag(QNetworkInterface::IsRunning)) + d->m_iface = iface; + else + d->errorOccurred(Error::Network, tr("Could not set an affinity to any interface")); +} + +/*! + Returns the multicast address used by the QKnxNetIpRoutingInterface. + */ +QHostAddress QKnxNetIpRoutingInterface::multicastAddress() const +{ + Q_D(const QKnxNetIpRoutingInterface); + return d->m_multicastAddress; +} + +/*! + Sets the multicast address to \a address. + */ +void QKnxNetIpRoutingInterface::setMulticastAddress(const QHostAddress &address) +{ + Q_D(QKnxNetIpRoutingInterface); + + auto isIPv4 = false; + address.toIPv4Address(&isIPv4); + if (!isIPv4 || !address.isMulticast()) + return; + + d->m_multicastAddress = address; +} + +/*! + Returns the routing interface's individual address. +*/ +QKnxAddress QKnxNetIpRoutingInterface::individualAddress() const +{ + Q_D(const QKnxNetIpRoutingInterface); + return d->m_individualAddress; +} + +/*! + Sets the routing interface's individual address to \a address. +*/ +void QKnxNetIpRoutingInterface::setIndividualAddress(const QKnxAddress &address) +{ + Q_D(QKnxNetIpRoutingInterface); + if (!address.isCouplerOrRouter()) { + d->errorOccurred(QKnxNetIpRoutingInterface::Error::KnxRouting, tr("Could not set " + "individual address.")); + } else { + d->m_individualAddress = address; + } +} + +/*! + Multicasts the routing indication \a frame through the network interface + associated with the QKnxNetIpRoutingInterface. + */ +void QKnxNetIpRoutingInterface::sendRoutingIndication(const QKnxNetIpFrame &frame) +{ + Q_D(QKnxNetIpRoutingInterface); + + if (d->m_state != QKnxNetIpRoutingInterface::State::Routing) + return; + + QKnxNetIpRoutingIndicationProxy indication(frame); + if (!indication.isValid()) + return; + + if (!d->sendFrame(frame)) { + d->errorOccurred(QKnxNetIpRoutingInterface::Error::KnxRouting, tr("Could not send routing " + "indication.")); + } else { + emit routingIndicationSent(frame); + } +} + +/*! + Multicasts the routing indication \a linkFrame through the network interface + associated with the QKnxNetIpRoutingInterface. + */ +void QKnxNetIpRoutingInterface::sendRoutingIndication(const QKnxLinkLayerFrame &linkFrame) +{ + auto netIpFrame = QKnxNetIpRoutingIndicationProxy::builder() + .setCemi(linkFrame) + .create(); + sendRoutingIndication(netIpFrame); +} + +/*! + Multicasts the routing busy message containing \a frame through the + network interface associated with the QKnxNetIpRoutingInterface. +*/ +void QKnxNetIpRoutingInterface::sendRoutingBusy(const QKnxNetIpFrame &frame) +{ + Q_D(QKnxNetIpRoutingInterface); + + if (d->m_state != QKnxNetIpRoutingInterface::State::Routing) + return; + + QKnxNetIpRoutingBusyProxy routingBusy(frame); + if (!routingBusy.isValid()) + return; + + if (!d->sendFrame(frame)) { + d->errorOccurred(QKnxNetIpRoutingInterface::Error::KnxRouting, tr("Could not send routing " + "busy.")); + } else { + emit routingBusySent(frame); + } +} + +/*! + Multicasts the routing lost message \a frame through the network interface + associated with the QKnxNetIpRoutingInterface. +*/ +void QKnxNetIpRoutingInterface::sendRoutingLostMessage(const QKnxNetIpFrame &frame) +{ + Q_D(QKnxNetIpRoutingInterface); + + if (d->m_state != QKnxNetIpRoutingInterface::State::Routing) + return; + + QKnxNetIpRoutingLostMessageProxy lostMessageFrame(frame); + if (!lostMessageFrame.isValid()) + return; + + if (!d->sendFrame(frame)) { + d->errorOccurred(QKnxNetIpRoutingInterface::Error::KnxRouting, tr("Could not send routing " + "lost count.")); + } else { + emit routingLostCountSent(frame); + } +} + +/*! + Signals the QKnxNetIpRoutingInterface to start listening for messages + received and accept sending messages. +*/ +void QKnxNetIpRoutingInterface::start() +{ + Q_D(QKnxNetIpRoutingInterface); + if (d->m_state == State::NotInit) + d->start(); + else if (d->m_state == State::Failure || d->m_state == State::Stop) + d->restart(); +} + +/*! + Stops the QKnxNetIpRoutingInterface. No messages can be received and/or + sent to the network. +*/ +void QKnxNetIpRoutingInterface::stop() +{ + Q_D(QKnxNetIpRoutingInterface); + d->stop(); + d->changeState(QKnxNetIpRoutingInterface::State::Stop); +} + +QT_END_NAMESPACE diff --git a/src/knx/netip/qknxnetiproutinginterface.h b/src/knx/netip/qknxnetiproutinginterface.h new file mode 100644 index 0000000..39560f6 --- /dev/null +++ b/src/knx/netip/qknxnetiproutinginterface.h @@ -0,0 +1,137 @@ +/****************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** 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. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef QKNXNETIPROUTINGINTERFACE_H +#define QKNXNETIPROUTINGINTERFACE_H + +#include +#include +#include +#include + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QKnxNetIpRoutingInterfacePrivate; +class Q_KNX_EXPORT QKnxNetIpRoutingInterface : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(QKnxNetIpRoutingInterface) + Q_DECLARE_PRIVATE(QKnxNetIpRoutingInterface) + +public: + using FilterTable = QSet; + + enum class State : quint8 + { + NotInit, + Routing, + NeighborBusy, + Stop, + Failure + }; + Q_ENUM(State) + QKnxNetIpRoutingInterface::State state() const; + + enum class Error : quint8 + { + None, + KnxRouting, + Network + }; + Q_ENUM(Error) + + QString errorString() const; + QKnxNetIpRoutingInterface::Error error() const; + + enum class FilterAction : quint8 + { + RouteUnmodified, + RouteDecremented, + RouteLast, + ForwardLocally, + IgnoreTotally, + IgnoreAcked + }; + Q_ENUM(FilterAction) + + enum class RoutingMode : quint8 + { + BlockRouting, + RouteAll, + FilterTableRouting + }; + + QKnxNetIpRoutingInterface(QObject *parent = nullptr); + ~QKnxNetIpRoutingInterface() = default; + + RoutingMode routingMode() const; + void setRoutingMode(RoutingMode mode); + + FilterTable filterTable() const; + void setFilterTable(const FilterTable &table); + + QNetworkInterface interfaceAffinity() const; + void setInterfaceAffinity(const QHostAddress &address); + void setInterfaceAffinity(const QNetworkInterface &iface); + + QHostAddress multicastAddress() const; + void setMulticastAddress(const QHostAddress &address); + + QKnxAddress individualAddress() const; + void setIndividualAddress(const QKnxAddress &address); + +public Q_SLOTS: + void sendRoutingIndication(const QKnxLinkLayerFrame &linkFrame); + void sendRoutingIndication(const QKnxNetIpFrame &frame); + void sendRoutingBusy(const QKnxNetIpFrame &frame); + void sendRoutingLostMessage(const QKnxNetIpFrame &frame); + + void start(); + void stop(); + +Q_SIGNALS: + void routingIndicationSent(QKnxNetIpFrame frame); + void routingBusySent(QKnxNetIpFrame frame); + void routingLostCountSent(QKnxNetIpFrame frame); + + void routingIndicationReceived(QKnxNetIpFrame frame, QKnxNetIpRoutingInterface::FilterAction action); + void routingBusyReceived(QKnxNetIpFrame frame); + void routingLostCountReceived(QKnxNetIpFrame frame); + + void stateChanged(QKnxNetIpRoutingInterface::State state); + void errorOccurred(QKnxNetIpRoutingInterface::Error error, QString errorString); +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/knx/netip/qknxnetiproutinginterface_p.cpp b/src/knx/netip/qknxnetiproutinginterface_p.cpp new file mode 100644 index 0000000..19067cd --- /dev/null +++ b/src/knx/netip/qknxnetiproutinginterface_p.cpp @@ -0,0 +1,411 @@ +/****************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** 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. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "qknxnetiproutingbusy.h" +#include "qknxnetiproutingindication.h" +#include "qknxnetiproutinginterface_p.h" +#include "qknxnetiproutinginterface.h" +#include "qknxnetiproutinglostmessage.h" + +#ifdef QT_BUILD_INTERNAL +#include "qknxtestingrouter_p.h" +#endif + +#include + +QT_BEGIN_NAMESPACE + +void QKnxNetIpRoutingInterfacePrivate::errorOccurred(QKnxNetIpRoutingInterface::Error error, + const QString &errorString) +{ + m_error = error; + m_errorMessage = errorString; + changeState(QKnxNetIpRoutingInterface::State::Failure); + + Q_Q(QKnxNetIpRoutingInterface); + emit q->errorOccurred(m_error, m_errorMessage); +} + +void QKnxNetIpRoutingInterfacePrivate::changeState(QKnxNetIpRoutingInterface::State state) +{ + if (m_state == state) + return; + m_state = state; + + Q_Q(QKnxNetIpRoutingInterface); + emit q->stateChanged(m_state); +} + +void QKnxNetIpRoutingInterfacePrivate::start() +{ + if (m_state != QKnxNetIpRoutingInterface::State::NotInit + && m_state != QKnxNetIpRoutingInterface::State::Stop) + return; + +#ifdef QT_BUILD_INTERNAL + TestingRouter::instance()->setRouterInstance(this); +#endif + + if (!m_iface.isValid()) { + // Choose first interface available and capable of multicasting + const auto interfaces = QNetworkInterface::allInterfaces(); + for (const auto &iface : interfaces) { + if (iface.flags().testFlag(QNetworkInterface::IsRunning) + && iface.flags().testFlag(QNetworkInterface::CanMulticast) + && !iface.flags().testFlag(QNetworkInterface::IsLoopBack)) { + m_iface = iface; + break; + } + } + // still no interface valid found + if (!m_iface.isValid()) { + errorOccurred(QKnxNetIpRoutingInterface::Error::Network, + QKnxNetIpRoutingInterface::tr("Could not start routing because there isn't a " + "valid interface.")); + return; + } + } + m_ownAddress = m_iface.addressEntries().first().ip(); + + m_busyTimer = new QTimer; + m_busyTimer->setSingleShot(true); + + // while neighbor router busy don't overflow him with messages, timeout until some msec + QObject::connect(m_busyTimer, &QTimer::timeout, [&]() { + switch (m_busyStage) { + case BusyTimerStage::NotInit: + break; + case BusyTimerStage::Wait: + m_busyTimer->setInterval(QRandomGenerator().bounded(m_busyCounter * 50)); + m_busyTimer->start(); + m_busyStage = BusyTimerStage::RandomWait; + break; + case BusyTimerStage::RandomWait: + m_busyTimer->setInterval(m_busyCounter * 100); + m_busyTimer->start(); + m_busyStage = BusyTimerStage::SlowDuration; + this->changeState(QKnxNetIpRoutingInterface::State::Routing); + break; + case BusyTimerStage::SlowDuration: + m_busyTimer->setInterval(5); + m_busyTimer->setSingleShot(false); + m_busyTimer->start(); + m_busyStage = BusyTimerStage::DecrementBusyCounter; + break; + case BusyTimerStage::DecrementBusyCounter: + if (--m_busyCounter == 0) { + m_busyTimer->stop(); + m_busyStage = BusyTimerStage::NotInit; + m_busyTimer->setSingleShot(true); + } + break; + } + }); + + m_socket = new QUdpSocket; + m_socket->setSocketOption(QUdpSocket::SocketOption::MulticastTtlOption, 60); + + // handle QUdpSocket state changes here + QObject::connect(m_socket, &QUdpSocket::stateChanged, [&](QUdpSocket::SocketState s) { + switch (s) { + case QUdpSocket::BoundState: + m_socket->setMulticastInterface(m_iface); + if (m_socket->joinMulticastGroup(m_multicastAddress, m_iface)) { + changeState(QKnxNetIpRoutingInterface::State::Routing); + } else { + errorOccurred(QKnxNetIpRoutingInterface::Error::Network, + QKnxNetIpRoutingInterface::tr("Could not join multicast group.")); + } + break; + case QUdpSocket::ClosingState: + if (!m_socket->leaveMulticastGroup(m_multicastAddress, m_iface)) { + changeState(QKnxNetIpRoutingInterface::State::NotInit); + } else { + errorOccurred(QKnxNetIpRoutingInterface::Error::Network, + QKnxNetIpRoutingInterface::tr("Could not leave multicast group.")); + } + break; + default: + break; + } + }); + + // handle frames received by the UDP socket + QObject::connect(m_socket, &QUdpSocket::readyRead, [&]() { + // TODO: Review this part, the following members might get cleared unexpectedly + // when messages come in one after the other and are not contained all in a single + // datagram. + m_framesReadCount = 0; + m_sameKnxDstAddressIndicationCount = 0; + m_lastIndicationAddress = QKnxAddress(); + while (m_socket && m_socket->state() == QUdpSocket::BoundState + && m_socket->hasPendingDatagrams()) { + + QNetworkDatagram datagram = m_socket->receiveDatagram(); + if (datagram.senderAddress() == m_ownAddress + || m_framesReadCount == 10 // incoming queue too big, signal busy + || m_sameKnxDstAddressIndicationCount == 5) { + continue; // discard packet + } + + auto data = QKnxByteArray::fromByteArray(datagram.data()); + const auto header = QKnxNetIpFrameHeader::fromBytes(data, 0); + if (!header.isValid() || header.totalSize() != data.size()) + continue; // discard packet + + m_framesReadCount++; + switch (header.serviceType()) { + case QKnxNetIp::ServiceType::RoutingIndication: + processRoutingIndication(QKnxNetIpFrame::fromBytes(data, 0)); + break; + case QKnxNetIp::ServiceType::RoutingBusy: + processRoutingBusy(QKnxNetIpFrame::fromBytes(data, 0)); + break; + case QKnxNetIp::ServiceType::RoutingLostMessage: + processRoutingLostMessage(QKnxNetIpFrame::fromBytes(data, 0)); + break; + default: + break; + } + } + + if (m_framesReadCount == 10 || m_sameKnxDstAddressIndicationCount == 5) { + // incoming queue over 10 packets or over 5 packets with + // individual address destination. + auto routingBusyNetIpFrame = QKnxNetIpRoutingBusyProxy::builder() + .setDeviceState(QKnxNetIp::DeviceState::KnxFault) + .setRoutingBusyWaitTime(m_busyWaitTime) + .setRoutingBusyControl(0) + .create(); + sendFrame(routingBusyNetIpFrame); + flowControlHandling(m_busyWaitTime); + } + }); + + // handle UDP socket errors + using overload = void (QUdpSocket::*)(QUdpSocket::SocketError); + QObject::connect(m_socket, + static_cast(&QUdpSocket::error), [&](QUdpSocket::SocketError) { + errorOccurred(QKnxNetIpRoutingInterface::Error::Network, + m_socket->errorString()); + }); + + + // initialize UDP socket and bind + if (!m_socket->bind(QHostAddress(QHostAddress::AnyIPv4), m_multicastPort, + (QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint))) { + errorOccurred(QKnxNetIpRoutingInterface::Error::Network, + QKnxNetIpRoutingInterface::tr("Could not bind endpoint: %1") + .arg(m_socket->errorString())); + } +} + +void QKnxNetIpRoutingInterfacePrivate::restart() +{ + stop(); + start(); +} + +void QKnxNetIpRoutingInterfacePrivate::stop() +{ + if (m_state == QKnxNetIpRoutingInterface::State::NotInit) + return; + cleanup(); +} + +void QKnxNetIpRoutingInterfacePrivate::cleanup() +{ + if (m_socket) { + m_socket->disconnect(); + m_socket->deleteLater(); + m_socket = nullptr; + } + + if (m_busyTimer) { + (m_busyTimer)->stop(); + (m_busyTimer)->disconnect(); + (m_busyTimer)->deleteLater(); + (m_busyTimer) = nullptr; + } + m_busyCounter = 0; + m_busyStage = BusyTimerStage::NotInit; + + m_errorMessage = QString(); + m_error = QKnxNetIpRoutingInterface::Error::None; +} + +void QKnxNetIpRoutingInterfacePrivate::processRoutingIndication(const QKnxNetIpFrame &frame) +{ + QKnxNetIpRoutingIndicationProxy indication(frame); + if (!indication.isValid()) { + errorOccurred(QKnxNetIpRoutingInterface::Error::KnxRouting, + QKnxNetIpRoutingInterface::tr("QKnxNetIp Routing Indication Message is not " + "correctly formed.")); + return; + } + + auto cemi = indication.cemi(); + auto currentDstAddress = cemi.destinationAddress(); + if (currentDstAddress.type() == QKnxAddress::Type::Individual + && m_lastIndicationAddress == currentDstAddress) { + m_sameKnxDstAddressIndicationCount++; + } else { + m_lastIndicationAddress = currentDstAddress; + m_sameKnxDstAddressIndicationCount = 0; + } + + Q_Q(QKnxNetIpRoutingInterface); + emit q->routingIndicationReceived(frame, filterAction(cemi)); +} + +void QKnxNetIpRoutingInterfacePrivate::processRoutingBusy(const QKnxNetIpFrame &frame) +{ + QKnxNetIpRoutingBusyProxy busyMessage(frame); + if (!busyMessage.isValid()) + return; + + flowControlHandling(busyMessage.routingBusyWaitTime()); + + Q_Q(QKnxNetIpRoutingInterface); + emit q->routingBusyReceived(frame); +} + +void QKnxNetIpRoutingInterfacePrivate::processRoutingLostMessage(const QKnxNetIpFrame &frame) +{ + QKnxNetIpRoutingLostMessageProxy lostMessage(frame); + if (!lostMessage.isValid()) { + errorOccurred(QKnxNetIpRoutingInterface::Error::KnxRouting, + QKnxNetIpRoutingInterface::tr("QKnxNetIp Routing Lost Message is not " + "correctly formed.")); + return; + } + + Q_Q(QKnxNetIpRoutingInterface); + emit q->routingLostCountReceived(frame); +} + +bool QKnxNetIpRoutingInterfacePrivate::sendFrame(const QKnxNetIpFrame &frame) +{ + if (m_state != QKnxNetIpRoutingInterface::State::Routing) + return true; // no errors, only ignore the frame + + return m_socket->writeDatagram(frame.bytes().toByteArray(), + m_multicastAddress, + m_multicastPort) != -1; +} + +void QKnxNetIpRoutingInterfacePrivate::flowControlHandling(quint16 newBusyWaitTime) +{ + if (m_busyStage == BusyTimerStage::Wait) { + auto elapsedTime = (m_busyTimer->interval() - m_busyTimer->remainingTime()); + if (elapsedTime >= 10) + m_busyCounter++; + + if (m_busyTimer->remainingTime() < newBusyWaitTime) { + m_busyTimer->stop(); + m_busyTimer->setInterval(newBusyWaitTime); + } + } else { + m_busyTimer->stop(); + m_busyTimer->setInterval(newBusyWaitTime); + m_busyTimer->setSingleShot(true); + m_busyStage = BusyTimerStage::Wait; + } + m_busyTimer->start(); + changeState(QKnxNetIpRoutingInterface::State::NeighborBusy); +} + +QKnxNetIpRoutingInterface::FilterAction + QKnxNetIpRoutingInterfacePrivate::filterAction(const QKnxLinkLayerFrame &frame) +{ + auto dst = frame.destinationAddress(); + auto extCtrlField = frame.extendedControlField(); + auto hopCount = extCtrlField.hopCount(); + + if (dst.type() == QKnxAddress::Type::Group) { + auto gAdd = QKnxAddress::createGroup(dst.main(), dst.middle(), 0); + bool routingCondition = true; + if (m_routingMode == QKnxNetIpRoutingInterface::RoutingMode::FilterTableRouting) + routingCondition = m_filterTable.contains(gAdd); + if (m_routingMode == QKnxNetIpRoutingInterface::RoutingMode::BlockRouting) + return QKnxNetIpRoutingInterface::FilterAction::IgnoreTotally; + if (routingCondition && hopCount > 0 && hopCount < 7) + return QKnxNetIpRoutingInterface::FilterAction::RouteDecremented; + if (routingCondition && hopCount == 0) + return QKnxNetIpRoutingInterface::FilterAction::IgnoreAcked; + if (hopCount == 7) + return QKnxNetIpRoutingInterface::FilterAction::RouteUnmodified; + return QKnxNetIpRoutingInterface::FilterAction::IgnoreTotally; + } + + // only gets here if destination address is individual + if (!m_individualAddress.isValid()) + return QKnxNetIpRoutingInterface::FilterAction::IgnoreTotally; + + auto ownAddress = m_individualAddress; + bool isLineCoupler = ownAddress.middle() != 0; + if (isLineCoupler) { + // sub-line to main line routing + bool notInOwnSubnetwork = (dst.main() != ownAddress.main()) + || (dst.middle() == ownAddress.middle()); + if (notInOwnSubnetwork) { + if (dst.sub() == 0) + // address to this line coupler + return QKnxNetIpRoutingInterface::FilterAction::ForwardLocally; + if (hopCount == 7) + return QKnxNetIpRoutingInterface::FilterAction::RouteUnmodified; + if (hopCount > 0 && hopCount < 7) + return QKnxNetIpRoutingInterface::FilterAction::RouteDecremented; + return QKnxNetIpRoutingInterface::FilterAction::IgnoreAcked; + } + // no main line to sub-line routing done by a KNXnet/IP RoutingInteface + return QKnxNetIpRoutingInterface::FilterAction::IgnoreTotally; + } + + // backbone to main line + if (dst.main() == ownAddress.main()) { + if (dst.middle() == 0 && dst.sub() == 0) + // address to area coupler or backbone router. + return QKnxNetIpRoutingInterface::FilterAction::ForwardLocally; + if (hopCount == 7) + return QKnxNetIpRoutingInterface::FilterAction::RouteUnmodified; + if (hopCount > 0 && hopCount < 7) + return QKnxNetIpRoutingInterface::FilterAction::RouteDecremented; + return QKnxNetIpRoutingInterface::FilterAction::IgnoreAcked; + } + + // main line to backbone routing + if (hopCount == 7) + return QKnxNetIpRoutingInterface::FilterAction::RouteUnmodified; + if (hopCount > 0 && hopCount < 7) + return QKnxNetIpRoutingInterface::FilterAction::RouteDecremented; + return QKnxNetIpRoutingInterface::FilterAction::IgnoreAcked; +} + +QT_END_NAMESPACE diff --git a/src/knx/netip/qknxnetiproutinginterface_p.h b/src/knx/netip/qknxnetiproutinginterface_p.h new file mode 100644 index 0000000..2b1648a --- /dev/null +++ b/src/knx/netip/qknxnetiproutinginterface_p.h @@ -0,0 +1,121 @@ +/****************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** 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. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef QKNXNETIPROUTINGINTERFACE_P_H +#define QKNXNETIPROUTINGINTERFACE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt KNX API. It exists for the convenience +// of the Qt KNX implementation. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +#include +#include +#include + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QKnxNetIpRoutingInterfacePrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QKnxNetIpRoutingInterface) +public: + QKnxNetIpRoutingInterfacePrivate() = default; + ~QKnxNetIpRoutingInterfacePrivate() = default; + + void start(); + void restart(); + void stop(); + + void cleanup(); + + void processRoutingIndication(const QKnxNetIpFrame &frame); + void processRoutingBusy(const QKnxNetIpFrame &frame); + void processRoutingLostMessage(const QKnxNetIpFrame &frame); + + bool sendFrame(const QKnxNetIpFrame &frame); + + void flowControlHandling(quint16 newBusyWaitTime); + + void changeState(QKnxNetIpRoutingInterface::State state); + void errorOccurred(QKnxNetIpRoutingInterface::Error error, const QString &errorString); + + QKnxNetIpRoutingInterface::FilterAction filterAction(const QKnxLinkLayerFrame &frame); + + QUdpSocket *m_socket { nullptr }; + + QKnxAddress m_individualAddress; + + quint16 m_framesReadCount; + QKnxAddress m_lastIndicationAddress; + quint16 m_sameKnxDstAddressIndicationCount; + + QNetworkInterface m_iface; + QHostAddress m_multicastAddress { QLatin1String(QKnxNetIp::Constants::MulticastAddress) }; + quint16 m_multicastPort { QKnxNetIp::Constants::DefaultPort }; + QHostAddress m_ownAddress; + + QKnxNetIpRoutingInterface::State m_state { QKnxNetIpRoutingInterface::State::NotInit }; + + quint16 m_busyWaitTime { QKnxNetIp::RoutingBusyWaitTime }; + QTimer *m_busyTimer { nullptr }; + + enum class BusyTimerStage : quint8 + { + NotInit, + Wait, + RandomWait, + SlowDuration, + DecrementBusyCounter + }; + BusyTimerStage m_busyStage { BusyTimerStage::NotInit }; + quint32 m_busyCounter { 0 }; + + QKnxNetIpRoutingInterface::Error m_error { QKnxNetIpRoutingInterface::Error::None }; + QString m_errorMessage; + + QKnxNetIpRoutingInterface::FilterTable m_filterTable; + QKnxNetIpRoutingInterface::RoutingMode m_routingMode { QKnxNetIpRoutingInterface::RoutingMode::BlockRouting }; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/knx/netip/qknxtestingrouter_p.cpp b/src/knx/netip/qknxtestingrouter_p.cpp new file mode 100644 index 0000000..22302c3 --- /dev/null +++ b/src/knx/netip/qknxtestingrouter_p.cpp @@ -0,0 +1,67 @@ +/****************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** 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. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "qknxtestingrouter_p.h" +#include "qknxnetiproutinginterface_p.h" + +#include +#include + +QT_BEGIN_NAMESPACE + +TestingRouter *TestingRouter::instance() +{ + static TestingRouter routerInterface; + return &routerInterface; +} + +void TestingRouter::emitReadyRead(bool readAllPackets) +{ + auto socket = m_router->m_socket; + Q_ASSERT(socket); + + // By default the QKnxNetIpRoutingInterface gets the IP of the interface. A dummy + // address is assigned to trick the router into not discarding packets that + // have the same address as the interface used by the QKnxNetIpRoutingInterface + if (readAllPackets) + m_router->m_ownAddress = QHostAddress(16843010); // setting ip 1.1.1.2 + socket->waitForReadyRead(1); +} + +void TestingRouter::setRouterInstance(QKnxNetIpRoutingInterfacePrivate *router) +{ + m_router = router; +} + +QKnxNetIpRoutingInterfacePrivate *TestingRouter::routerInstance() +{ + return m_router; +} + +QT_END_NAMESPACE diff --git a/src/knx/netip/qknxtestingrouter_p.h b/src/knx/netip/qknxtestingrouter_p.h new file mode 100644 index 0000000..b6d0b76 --- /dev/null +++ b/src/knx/netip/qknxtestingrouter_p.h @@ -0,0 +1,66 @@ +/****************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** 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. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef QKNXTESTINGROUTER_P_H +#define QKNXTESTINGROUTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt KNX API. It exists for the convenience +// of the Qt KNX implementation. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include + +QT_BEGIN_NAMESPACE + +class QKnxNetIpRoutingInterfacePrivate; +class Q_AUTOTEST_EXPORT TestingRouter +{ +public: + static TestingRouter *instance(); + + void emitReadyRead(bool readAllPackets = true); + + QKnxNetIpRoutingInterfacePrivate *routerInstance(); + void setRouterInstance(QKnxNetIpRoutingInterfacePrivate *router); + +private: + TestingRouter() = default; + QKnxNetIpRoutingInterfacePrivate *m_router { nullptr }; +}; + +QT_END_NAMESPACE + +#endif diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index a598ec0..a2b49ba 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -45,4 +45,5 @@ SUBDIRS += cmake \ qknxnetiproutingsystembroadcast \ qknxnetipsecuredservicefamiliesdib \ qknxnetipsessionrequest \ - qknxnetipsessionresponse + qknxnetipsessionresponse \ + qknxnetiproutinginterface diff --git a/tests/auto/qknxnetiproutinginterface/qknxnetiproutinginterface.pro b/tests/auto/qknxnetiproutinginterface/qknxnetiproutinginterface.pro new file mode 100644 index 0000000..11f421d --- /dev/null +++ b/tests/auto/qknxnetiproutinginterface/qknxnetiproutinginterface.pro @@ -0,0 +1,7 @@ +TARGET = tst_qknxnetiproutinginterface + +QT = core testlib knx network knx-private +CONFIG += testcase c++11 + +CONFIG -= app_bundle +SOURCES += tst_qknxnetiproutinginterface.cpp diff --git a/tests/auto/qknxnetiproutinginterface/tst_qknxnetiproutinginterface.cpp b/tests/auto/qknxnetiproutinginterface/tst_qknxnetiproutinginterface.cpp new file mode 100644 index 0000000..aa20035 --- /dev/null +++ b/tests/auto/qknxnetiproutinginterface/tst_qknxnetiproutinginterface.cpp @@ -0,0 +1,515 @@ +/****************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** 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. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +Q_DECLARE_METATYPE(QKnxAddress) + +#ifdef QT_BUILD_INTERNAL + +class tst_QKnxNetIpRoutingInterface : public QObject +{ + Q_OBJECT + +public: + tst_QKnxNetIpRoutingInterface(); + +private slots: + void initTestCase(); + void cleanup(); + void test_udp_sockets(); + void test_network_interface(); + void test_multicast_address(); + void test_routing(); + void test_routing_sends_indications(); + void test_routing_receives_indications(); + void test_routing_receives_busy(); + void test_routing_busy_sent_packets_same_individual_address(); + void test_routing_filter(); + void test_routing_filter_data(); + +private: + void simulateFramesReceived(const QKnxNetIpFrame &netIpFrame, int numFrames = 1); + + bool runTests { true }; + int kMulticastPort { 3671 }; + + QNetworkInterface kIface; + QHostAddress kMulticastAddress { QString("224.0.23.12") }; + + QKnxNetIpRoutingInterface m_routingInterface; +}; + +tst_QKnxNetIpRoutingInterface::tst_QKnxNetIpRoutingInterface() +{ + // look for one interface running and able to multicast + + const auto interfaces = QNetworkInterface::allInterfaces(); + QNetworkInterface foundIface; + for (const auto &iface : interfaces) { + if (iface.addressEntries().isEmpty()) + continue; + + if (!iface.flags().testFlag(QNetworkInterface::IsRunning) + || !iface.flags().testFlag(QNetworkInterface::CanMulticast)) { + continue; + } + + kIface = iface; + } +} + +void tst_QKnxNetIpRoutingInterface::initTestCase() +{ + // don't run test case if this check fails + runTests = kIface.flags().testFlag(QNetworkInterface::IsRunning) + && kIface.flags().testFlag(QNetworkInterface::CanMulticast); + + if (!runTests) + return; + + auto interfaceIndividualAddress = QKnxAddress::createIndividual(1, 1, 0); + m_routingInterface.setIndividualAddress(interfaceIndividualAddress); + m_routingInterface.setInterfaceAffinity(kIface); +} + +void tst_QKnxNetIpRoutingInterface::cleanup() +{ + if (!runTests) + return; + + m_routingInterface.disconnect(); + m_routingInterface.stop(); +} + +void tst_QKnxNetIpRoutingInterface::test_network_interface() +{ + if (!runTests) + return; + + bool errorEmitted = false; + QObject::connect(&m_routingInterface, &QKnxNetIpRoutingInterface::errorOccurred, + [&](QKnxNetIpRoutingInterface::Error error, QString errorString){ + QCOMPARE(error, QKnxNetIpRoutingInterface::Error::Network); + QCOMPARE(error, m_routingInterface.error()); + QCOMPARE(errorString, m_routingInterface.errorString()); + QCOMPARE(m_routingInterface.state(), QKnxNetIpRoutingInterface::State::Failure); + errorEmitted = true; + }); + { + // setting invalid interface should cause error + auto m_iface = QNetworkInterface(); + m_routingInterface.setInterfaceAffinity(m_iface); + QVERIFY(errorEmitted); + } + { + // setting invalid interface using IP address of the interface + errorEmitted = false; + m_routingInterface.setInterfaceAffinity(QHostAddress("2.2.2.2")); + QVERIFY(errorEmitted); + } + { + // local host address does not allow multicast + errorEmitted = false; + m_routingInterface.setInterfaceAffinity(QHostAddress(QHostAddress::LocalHost)); + QVERIFY(errorEmitted); + } + { + // setting a valid interface no signal emitted + errorEmitted = false; + m_routingInterface.setInterfaceAffinity(kIface); + QVERIFY(!errorEmitted); + QCOMPARE(kIface.index(), m_routingInterface.interfaceAffinity().index()); + } +} + +void tst_QKnxNetIpRoutingInterface::test_multicast_address() +{ + if (!runTests) + return; + + { + // default multicast address + QKnxNetIpRoutingInterface m_router; + QCOMPARE(m_router.multicastAddress(), kMulticastAddress); + } + { + // setting an invalid multicast address shouldn't work + QKnxNetIpRoutingInterface m_router; + auto kMulticastAddress = QHostAddress(QHostAddress::LocalHost); + m_router.setMulticastAddress(kMulticastAddress); + QVERIFY(m_router.multicastAddress() != kMulticastAddress); + } + { + // setting a valid multicast address + QKnxNetIpRoutingInterface m_router; + auto address = QHostAddress("224.0.55.55"); + m_router.setMulticastAddress(address); + QCOMPARE(m_router.multicastAddress(), address); + } +} + +void tst_QKnxNetIpRoutingInterface::test_udp_sockets() +{ + if (!runTests) + return; + + QUdpSocket receiver; + QSignalSpy recvSpy(&receiver, SIGNAL(readyRead())); + receiver.bind(QHostAddress(QHostAddress::AnyIPv4), + kMulticastPort, + (QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint)); + receiver.joinMulticastGroup(kMulticastAddress); + QList datagrams = QList() + << QByteArray("0123") + << QByteArray("4567") + << QByteArray("89ab") + << QByteArray("cdef"); + + QUdpSocket sender; + QSignalSpy bytesspy(&sender, SIGNAL(bytesWritten(qint64))); + for (const QByteArray &datagram : datagrams) { + QNetworkDatagram dgram(datagram, kMulticastAddress , kMulticastPort); + sender.writeDatagram(dgram); + } + + QCOMPARE(bytesspy.count(), 4); + + receiver.waitForReadyRead(1); + for (int i = 0; i < 4; ++i) { + QCOMPARE(receiver.pendingDatagramSize(), 4); + QCOMPARE(receiver.readDatagram(0, 0), qint64(0)); + QCOMPARE(recvSpy.count(), 1); + } +} + +QKnxNetIpFrame dummyRoutingIndication(QKnxAddress dst = QKnxAddress::createIndividual(1,1,1), + quint8 hopCount = 6) +{ + //auto dst = QKnxAddress::createIndividual(1,1,1); + auto tpdu = QKnxTpduFactory::Multicast::createGroupValueReadTpdu(); + auto ctrl = QKnxControlField::builder() + .setFrameFormat(QKnxControlField::FrameFormat::Standard) + .setBroadcast(QKnxControlField::Broadcast::Domain) + .setPriority(QKnxControlField::Priority::Normal) + .create(); + + auto extCtrl = QKnxExtendedControlField::builder() + .setDestinationAddressType(dst.type()) + .setHopCount(hopCount) + .create(); + + auto frame = QKnxLinkLayerFrame::builder() + .setControlField(ctrl) + .setExtendedControlField(extCtrl) + .setTpdu(tpdu) + .setDestinationAddress(dst) + .setSourceAddress({ QKnxAddress::Type::Individual, 0 }) + .setMessageCode(QKnxLinkLayerFrame::MessageCode::DataIndication) + .setMedium(QKnx::MediumType::NetIP) + .createFrame(); + return QKnxNetIpRoutingIndicationProxy::builder() + .setLinkLayerFrame(frame) + .create(); +} + +void tst_QKnxNetIpRoutingInterface::simulateFramesReceived(const QKnxNetIpFrame &netIpFrame, int numFrames) +{ + if (!runTests) + return; + + QUdpSocket * s = new QUdpSocket(); + + s->bind(QHostAddress(QHostAddress::AnyIPv4), 0); + s->setMulticastInterface(kIface); + + for (; numFrames > 0 ; --numFrames) { + QNetworkDatagram datagram(netIpFrame.bytes().toByteArray(), + kMulticastAddress, kMulticastPort); + s->writeDatagram(datagram); + } + + // notify router socket to wait for any received packet + TestingRouter::instance()->emitReadyRead(); + + if (s) { + s->disconnect(); + s->deleteLater(); + } +} + +void tst_QKnxNetIpRoutingInterface::test_routing() +{ + if (!runTests) + return; + + bool stateChangedEmitted = false; + QObject::connect(&m_routingInterface, &QKnxNetIpRoutingInterface::stateChanged + , [&](QKnxNetIpRoutingInterface::State state) { + stateChangedEmitted = true; + QCOMPARE(state, QKnxNetIpRoutingInterface::State::Routing); + }); + + m_routingInterface.start(); + QVERIFY(stateChangedEmitted); +} + + +void tst_QKnxNetIpRoutingInterface::test_routing_sends_indications() +{ + if (!runTests) + return; + + m_routingInterface.start(); + + auto bytes = QKnxByteArray::fromHex("2900b4e000000002010000"); + auto frameSent = QKnxLinkLayerFrame::builder() + .setData(bytes) + .setMedium(QKnx::MediumType::NetIP) + .createFrame(); + + bool indicationSentEmitted = false; + QObject::connect(&m_routingInterface, &QKnxNetIpRoutingInterface::routingIndicationSent + , [&](QKnxNetIpFrame frame) { + indicationSentEmitted = true; + QKnxNetIpRoutingIndicationProxy indicationSent(frame); + QVERIFY(indicationSent.isValid()); + QCOMPARE(indicationSent.linkLayerFrame().bytes(), frameSent.bytes()); + }); + m_routingInterface.sendRoutingIndication(frameSent); + QVERIFY(indicationSentEmitted); + + bool stateChangedEmitted = false; + QObject::connect(&m_routingInterface, &QKnxNetIpRoutingInterface::stateChanged + , [&](QKnxNetIpRoutingInterface::State state) { + stateChangedEmitted = true; + QCOMPARE(state, QKnxNetIpRoutingInterface::State::Stop); + }); + m_routingInterface.stop(); + QVERIFY(stateChangedEmitted); +} + +// Test receiving routing indications! +void tst_QKnxNetIpRoutingInterface::test_routing_receives_indications() +{ + if (!runTests) + return; + + m_routingInterface.start(); + + bool indicationRcvEmitted = false; + QObject::connect(&m_routingInterface, &QKnxNetIpRoutingInterface::routingIndicationReceived + , [&](QKnxNetIpFrame frame + , QKnxNetIpRoutingInterface::FilterAction routingAction) { + indicationRcvEmitted = true; + QKnxNetIpRoutingIndicationProxy indicationRcv(frame); + QVERIFY(indicationRcv.isValid()); + QCOMPARE(routingAction, QKnxNetIpRoutingInterface::FilterAction::RouteDecremented); + }); + simulateFramesReceived(dummyRoutingIndication()); + QVERIFY(indicationRcvEmitted); +} + +void tst_QKnxNetIpRoutingInterface::test_routing_receives_busy() +{ + if (!runTests) + return; + + // test receiving routing busy! + m_routingInterface.start(); + + bool indicationRcvEmitted = false; + QObject::connect(&m_routingInterface, &QKnxNetIpRoutingInterface::routingBusyReceived + , [&](QKnxNetIpFrame frame) { + indicationRcvEmitted = true; + QKnxNetIpRoutingBusyProxy busy(frame); + QVERIFY(busy.isValid()); + }); + + auto routingBusyFrame = QKnxNetIpRoutingBusyProxy::builder() + .setDeviceState(QKnxNetIp::DeviceState::IpFault) + .setRoutingBusyWaitTime(50) + .setRoutingBusyControl(0) + .create(); + simulateFramesReceived(routingBusyFrame); + QVERIFY(indicationRcvEmitted); +} + +void tst_QKnxNetIpRoutingInterface::test_routing_busy_sent_packets_same_individual_address() +{ + if (!runTests) + return; + + // test receiving routing indications! + m_routingInterface.start(); + + int indRecvCount = 0; + QObject::connect(&m_routingInterface, &QKnxNetIpRoutingInterface::routingIndicationReceived + , [&](QKnxNetIpFrame frame + , QKnxNetIpRoutingInterface::FilterAction routingAction) { + QKnxNetIpRoutingIndicationProxy indicationRcv(frame); + QVERIFY(indicationRcv.isValid()); + QCOMPARE(routingAction, QKnxNetIpRoutingInterface::FilterAction::RouteDecremented); + indRecvCount++; + }); + simulateFramesReceived(dummyRoutingIndication(), 7); + + // The 7th packet is ignored because there were 5 consecutive packets with + // the same individual address. Router shall send automatically a busy message + QCOMPARE(indRecvCount, 6); + + QCOMPARE(TestingRouter::instance()->routerInstance()->m_framesReadCount, 6); + QCOMPARE(TestingRouter::instance()->routerInstance()->m_sameKnxDstAddressIndicationCount, 5); + + QCOMPARE(m_routingInterface.state(), QKnxNetIpRoutingInterface::State::NeighborBusy); +} + +void tst_QKnxNetIpRoutingInterface::test_routing_filter() +{ + if (!runTests) + return; + + QFETCH(QKnxAddress, interfaceIndividualAddress); + QFETCH(QKnxNetIpRoutingInterface::FilterAction, expectedRoutingAction); + QFETCH(QKnxAddress, dst); + QFETCH(int, hopCount); + QFETCH(QKnxNetIpRoutingInterface::FilterTable, filterTable); + + QKnxNetIpRoutingInterface m_routingInterface; + m_routingInterface.setIndividualAddress(interfaceIndividualAddress); + m_routingInterface.setInterfaceAffinity(kIface); + m_routingInterface.setFilterTable(filterTable); + m_routingInterface.start(); + + bool receivedIndication = false; + + QObject::connect(&m_routingInterface, &QKnxNetIpRoutingInterface::routingIndicationReceived + , [&](QKnxNetIpFrame frame + , QKnxNetIpRoutingInterface::FilterAction routingAction) { + QKnxNetIpRoutingIndicationProxy indicationRcv(frame); + QVERIFY(indicationRcv.isValid()); + QCOMPARE(routingAction, expectedRoutingAction); + receivedIndication = true; + }); + + + auto frame = dummyRoutingIndication(dst, hopCount); + simulateFramesReceived(frame); + QVERIFY(receivedIndication); +} + +void tst_QKnxNetIpRoutingInterface::test_routing_filter_data() +{ + QTest::addColumn("interfaceIndividualAddress"); + QTest::addColumn("dst"); + QTest::addColumn("hopCount"); + QTest::addColumn("expectedRoutingAction"); + QTest::addColumn("filterTable"); + + QKnxNetIpRoutingInterface::FilterTable filterTable; + filterTable << QKnxAddress::createGroup(1,1,0); + QTest::newRow("RouterIndividual(1.1.0)_groupDestination(1.1.1)_hop6") + << QKnxAddress::createIndividual(1,1,0) + << QKnxAddress::createGroup(1,1,1) + << 6 + << QKnxNetIpRoutingInterface::FilterAction::RouteDecremented + << filterTable; + + QTest::newRow("destinationAddressFiltered") + << QKnxAddress::createIndividual(1,1,0) + << QKnxAddress::createGroup(1,2,1) + << 6 + << QKnxNetIpRoutingInterface::FilterAction::IgnoreTotally + << filterTable; + + QTest::newRow("RouterIndividual(1.1.0)_groupDestination(1.1.1)_hop0") + << QKnxAddress::createIndividual(1,1,0) + << QKnxAddress::createGroup(1,1,1) + << 0 + << QKnxNetIpRoutingInterface::FilterAction::IgnoreAcked + << filterTable; + + QTest::newRow("RouterIndividual(1.1.0)_individualDestination(1.1.1)_hop0") + << QKnxAddress::createIndividual(1,1,0) + << QKnxAddress::createIndividual(1,1,1) + << 0 + << QKnxNetIpRoutingInterface::FilterAction::IgnoreAcked + << filterTable; + + QTest::newRow("RouterIndividual(1.1.0)_individualDestination(1.1.1)_hop5") + << QKnxAddress::createIndividual(1,1,0) + << QKnxAddress::createIndividual(1,1,1) + << 5 + << QKnxNetIpRoutingInterface::FilterAction::RouteDecremented + << filterTable; + + QTest::newRow("RouterIndividual(1.1.0)_individualDestination(1.1.1)_hop5") + << QKnxAddress::createIndividual(1,1,0) + << QKnxAddress::createIndividual(1,1,0) + << 5 + << QKnxNetIpRoutingInterface::FilterAction::ForwardLocally + << filterTable; +} + +//TODO: test threshold for sending busy after incoming queue is +// filled with 10 packets (queue should be able to hold until 30 messages) + +#else + +class tst_QKnxNetIpRoutingInterface : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase() + { + QSKIP("QKnxNetIpRoutingInterface methods aren't available to test"); + } +}; + +#endif + +QTEST_MAIN(tst_QKnxNetIpRoutingInterface) + +#include "tst_qknxnetiproutinginterface.moc" -- cgit v1.2.3