summaryrefslogtreecommitdiffstats
path: root/src/plugins/canbus/virtualcan
diff options
context:
space:
mode:
authorAndre Hartmann <aha_1980@gmx.de>2017-12-20 22:28:16 +0100
committerAndré Hartmann <aha_1980@gmx.de>2018-08-01 17:18:34 +0000
commit13d7ca49093a2b7d60ccee703c0d45542e4990de (patch)
tree7ada6acc9e5e3111e63579857fcda74fb8b9e64e /src/plugins/canbus/virtualcan
parentfcf126812561310a29aab86b668205fe3f4eaab5 (diff)
Add a virtual CAN bus plugin
Based on a TCP/IP server/client infrastructure, to allow testing programs against a second program without CAN hardware connected. [ChangeLog][QCanBus] Added a generic virtual CAN bus plugin. Task-number: QTBUG-61837 Change-Id: I7acf04bd476b65b4d1d04254463d61a4dc9042d5 Reviewed-by: Alex Blasche <alexander.blasche@qt.io>
Diffstat (limited to 'src/plugins/canbus/virtualcan')
-rw-r--r--src/plugins/canbus/virtualcan/main.cpp76
-rw-r--r--src/plugins/canbus/virtualcan/plugin.json3
-rw-r--r--src/plugins/canbus/virtualcan/virtualcan.pro17
-rw-r--r--src/plugins/canbus/virtualcan/virtualcanbackend.cpp362
-rw-r--r--src/plugins/canbus/virtualcan/virtualcanbackend.h106
5 files changed, 564 insertions, 0 deletions
diff --git a/src/plugins/canbus/virtualcan/main.cpp b/src/plugins/canbus/virtualcan/main.cpp
new file mode 100644
index 0000000..287b95a
--- /dev/null
+++ b/src/plugins/canbus/virtualcan/main.cpp
@@ -0,0 +1,76 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 Andre Hartmann <aha_1980@gmx.de>
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the QtSerialBus module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** 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 http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "virtualcanbackend.h"
+
+#include <QtSerialBus/qcanbus.h>
+#include <QtSerialBus/qcanbusdevice.h>
+#include <QtSerialBus/qcanbusfactory.h>
+
+#include <QtCore/qloggingcategory.h>
+
+QT_BEGIN_NAMESPACE
+
+Q_LOGGING_CATEGORY(QT_CANBUS_PLUGINS_VIRTUALCAN, "qt.canbus.plugins.virtualcan")
+
+class VirtualCanBusPlugin : public QObject, public QCanBusFactoryV2
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QCanBusFactory" FILE "plugin.json")
+ Q_INTERFACES(QCanBusFactoryV2)
+
+public:
+ QList<QCanBusDeviceInfo> availableDevices(QString *errorMessage) const override
+ {
+ if (errorMessage != nullptr)
+ errorMessage->clear();
+
+ return VirtualCanBackend::interfaces();
+ }
+
+ QCanBusDevice *createDevice(const QString &interfaceName, QString *errorMessage) const override
+ {
+ if (errorMessage)
+ errorMessage->clear();
+
+ auto device = new VirtualCanBackend(interfaceName);
+ return device;
+ }
+};
+
+QT_END_NAMESPACE
+
+#include "main.moc"
diff --git a/src/plugins/canbus/virtualcan/plugin.json b/src/plugins/canbus/virtualcan/plugin.json
new file mode 100644
index 0000000..4b13f6e
--- /dev/null
+++ b/src/plugins/canbus/virtualcan/plugin.json
@@ -0,0 +1,3 @@
+{
+ "Key": "virtualcan"
+}
diff --git a/src/plugins/canbus/virtualcan/virtualcan.pro b/src/plugins/canbus/virtualcan/virtualcan.pro
new file mode 100644
index 0000000..7b5504d
--- /dev/null
+++ b/src/plugins/canbus/virtualcan/virtualcan.pro
@@ -0,0 +1,17 @@
+TARGET = qtvirtualcanbus
+
+QT = core network serialbus
+
+HEADERS += \
+ virtualcanbackend.h
+
+SOURCES += \
+ main.cpp \
+ virtualcanbackend.cpp
+
+DISTFILES = plugin.json
+
+PLUGIN_TYPE = canbus
+PLUGIN_EXTENDS = serialbus
+PLUGIN_CLASS_NAME = VirtualCanBusPlugin
+load(qt_plugin)
diff --git a/src/plugins/canbus/virtualcan/virtualcanbackend.cpp b/src/plugins/canbus/virtualcan/virtualcanbackend.cpp
new file mode 100644
index 0000000..2ba13ce
--- /dev/null
+++ b/src/plugins/canbus/virtualcan/virtualcanbackend.cpp
@@ -0,0 +1,362 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 Andre Hartmann <aha_1980@gmx.de>
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the QtSerialBus module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** 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 http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "virtualcanbackend.h"
+
+#include <QtCore/qdatetime.h>
+#include <QtCore/qloggingcategory.h>
+#include <QtCore/qregularexpression.h>
+
+#include <QtNetwork/qtcpserver.h>
+#include <QtNetwork/qtcpsocket.h>
+
+QT_BEGIN_NAMESPACE
+
+Q_DECLARE_LOGGING_CATEGORY(QT_CANBUS_PLUGINS_VIRTUALCAN)
+
+enum {
+ ServerDefaultTcpPort = 35468,
+ VirtualChannels = 2
+};
+
+static const char RemoteRequestFlag = 'R';
+static const char ExtendedFormatFlag = 'X';
+static const char FlexibleDataRateFlag = 'F';
+static const char BitRateSwitchFlag = 'B';
+static const char ErrorStateFlag = 'E';
+static const char LocalEchoFlag = 'L';
+
+VirtualCanServer::VirtualCanServer(QObject *parent)
+ : QObject(parent)
+{
+ qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN, "Server [%p]: constructed.", this);
+}
+
+VirtualCanServer::~VirtualCanServer()
+{
+ qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN, "Server [%p]: destructed.", this);
+}
+
+void VirtualCanServer::start(quint16 port)
+{
+ // If there is already a server object, return immediately
+ if (m_server) {
+ qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN, "Server [%p] is already running.", this);
+ return;
+ }
+
+ // Otherwise try to start a new server. If there is already
+ // another server listen on the specified port, give up.
+ m_server = new QTcpServer(this);
+ if (!m_server->listen(QHostAddress::LocalHost, port)) {
+ qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN,
+ "Server [%p] could not be started, port %d is already in use.", this, port);
+ m_server->deleteLater();
+ m_server = nullptr;
+ return;
+ }
+
+ // Server successfully started
+ connect(m_server, &QTcpServer::newConnection, this, &VirtualCanServer::connected);
+ qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN,
+ "Server [%p] started and listening on port %d.", this, port);
+ return;
+}
+
+void VirtualCanServer::connected()
+{
+ while (m_server->hasPendingConnections()) {
+ qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN, "Server [%p]: client connected.", this);
+ QTcpSocket *next = m_server->nextPendingConnection();
+ m_serverSockets.append(next);
+ connect(next, &QIODevice::readyRead, this, &VirtualCanServer::readyRead);
+ connect(next, &QTcpSocket::disconnected, this, &VirtualCanServer::disconnected);
+ }
+}
+
+void VirtualCanServer::disconnected()
+{
+ qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN, "Server [%p]: client disconnected.", this);
+
+ auto socket = qobject_cast<QTcpSocket *>(sender());
+ Q_ASSERT(socket);
+
+ m_serverSockets.removeOne(socket);
+ socket->deleteLater();
+}
+
+void VirtualCanServer::readyRead()
+{
+ auto readSocket = qobject_cast<QTcpSocket *>(sender());
+ Q_ASSERT(readSocket);
+
+ while (readSocket->canReadLine()) {
+ const QByteArray command = readSocket->readLine().trimmed();
+ qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN,
+ "Server [%p] received: '%s'.", this, command.constData());
+
+ if (command.startsWith("connect:")) {
+ const QVariant interfaces = readSocket->property("interfaces");
+ QStringList list = interfaces.toStringList();
+ list.append(command.mid(int(strlen("connect:"))));
+ readSocket->setProperty("interfaces", list);
+
+ } else if (command.startsWith("disconnect:")) {
+ const QVariant interfaces = readSocket->property("interfaces");
+ QStringList list = interfaces.toStringList();
+ list.removeAll(command.mid(int(strlen("disconnect:"))));
+ readSocket->setProperty("interfaces", list);
+
+ } else {
+ const QByteArrayList commandList = command.split(':');
+ Q_ASSERT(commandList.size() == 2);
+
+ for (QTcpSocket *writeSocket : qAsConst(m_serverSockets)) {
+ // Don't send the frame back to its origin
+ if (writeSocket == readSocket)
+ continue;
+
+ // Send frame to all clients registered to the same interface as sender
+ const QVariant property = writeSocket->property("interfaces");
+ if (!property.isValid())
+ continue;
+
+ const QStringList propertyList = property.toStringList();
+ if (propertyList.contains(commandList.first()))
+ writeSocket->write(commandList.last() + '\n');
+ }
+ }
+ }
+}
+
+Q_GLOBAL_STATIC(VirtualCanServer, g_server)
+
+VirtualCanBackend::VirtualCanBackend(const QString &interface, QObject *parent)
+ : QCanBusDevice(parent)
+{
+ m_url = QUrl(interface);
+ const QString canDevice = m_url.fileName();
+
+ const QRegularExpression re(QStringLiteral("can(\\d)"));
+ const QRegularExpressionMatch match = re.match(canDevice);
+
+ if (Q_UNLIKELY(!match.hasMatch())) {
+ qCWarning(QT_CANBUS_PLUGINS_VIRTUALCAN,
+ "Invalid interface '%ls'.", qUtf16Printable(interface));
+ setError(tr("Invalid interface '%1'.").arg(interface), QCanBusDevice::ConnectionError);
+ return;
+ }
+
+ const uint channel = match.captured(1).toUInt();
+ if (Q_UNLIKELY(channel >= VirtualChannels)) {
+ qCWarning(QT_CANBUS_PLUGINS_VIRTUALCAN,
+ "Invalid interface '%ls'.", qUtf16Printable(interface));
+ setError(tr("Invalid interface '%1'.").arg(interface), QCanBusDevice::ConnectionError);
+ return;
+ }
+
+ m_channel = channel;
+}
+
+VirtualCanBackend::~VirtualCanBackend()
+{
+ qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN, "Client [%p] socket destructed.", this);
+}
+
+bool VirtualCanBackend::open()
+{
+ setState(QCanBusDevice::ConnectingState);
+
+ const QString host = m_url.host();
+ const QHostAddress address = host.isEmpty() ? QHostAddress::LocalHost : QHostAddress(host);
+ const quint16 port = static_cast<quint16>(m_url.port(ServerDefaultTcpPort));
+
+ if (address.isLoopback())
+ g_server->start(port);
+
+ m_clientSocket = new QTcpSocket(this);
+ m_clientSocket->connectToHost(address, port, QIODevice::ReadWrite);
+ connect(m_clientSocket, &QAbstractSocket::connected, this, &VirtualCanBackend::clientConnected);
+ connect(m_clientSocket, &QAbstractSocket::disconnected, this, &VirtualCanBackend::clientDisconnected);
+ connect(m_clientSocket, &QIODevice::readyRead, this, &VirtualCanBackend::clientReadyRead);
+ qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN, "Client [%p] socket created.", this);
+ return true;
+}
+
+void VirtualCanBackend::close()
+{
+ setState(ClosingState);
+
+ m_clientSocket->write("disconnect:can" + QByteArray::number(m_channel) + '\n');
+}
+
+void VirtualCanBackend::setConfigurationParameter(int key, const QVariant &value)
+{
+ if (key == QCanBusDevice::ReceiveOwnKey || key == QCanBusDevice::CanFdKey)
+ QCanBusDevice::setConfigurationParameter(key, value);
+}
+
+/*
+ Protocol format: All data is in ASCII, one CAN message per line,
+ each line ends with line feed '\n'.
+
+ Format: "<CAN-Channel>:<Flags>#<CAN-ID>#<Data-Bytes>\n"
+ Example: "can0:XF#123#123456\n"
+
+ The first part is the destination CAN channel, "can0" or "can1",
+ followed by the flags list:
+
+ * R - Remote Request
+ * X - Extended Frame Format
+ * F - Flexible Data Rate Format
+ * B - Bitrate Switch
+ * E - Error State Indicator
+ * L - Local Echo
+
+ Afterwards the CAN-ID and the data follows, both separated by '#'.
+*/
+
+bool VirtualCanBackend::writeFrame(const QCanBusFrame &frame)
+{
+ if (Q_UNLIKELY(state() != ConnectedState)) {
+ qCWarning(QT_CANBUS_PLUGINS_VIRTUALCAN, "Error: Cannot write frame as client is not connected!");
+ return false;
+ }
+
+ bool canFdEnabled = configurationParameter(QCanBusDevice::CanFdKey).toBool();
+ if (Q_UNLIKELY(frame.hasFlexibleDataRateFormat() && !canFdEnabled)) {
+ qCWarning(QT_CANBUS_PLUGINS_VIRTUALCAN,
+ "Error: Cannot write CAN FD frame as CAN FD is not enabled!");
+ return false;
+ }
+
+ QByteArray flags;
+ if (frame.frameType() == QCanBusFrame::RemoteRequestFrame)
+ flags.append(RemoteRequestFlag);
+ if (frame.hasExtendedFrameFormat())
+ flags.append(ExtendedFormatFlag);
+ if (frame.hasFlexibleDataRateFormat())
+ flags.append(FlexibleDataRateFlag);
+ if (frame.hasBitrateSwitch())
+ flags.append(BitRateSwitchFlag);
+ if (frame.hasErrorStateIndicator())
+ flags.append(ErrorStateFlag);
+ if (frame.hasLocalEcho())
+ flags.append(LocalEchoFlag);
+ const QByteArray frameId = QByteArray::number(frame.frameId());
+ const QByteArray command = "can" + QByteArray::number(m_channel)
+ + ':' + frameId + '#' + flags + '#' + frame.payload().toHex() + '\n';
+ m_clientSocket->write(command);
+
+ if (configurationParameter(QCanBusDevice::ReceiveOwnKey).toBool()) {
+ const qint64 timeStamp = QDateTime::currentDateTime().toMSecsSinceEpoch();
+ QCanBusFrame echoFrame = frame;
+ echoFrame.setLocalEcho(true);
+ echoFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(timeStamp * 1000));
+ enqueueReceivedFrames({echoFrame});
+ }
+
+ return true;
+}
+
+QString VirtualCanBackend::interpretErrorFrame(const QCanBusFrame &errorFrame)
+{
+ Q_UNUSED(errorFrame);
+ return QString();
+}
+
+QList<QCanBusDeviceInfo> VirtualCanBackend::interfaces()
+{
+ QList<QCanBusDeviceInfo> result;
+
+ for (int channel = 0; channel < VirtualChannels; ++channel) {
+ result.append(std::move(createDeviceInfo(
+ QStringLiteral("can%1").arg(channel), QString(),
+ QStringLiteral("Qt Virtual CAN bus"), channel,
+ true, true)));
+ }
+
+ return result;
+}
+
+void VirtualCanBackend::clientConnected()
+{
+ qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN, "Client [%p] socket connected.", this);
+ m_clientSocket->write("connect:can" + QByteArray::number(m_channel) + '\n');
+
+ setState(QCanBusDevice::ConnectedState);
+}
+
+void VirtualCanBackend::clientDisconnected()
+{
+ qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN, "Client [%p] socket disconnected.", this);
+
+ setState(UnconnectedState);
+}
+
+void VirtualCanBackend::clientReadyRead()
+{
+ while (m_clientSocket->canReadLine()) {
+ const QByteArray answer = m_clientSocket->readLine().trimmed();
+ qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN, "Client [%p] received: '%s'.",
+ this, answer.constData());
+
+ if (answer.startsWith("disconnect:can" + QByteArray::number(m_channel))) {
+ m_clientSocket->disconnectFromHost();
+ continue;
+ }
+
+ const QByteArrayList list = answer.split('#');
+ Q_ASSERT(list.size() == 3);
+
+ const quint32 id = list.at(0).toUInt();
+ const QByteArray flags = list.at(1);
+ const QByteArray data = QByteArray::fromHex(list.at(2));
+ const qint64 timeStamp = QDateTime::currentDateTime().toMSecsSinceEpoch();
+ QCanBusFrame frame(id, data);
+ frame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(timeStamp * 1000));
+ if (flags.contains(RemoteRequestFlag))
+ frame.setFrameType(QCanBusFrame::RemoteRequestFrame);
+ frame.setExtendedFrameFormat(flags.contains(ExtendedFormatFlag));
+ frame.setFlexibleDataRateFormat(flags.contains(FlexibleDataRateFlag));
+ frame.setBitrateSwitch(flags.contains(BitRateSwitchFlag));
+ frame.setErrorStateIndicator(flags.contains(ErrorStateFlag));
+ frame.setLocalEcho(flags.contains(LocalEchoFlag));
+ enqueueReceivedFrames({frame});
+ }
+}
+
+QT_END_NAMESPACE
diff --git a/src/plugins/canbus/virtualcan/virtualcanbackend.h b/src/plugins/canbus/virtualcan/virtualcanbackend.h
new file mode 100644
index 0000000..c83b568
--- /dev/null
+++ b/src/plugins/canbus/virtualcan/virtualcanbackend.h
@@ -0,0 +1,106 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 Andre Hartmann <aha_1980@gmx.de>
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the QtSerialBus module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** 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 http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef VIRTUALCANBACKEND_H
+#define VIRTUALCANBACKEND_H
+
+#include <QtSerialBus/qcanbusframe.h>
+#include <QtSerialBus/qcanbusdevice.h>
+#include <QtSerialBus/qcanbusdeviceinfo.h>
+
+#include <QtCore/qlist.h>
+#include <QtCore/qurl.h>
+#include <QtCore/qvariant.h>
+#include <QtCore/qvector.h>
+
+QT_BEGIN_NAMESPACE
+
+class QTcpServer;
+class QTcpSocket;
+
+class VirtualCanServer : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(VirtualCanServer)
+
+public:
+ explicit VirtualCanServer(QObject *parent = nullptr);
+ ~VirtualCanServer() override;
+
+ void start(quint16 port);
+
+private:
+ void connected();
+ void disconnected();
+ void readyRead();
+
+ QTcpServer *m_server = nullptr;
+ QList<QTcpSocket *> m_serverSockets;
+};
+
+class VirtualCanBackend : public QCanBusDevice
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(VirtualCanBackend)
+
+public:
+ explicit VirtualCanBackend(const QString &interface, QObject *parent = nullptr);
+ ~VirtualCanBackend() override;
+
+ bool open() override;
+ void close() override;
+
+ void setConfigurationParameter(int key, const QVariant &value) override;
+
+ bool writeFrame(const QCanBusFrame &frame) override;
+
+ QString interpretErrorFrame(const QCanBusFrame &errorFrame) override;
+
+ static QList<QCanBusDeviceInfo> interfaces();
+
+private:
+ void clientConnected();
+ void clientDisconnected();
+ void clientReadyRead();
+
+ QUrl m_url;
+ uint m_channel = 0;
+ QTcpSocket *m_clientSocket = nullptr;
+};
+
+QT_END_NAMESPACE
+
+#endif // VIRTUALCANBACKEND_H